Compare commits

..
Author SHA1 Message Date
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
58 changed files with 2917 additions and 3222 deletions

No files matched your search

@@ -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();
}
}
@@ -18,9 +18,9 @@ namespace FluentTerminal.App.Services
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();
@@ -23,7 +23,8 @@ namespace FluentTerminal.App.Services.Implementation
AlwaysShowTabs = false,
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false,
EnableTrayIcon = true
EnableTrayIcon = true,
ShowCustomTitleInTitlebar = true
};
}
@@ -5,7 +5,6 @@ using FluentTerminal.Models.Responses;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Implementation
@@ -14,7 +13,7 @@ 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;
@@ -22,7 +21,7 @@ namespace FluentTerminal.App.Services.Implementation
public TrayProcessCommunicationService(ISettingsService settingsService)
{
_settingsService = settingsService;
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
_terminalOutputHandlers = new Dictionary<int, Action<byte[]>>();
}
public int GetNextTerminalId()
@@ -35,8 +34,11 @@ namespace FluentTerminal.App.Services.Implementation
var request = new GetAvailablePortRequest();
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
return 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)
@@ -49,9 +51,14 @@ namespace FluentTerminal.App.Services.Implementation
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)
@@ -75,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);
}
}
@@ -97,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;
}
@@ -114,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));
@@ -132,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);
}
}
}
+12 -4
View File
@@ -13,9 +13,18 @@ namespace FluentTerminal.App.Services
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>
@@ -29,7 +38,7 @@ namespace FluentTerminal.App.Services
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<string> OutputReceived;
public event EventHandler<byte[]> OutputReceived;
/// <summary>
/// to be observed by viewmodel
@@ -51,7 +60,6 @@ namespace FluentTerminal.App.Services
public async Task Close()
{
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
Closed?.Invoke(this, System.EventArgs.Empty);
}
/// <summary>
@@ -118,9 +126,9 @@ namespace FluentTerminal.App.Services
}
}
public Task Write(string text)
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.WriteText(Id, text);
return _trayProcessCommunicationService.Write(Id, data);
}
}
}
@@ -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.Close();
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.Close();
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);
});
}
}
}
@@ -7,6 +7,7 @@ using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
@@ -16,17 +17,19 @@ namespace FluentTerminal.App.ViewModels
private readonly IKeyboardCommandService _keyboardCommandService;
private readonly IDispatcherTimer _resizeOverlayTimer;
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 TerminalTheme _terminalTheme;
private string _title;
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)
{
SettingsService = settingsService;
@@ -40,7 +43,6 @@ namespace FluentTerminal.App.ViewModels
DialogService = dialogService;
_keyboardCommandService = keyboardCommandService;
ApplicationSettings = applicationSettings;
StartupDirectory = startupDirectory;
ApplicationView = applicationView;
ClipboardService = clipboardService;
@@ -61,11 +63,6 @@ namespace FluentTerminal.App.ViewModels
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
EditTitleCommand = new AsyncCommand(EditTitle);
if (!string.IsNullOrWhiteSpace(StartupDirectory))
{
ShellProfile.WorkingDirectory = StartupDirectory;
}
Terminal = new Terminal(TrayProcessCommunicationService);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
Terminal.OutputReceived += Terminal_OutputReceived;
@@ -82,6 +79,8 @@ namespace FluentTerminal.App.ViewModels
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; }
@@ -119,8 +118,7 @@ namespace FluentTerminal.App.ViewModels
{
if (IsSelected)
{
ApplicationView.Title = Title ?? string.Empty;
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
@@ -178,8 +176,6 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _showSearchPanel, value);
}
public string StartupDirectory { get; }
public TabTheme TabTheme
{
get => _tabTheme;
@@ -201,14 +197,26 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _terminalTheme, value);
}
public string Title
public string TabTitle
{
get => _title;
get => _tabTitle;
set
{
if (Set(ref _title, value) && IsSelected)
if (Set(ref _tabTitle, value))
{
ApplicationView.Title = Title;
CustomTitleChanged?.Invoke(this, value);
}
}
}
public string ShellTitle
{
get => _shellTitle;
set
{
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
{
TabTitle = value;
}
}
}
@@ -217,6 +225,10 @@ namespace FluentTerminal.App.ViewModels
public Task Close()
{
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
return Terminal.Close();
}
@@ -230,7 +242,16 @@ namespace FluentTerminal.App.ViewModels
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;
}
}
}
@@ -280,9 +301,9 @@ namespace FluentTerminal.App.ViewModels
});
}
private void OnKeyBindingsChanged(object sender, EventArgs e)
private async void OnKeyBindingsChanged(object sender, EventArgs e)
{
KeyBindingsChanged?.Invoke(this, EventArgs.Empty);
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
}
private void OnResizeOverlayTimerFinished(object sender, object e)
@@ -291,9 +312,9 @@ namespace FluentTerminal.App.ViewModels
ShowResizeOverlay = false;
}
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
OptionsChanged?.Invoke(this, e);
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
}
private void SelectTabTheme(string name)
@@ -303,7 +324,7 @@ namespace FluentTerminal.App.ViewModels
private void Terminal_Closed(object sender, EventArgs e)
{
Closed?.Invoke(this, EventArgs.Empty);
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
@@ -322,7 +343,7 @@ namespace FluentTerminal.App.ViewModels
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(content).ConfigureAwait(true);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
}
@@ -332,7 +353,7 @@ namespace FluentTerminal.App.ViewModels
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await Terminal.Write(content).ConfigureAwait(true);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
}
@@ -353,11 +374,11 @@ namespace FluentTerminal.App.ViewModels
}
}
private void Terminal_OutputReceived(object sender, string e)
private void Terminal_OutputReceived(object sender, byte[] e)
{
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
NewOutput = true;
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
}
}
@@ -369,7 +390,8 @@ namespace FluentTerminal.App.ViewModels
private void Terminal_TitleChanged(object sender, string e)
{
Title = e;
ShellTitle = e;
ShellTitleChanged?.Invoke(this, e);
}
private async Task TryClose()
@@ -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>
+174 -49
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()
{
@@ -93,14 +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;
});
}
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
{
System.Diagnostics.Debug.WriteLine(e.Exception.ToString());
Logger.Instance.Error(e.Exception, "Unhandled Exception");
}
protected override async void OnActivated(IActivatedEventArgs args)
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)
{
@@ -109,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);
}
}
});
}
}
@@ -152,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);
}
}
@@ -220,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;
@@ -243,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)
@@ -271,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);
}
@@ -281,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)
@@ -307,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();
}
}
}
+18 -15
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>
@@ -187,9 +193,6 @@
<Compile Include="Views\TerminalView.xaml.cs">
<DependentUpon>TerminalView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\VtNetCoreTerminalView.xaml.cs">
<DependentUpon>VtNetCoreTerminalView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\XtermTerminalView.xaml.cs">
<DependentUpon>XtermTerminalView.xaml</DependentUpon>
</Compile>
@@ -209,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" />
@@ -357,10 +364,6 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\VtNetCoreTerminalView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\XtermTerminalView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@@ -368,25 +371,25 @@
</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>
</PackageReference>
<PackageReference Include="VtNetCore">
<Version>1.0.15</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.0.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();
}
}
}
+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"
@@ -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"
@@ -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"
@@ -21,8 +21,7 @@ namespace FluentTerminal.App.Views
ViewModel.FindNextRequested += OnFindNextRequested;
ViewModel.FindPreviousRequested += OnFindPreviousRequested;
InitializeComponent();
//_terminalView = new XtermTerminalView();
_terminalView = new VtNetCoreTerminalView();
_terminalView = new XtermTerminalView();
TerminalContainer.Children.Add((UIElement)_terminalView);
_terminalView.Initialize(ViewModel);
}
@@ -1,15 +0,0 @@
<UserControl
x:Class="FluentTerminal.App.Views.VtNetCoreTerminalView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
IsTabStop="True"
mc:Ignorable="d">
<Grid>
<canvas:CanvasControl x:Name="Canvas" ClearColor="Transparent" />
</Grid>
</UserControl>
@@ -1,660 +0,0 @@
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using VtNetCore.VirtualTerminal;
using VtNetCore.VirtualTerminal.Layout;
using VtNetCore.XTermParser;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
namespace FluentTerminal.App.Views
{
public sealed partial class VtNetCoreTerminalView : UserControl, ITerminalView
{
private readonly int _blinkHideMs = 300;
private readonly int _blinkShowMs = 600;
private readonly DataConsumer _consumer;
private readonly VirtualTerminalController _terminal;
private readonly DispatcherTimer blinkDispatcher;
private double _characterHeight = -1;
private double _characterWidth = -1;
private int _columns = -1;
private TextPosition _mouseOver = new TextPosition();
private int _rows = -1;
private bool _scrolling;
private bool _selecting;
private TextRange _textSelection;
public VtNetCoreTerminalView()
{
this.InitializeComponent();
_terminal = new VirtualTerminalController();
_consumer = new DataConsumer(_terminal);
ViewTop = _terminal.ViewPort.TopRow;
_terminal.SendData += OnSendData;
blinkDispatcher = new DispatcherTimer();
blinkDispatcher.Tick += BlinkTimerHandler;
blinkDispatcher.Interval = TimeSpan.FromMilliseconds(Math.Min(150, GCD(_blinkShowMs, _blinkHideMs)));
blinkDispatcher.Start();
Canvas.Draw += OnDraw;
KeyDown += TerminalKeyDown;
Tapped += OnTapped;
GotFocus += TerminalGotFocus;
LostFocus += TerminalLostFocus;
PointerWheelChanged += OnWheelChanged;
PointerMoved += TerminalPointerMoved;
PointerExited += TerminalPointerExited;
PointerPressed += TerminalPointerPressed;
PointerReleased += TerminalPointerReleased;
}
public int MaxScrollValue => _terminal.ViewPort.TopRow;
public TextPosition MousePressedAt { get; set; }
public int ScrollValue
{
get { return ViewTop; }
set
{
_scrolling = true;
ViewTop = value;
Canvas.Invalidate();
}
}
public TerminalViewModel ViewModel { get; private set; }
public int ViewTop { get; set; }
public Task ChangeKeyBindings()
{
return Task.CompletedTask;
}
public Task ChangeOptions(TerminalOptions options)
{
return Task.CompletedTask;
}
public Task ChangeTheme(TerminalTheme theme)
{
return Task.CompletedTask;
}
public Task FindNext(string searchText)
{
return Task.CompletedTask;
}
public Task FindPrevious(string searchText)
{
return Task.CompletedTask;
}
public Task FocusTerminal()
{
return Task.CompletedTask;
}
public Color GetSolidColorBrush(string hex)
{
byte a = 255; // (byte)(Convert.ToUInt32(hex.Substring(0, 2), 16));
byte r = (byte)(Convert.ToUInt32(hex.Substring(1, 2), 16));
byte g = (byte)(Convert.ToUInt32(hex.Substring(3, 2), 16));
byte b = (byte)(Convert.ToUInt32(hex.Substring(5, 2), 16));
return Color.FromArgb(a, r, g, b);
}
public async Task Initialize(TerminalViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
ViewModel.Terminal.Closed += Terminal_Closed;
ViewModel.Terminal.RegisterSelectedTextCallback(() => Task.FromResult(_terminal.GetText(_textSelection.Start.Column, _textSelection.Start.Row, _textSelection.End.Column, _textSelection.End.Row)));
_terminal.WindowTitleChanged += OnWindowTitleChanged;
_terminal.SizeChanged += OnSizeChanged;
var settings = ViewModel.SettingsService.GetApplicationSettings();
var options = ViewModel.SettingsService.GetTerminalOptions();
Canvas.FontFamily = new FontFamily(options.FontFamily);
Canvas.FontSize = options.FontSize;
Canvas.Margin = new Thickness(options.Padding, options.Padding, options.Padding, options.Padding);
var sessionType = SessionType.Unknown;
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
{
sessionType = SessionType.WinPty;
}
else
{
sessionType = SessionType.ConPty;
}
await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, new TerminalSize { Columns = 80, Rows = 30 }, sessionType).ConfigureAwait(true);
}
private void BlinkTimerHandler(object sender, object e)
{
Canvas.Invalidate();
}
private bool BlinkVisible()
{
var blinkCycle = _blinkShowMs + _blinkHideMs;
return (DateTime.Now.Subtract(DateTime.MinValue).Milliseconds % blinkCycle) < _blinkHideMs;
}
private bool ControlPressed()
{
return (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Control) & CoreVirtualKeyStates.Down) != 0;
}
private void CoreWindow_CharacterReceived(CoreWindow sender, CharacterReceivedEventArgs args)
{
var ch = char.ConvertFromUtf32((int)args.KeyCode);
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
// I lookup whether KeyPressed should handle the key here or there.
var code = _terminal.GetKeySequence(ch, false, false);
if (code == null)
{
args.Handled = _terminal.KeyPressed(ch, false, false);
}
}
private long GCD(long a, long b)
{
a = Math.Abs(a);
b = Math.Abs(b);
// Pull out remainders.
for (; ; )
{
long remainder = a % b;
if (remainder == 0) return b;
a = b;
b = remainder;
};
}
private void OnDraw(CanvasControl sender, CanvasDrawEventArgs args)
{
var drawingSession = args.DrawingSession;
var textFormat =
new CanvasTextFormat
{
FontSize = Convert.ToSingle(Canvas.FontSize),
FontFamily = Canvas.FontFamily.Source,
FontWeight = Canvas.FontWeight,
WordWrapping = CanvasWordWrapping.NoWrap
};
ProcessTextFormat(drawingSession, textFormat);
var showBlink = BlinkVisible();
var spans = default(List<LayoutRow>);
var cursorPosition = default(TextPosition);
var showCursor = false;
var cursorColor = Colors.Green;
var TerminalTop = -1;
lock (_terminal)
{
TerminalTop = _terminal.ViewPort.TopRow;
if (_terminal.ViewPort.CursorPosition.Row > _rows)
{
throw new Exception("We should never be here");
}
spans = _terminal.ViewPort.GetPageSpans(ViewTop, _rows, _columns, _textSelection);
showCursor = _terminal.CursorState.ShowCursor;
cursorPosition = _terminal.ViewPort.CursorPosition.Clone();
cursorColor = GetSolidColorBrush(_terminal.CursorState.Attributes.WebColor);
}
if (!_scrolling && ViewTop != TerminalTop)
{
ViewTop = TerminalTop;
}
var defaultTransform = drawingSession.Transform;
PaintBackgroundLayer(drawingSession, spans);
PaintTextLayer(drawingSession, spans, textFormat, showBlink);
if (showCursor)
{
PaintCursor(drawingSession, spans, textFormat, cursorPosition.OffsetBy(0, TerminalTop - ViewTop), cursorColor);
}
drawingSession.Transform = defaultTransform;
}
private void OnSendData(object sender, SendDataEventArgs e)
{
ViewModel.Terminal.Write(Encoding.UTF8.GetString(e.Data));
}
private async void OnSizeChanged(object sender, SizeEventArgs args)
{
await ViewModel.Terminal.SetSize(new TerminalSize { Rows = args.Height, Columns = args.Width }).ConfigureAwait(true);
}
private void OnWindowTitleChanged(object sender, TextEventArgs args)
{
ViewModel.Terminal.SetTitle(args.Text);
}
private void PaintBackgroundLayer(CanvasDrawingSession drawingSession, List<LayoutRow> spans)
{
double lineY = 0;
foreach (var textRow in spans)
{
drawingSession.Transform =
Matrix3x2.CreateScale(
(float)(textRow.DoubleWidth ? 2.0 : 1.0), // Scale double width
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0) // Scale double high
);
var drawY =
(lineY - (textRow.DoubleHeightBottom ? _characterHeight : 0)) * // Offset position upwards for bottom of double high char
((textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0); // Scale position for double height
double drawX = 0;
foreach (var textSpan in textRow.Spans)
{
var bounds =
new Rect(
drawX,
drawY,
_characterWidth * (textSpan.Text.Length) + 0.9,
_characterHeight + 0.9
);
if (textSpan.BackgroundColor != "#000000") // todo set theme
{
drawingSession.FillRectangle(bounds, GetSolidColorBrush(textSpan.BackgroundColor));
}
drawX += _characterWidth * (textSpan.Text.Length);
}
lineY += _characterHeight;
}
}
private void PaintCursor(CanvasDrawingSession drawingSession, List<LayoutRow> spans, CanvasTextFormat textFormat, TextPosition cursorPosition, Color cursorColor)
{
var cursorY = cursorPosition.Row;
if (cursorY < 0 || cursorY >= _rows)
return;
var drawX = cursorPosition.Column * _characterWidth;
var drawY = (cursorY * _characterHeight);
if (cursorY < spans.Count)
{
var textRow = spans[cursorY];
drawingSession.Transform =
Matrix3x2.CreateTranslation(
1.0f,
(float)(textRow.DoubleHeightBottom ? -_characterHeight : 0)
) *
Matrix3x2.CreateScale(
(float)(textRow.DoubleWidth ? 2.0 : 1.0),
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0)
);
drawY *= (textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0;
}
var cursorRect = new Rect(
drawX,
drawY,
_characterWidth,
_characterHeight + 0.9
);
drawingSession.DrawRectangle(cursorRect, cursorColor);
}
private void PaintTextLayer(CanvasDrawingSession drawingSession, List<LayoutRow> spans, CanvasTextFormat textFormat, bool showBlink)
{
var dipToDpiRatio = Windows.Graphics.Display.DisplayInformation.GetForCurrentView().LogicalDpi / 96;
double lineY = 0;
foreach (var textRow in spans)
{
drawingSession.Transform =
Matrix3x2.CreateScale(
(float)(textRow.DoubleWidth ? 2.0 : 1.0), // Scale double width
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0) // Scale double high
);
var drawY =
(lineY - (textRow.DoubleHeightBottom ? _characterHeight : 0)) * // Offset position upwards for bottom of double high char
((textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0); // Scale position for double height
double drawX = 0;
foreach (var textSpan in textRow.Spans)
{
var runWidth = _characterWidth * (textSpan.Text.Length);
if (textSpan.Hidden || (textSpan.Blink && !showBlink))
{
drawX += runWidth;
continue;
}
var color = GetSolidColorBrush(textSpan.ForgroundColor);
textFormat.FontWeight = textSpan.Bold ? FontWeights.Bold : FontWeights.Light;
var textLayout = new CanvasTextLayout(drawingSession, textSpan.Text, textFormat, 0.0f, 0.0f);
drawingSession.DrawTextLayout(textLayout, (float)drawX, (float)drawY, color);
if (textSpan.Underline)
{
// TODO : Come up with a better means of identifying line weight and offset
var underlineOffset = textLayout.LineMetrics[0].Baseline * dipToDpiRatio * 1.07;
drawingSession.DrawLine(new Vector2((float)drawX, (float)(drawY + underlineOffset)), new Vector2((float)(drawX + runWidth), (float)(drawY + underlineOffset)), color);
}
drawX += _characterWidth * (textSpan.Text.Length);
}
lineY += _characterHeight;
}
}
private void ProcessTextFormat(CanvasDrawingSession drawingSession, CanvasTextFormat format)
{
CanvasTextLayout textLayout = new CanvasTextLayout(drawingSession, "\u2560", format, 0.0f, 0.0f);
if (_characterWidth != textLayout.DrawBounds.Width || _characterHeight != textLayout.DrawBounds.Height)
{
_characterWidth = textLayout.DrawBounds.Width;
_characterHeight = textLayout.DrawBounds.Height;
}
int columns = Convert.ToInt32(Math.Floor(Canvas.RenderSize.Width / _characterWidth));
int rows = Convert.ToInt32(Math.Floor(Canvas.RenderSize.Height / _characterHeight));
if (_columns != columns || _rows != rows)
{
_columns = columns;
_rows = rows;
ViewModel.Terminal.SetSize(new TerminalSize { Columns = columns, Rows = rows });
}
}
private bool ShiftPressed()
{
return (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Shift) & CoreVirtualKeyStates.Down) != 0;
}
private void Terminal_Closed(object sender, EventArgs e)
{
}
private void Terminal_OutputReceived(object sender, string e)
{
var oldTopRow = _terminal.ViewPort.TopRow;
_consumer.Push(Encoding.UTF8.GetBytes(e));
if (_terminal.Changed)
{
_terminal.ClearChanges();
if (oldTopRow != _terminal.ViewPort.TopRow && oldTopRow >= ViewTop)
{
ViewTop = _terminal.ViewPort.TopRow;
}
Canvas.Invalidate();
}
}
private void TerminalGotFocus(object sender, RoutedEventArgs e)
{
Window.Current.CoreWindow.CharacterReceived += CoreWindow_CharacterReceived;
_terminal?.FocusIn();
}
private void TerminalKeyDown(object sender, KeyRoutedEventArgs e)
{
_scrolling = false;
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
// I lookup whether KeyPressed should handle the key here or there.
var code = _terminal.GetKeySequence(e.Key.ToString(), ControlPressed(), ShiftPressed());
if (code != null)
e.Handled = _terminal.KeyPressed(e.Key.ToString(), ControlPressed(), ShiftPressed());
if (ViewTop != _terminal.ViewPort.TopRow)
{
ViewTop = _terminal.ViewPort.TopRow;
Canvas.Invalidate();
}
}
private void TerminalLostFocus(object sender, RoutedEventArgs e)
{
Window.Current.CoreWindow.CharacterReceived -= CoreWindow_CharacterReceived;
_terminal?.FocusOut();
}
private void TerminalPointerExited(object sender, PointerRoutedEventArgs e)
{
_mouseOver = null;
Canvas.Invalidate();
}
private void TerminalPointerMoved(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
var position = PointToTextPosition(pointer.Position);
var textPosition = position.OffsetBy(0, ViewTop);
if ((_terminal.UseAllMouseTracking || _terminal.CellMotionMouseTracking) && position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
{
var button =
pointer.Properties.IsLeftButtonPressed ? 0 :
pointer.Properties.IsRightButtonPressed ? 1 :
pointer.Properties.IsMiddleButtonPressed ? 2 :
3; // No button
_terminal.MouseMove(position.Column, position.Row, button, ControlPressed(), ShiftPressed());
if (button == 3 && !_terminal.UseAllMouseTracking)
{
return;
}
}
if (_mouseOver != null && _mouseOver == position)
{
return;
}
_mouseOver = position;
if (pointer.Properties.IsLeftButtonPressed && MousePressedAt != null)
{
TextRange newSelection;
if (MousePressedAt != textPosition)
{
if (MousePressedAt <= textPosition)
{
newSelection = new TextRange
{
Start = MousePressedAt,
End = textPosition.OffsetBy(-1, 0)
};
}
else
{
newSelection = new TextRange
{
Start = textPosition,
End = MousePressedAt
};
}
_selecting = true;
if (_textSelection != newSelection)
{
_textSelection = newSelection;
Canvas.Invalidate();
}
}
}
}
private void TerminalPointerPressed(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
var position = PointToTextPosition(pointer.Position);
var textPosition = position.OffsetBy(0, ViewTop);
if (!_terminal.MouseTrackingEnabled)
{
if (pointer.Properties.IsLeftButtonPressed)
MousePressedAt = textPosition;
//else if (pointer.Properties.IsRightButtonPressed)
//PasteClipboard();
}
if (position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
{
var button =
pointer.Properties.IsLeftButtonPressed ? 0 :
pointer.Properties.IsRightButtonPressed ? 1 :
pointer.Properties.IsMiddleButtonPressed ? 2 :
3; // No button
_terminal.MousePress(position.Column, position.Row, button, ControlPressed(), ShiftPressed());
}
}
private void TerminalPointerReleased(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
var position = PointToTextPosition(pointer.Position);
var textPosition = position.OffsetBy(0, ViewTop);
if (!pointer.Properties.IsLeftButtonPressed)
{
if (_selecting)
{
MousePressedAt = null;
_selecting = false;
var captured = _terminal.GetText(_textSelection.Start.Column, _textSelection.Start.Row, _textSelection.End.Column, _textSelection.End.Row);
var dataPackage = new DataPackage();
dataPackage.SetText(captured);
dataPackage.Properties.EnterpriseId = "FluentTerminal";
try
{
Clipboard.SetContent(dataPackage);
}
catch (Exception exception)
{
Debug.WriteLine("Failed to post to clipboard: " + exception.Message);
}
}
else
{
_textSelection = null;
Canvas.Invalidate();
}
}
if (position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
{
_terminal.MouseRelease(position.Column, position.Row, ControlPressed(), ShiftPressed());
}
}
private void OnTapped(object sender, TappedRoutedEventArgs e)
{
Focus(FocusState.Pointer);
}
private void OnWheelChanged(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
int oldViewTop = ViewTop;
ViewTop -= pointer.Properties.MouseWheelDelta / 40;
_scrolling = true;
if (ViewTop < 0)
{
ViewTop = 0;
}
else if (ViewTop > _terminal.ViewPort.TopRow)
{
ViewTop = _terminal.ViewPort.TopRow;
}
if (oldViewTop != ViewTop)
{
Canvas.Invalidate();
}
}
private TextPosition PointToTextPosition(Point point)
{
var overColumn = (int)Math.Floor(point.X / _characterWidth);
if (overColumn >= _columns)
{
overColumn = _columns - 1;
}
var overRow = (int)Math.Floor(point.Y / _characterHeight);
if (overRow >= _rows)
{
overRow = _rows - 1;
}
return new TextPosition { Column = overColumn, Row = overRow };
}
}
}
@@ -1,6 +1,7 @@
using Fleck;
using FluentTerminal.App.Services;
using FluentTerminal.App.Utilities;
using FluentTerminal.App.ViewModels;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.RuntimeComponent.Enums;
@@ -14,6 +15,7 @@ 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;
@@ -28,37 +30,49 @@ namespace FluentTerminal.App.Views
public sealed partial class XtermTerminalView : UserControl, IxtermEventListener, ITerminalView
{
private readonly ManualResetEventSlim _connectedEvent;
private readonly SemaphoreSlim _loaded;
private bool _alreadyLoaded;
private InputBuffer _buffer;
private MenuFlyoutItem _copyMenuItem;
private readonly SemaphoreSlim _navigationCompleted;
private readonly MenuFlyoutItem _copyMenuItem;
private BlockingCollection<Action> _dispatcherJobs;
private MenuFlyoutItem _pasteMenuItem;
private IWebSocketConnection _webSocket;
private readonly MenuFlyoutItem _pasteMenuItem;
private WebView _webView;
private readonly DebouncedAction<TerminalOptions> _optionsChanged;
private IWebSocketConnection _socket;
public XtermTerminalView()
{
_loaded = new SemaphoreSlim(0, 1);
_connectedEvent = new ManualResetEventSlim(false);
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(true));
return settings;
};
InitializeComponent();
StartMediatorTask();
Loaded += XtermTerminalView_Loaded;
GotFocus += XtermTerminalView_GotFocus;
_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; }
@@ -73,8 +87,8 @@ namespace FluentTerminal.App.Views
public Task ChangeOptions(TerminalOptions options)
{
var serialized = JsonConvert.SerializeObject(options);
return ExecuteScriptAsync($"changeOptions('{serialized}')");
_optionsChanged.Invoke(options);
return Task.CompletedTask;
}
public Task ChangeTheme(TerminalTheme theme)
@@ -123,7 +137,7 @@ namespace FluentTerminal.App.Views
{
sessionType = SessionType.ConPty;
}
await _loaded.WaitAsync().ConfigureAwait(true);
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);
@@ -134,6 +148,8 @@ namespace FluentTerminal.App.Views
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()));
@@ -200,7 +216,8 @@ namespace FluentTerminal.App.Views
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
{
_loaded.Release();
Logger.Instance.Debug("WebView navigation completed. Target: {uri}", args.Uri);
_navigationCompleted.Release();
}
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
@@ -216,16 +233,42 @@ namespace FluentTerminal.App.Views
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 =>
{
_webSocket = socket;
_buffer = new InputBuffer(_webSocket.Send);
socket.OnOpen = () => _connectedEvent.Set();
socket.OnMessage = message => ViewModel.Terminal.Write(message);
_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);
}
@@ -272,7 +315,7 @@ namespace FluentTerminal.App.Views
{
_dispatcherJobs = new BlockingCollection<Action>();
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
Task.Run(async () =>
Task.Factory.StartNew(async () =>
{
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
{
@@ -285,66 +328,28 @@ namespace FluentTerminal.App.Views
Debug.WriteLine(e);
}
}
});
}, TaskCreationOptions.LongRunning);
}
private async void Terminal_Closed(object sender, EventArgs e)
{
ViewModel.Terminal.OutputReceived -= Terminal_OutputReceived;
ViewModel.Terminal.Closed -= Terminal_Closed;
_webView?.Navigate(new Uri("about:blank"));
_webView = null;
await ViewModel.Terminal.Close().ConfigureAwait(true);
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, string e)
private void Terminal_OutputReceived(object sender, byte[] e)
{
_webSocket.Send(e);
_socket.Send(Encoding.UTF8.GetString(e));
}
private async void XtermTerminalView_GotFocus(object sender, RoutedEventArgs e)
void IxtermEventListener.OnError(string error)
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
}
}
private void XtermTerminalView_Loaded(object sender, RoutedEventArgs e)
{
if (_alreadyLoaded)
{
return;
}
if (Root.Children.Any())
{
return;
}
_webView = new WebView(WebViewExecutionMode.SeparateThread)
{
DefaultBackgroundColor = Colors.Transparent
};
Root.Children.Add(_webView);
_webView.NavigationCompleted += _webView_NavigationCompleted;
_webView.NavigationStarting += _webView_NavigationStarting;
_copyMenuItem = new MenuFlyoutItem { Text = "Copy" };
_copyMenuItem.Click += Copy_Click;
_pasteMenuItem = new MenuFlyoutItem { Text = "Paste" };
_pasteMenuItem.Click += Paste_Click;
_webView.ContextFlyout = new MenuFlyout
{
Items = { _copyMenuItem, _pasteMenuItem }
};
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
_alreadyLoaded = true;
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"
}
}
+6 -3
View File
@@ -126,10 +126,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; }
}
}
+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; }
}
}
@@ -3,6 +3,6 @@
public class DisplayTerminalOutputRequest
{
public int TerminalId { get; set; }
public string Output { get; set; }
public byte[] Output { get; set; }
}
}
@@ -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; }
}
}
@@ -114,7 +114,7 @@
</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' ">
@@ -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);
}
}
@@ -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");
}
}
}
@@ -8,6 +8,7 @@ using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using System;
using FluentTerminal.Models.Responses;
using FluentTerminal.App.Services;
namespace FluentTerminal.SystemTray.Services
{
@@ -45,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)
@@ -84,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();
@@ -102,10 +108,10 @@ 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))
{
@@ -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,6 +10,7 @@ namespace FluentTerminal.SystemTray.Services.ConPty
{
private TerminalsManager _terminalsManager;
private Terminal _terminal;
private bool _exited;
public int Id { get; private set; }
@@ -34,16 +35,26 @@ namespace FluentTerminal.SystemTray.Services.ConPty
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();
}
@@ -63,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)
@@ -108,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
{
@@ -63,11 +76,11 @@ namespace FluentTerminal.SystemTray.Services
};
}
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);
}
}
@@ -88,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.Add("TERM", "xterm-256color");
environmentVariables.Add("TERM_PROGRAM", "FluentTerminal");
environmentVariables.Add("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)
@@ -102,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,6 +21,8 @@ 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)
{
@@ -37,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);
@@ -51,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
{
@@ -66,11 +90,19 @@ namespace FluentTerminal.SystemTray.Services.WinPty
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))
@@ -88,6 +120,7 @@ namespace FluentTerminal.SystemTray.Services.WinPty
public void Dispose()
{
_shellProcess.Exited -= _shellProcess_Exited;
Dispose(true);
GC.SuppressFinalize(this);
}
@@ -97,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)
@@ -111,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
@@ -160,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);
}
}
}
+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`