Compare commits

..
Author SHA1 Message Date
FelixSe 7bbed15874 Merge branch 'master' into Dev/NativeRenderer 2019-04-07 21:09:22 +02:00
FelixSe 997cb9814e fixed wrong thread event, made canvas resize 2019-04-03 23:03:24 +02:00
Andrew Kralovec 6524d14687 Native Renderer Collabate PR (#284)
* Load terminal options and add the inital scroll viewer

* Current Virtual Cavnas, problem with event handler calling the canvas on draw
2019-04-03 07:26:07 +02:00
FelixSe 7876984327 proof of concepts for vtnetcore, tterm inspired 2019-03-31 21:58:02 +02:00
409 changed files with 7779 additions and 19483 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
# Git will handle the files in whatever way it thinks is best.
# Overwrites global core.autocrlf git setting
* text=auto
-2
View File
@@ -292,5 +292,3 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
!FluentTerminal.App/MoshExecutables/*
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Services.Test
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
{
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowCreateKeyBindingDialog();
@@ -36,7 +36,7 @@ namespace FluentTerminal.App.Services.Test
var title = string.Empty;
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
@@ -49,7 +49,7 @@ namespace FluentTerminal.App.Services.Test
var title = _fixture.Create<string>();
var content = string.Empty;
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
@@ -61,7 +61,7 @@ namespace FluentTerminal.App.Services.Test
{
var title = _fixture.Create<string>();
var content = _fixture.Create<string>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
@@ -75,7 +75,7 @@ namespace FluentTerminal.App.Services.Test
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var messageDialog = new Mock<IMessageDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
@@ -86,7 +86,7 @@ namespace FluentTerminal.App.Services.Test
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
{
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowProfileSelectionDialogAsync();
@@ -98,7 +98,7 @@ namespace FluentTerminal.App.Services.Test
{
var title = "title";
var inputDialog = new Mock<IInputDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>);
dialogService.ShowInputDialogAsync(title);
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
@@ -11,11 +11,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Moq" Version="4.13.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<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>
@@ -34,11 +34,4 @@
<ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.FoundationContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
@@ -41,41 +41,6 @@ namespace FluentTerminal.App.Services.Test
invoke.Should().Throw<InvalidOperationException>();
}
[Fact]
public void DeregisterCommandHandler_CommandIsNull_ThrowsArgumentNullException()
{
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(null);
invoke.Should().Throw<ArgumentNullException>();
}
[Fact]
public void DeregisterCommandHandler_CommandRegistered_HandlerGetsRemoved()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var handler = _fixture.Create<Action>();
var keyboardCommandService = new KeyboardCommandService();
keyboardCommandService.RegisterCommandHandler(command, handler);
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact]
public void DeregisterCommandHandler_CommandNotRegistered_ShouldNotThrow()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact]
public void SendCommand_CommandIsRegistered_HandlerGetsInvoked()
@@ -10,8 +10,6 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
using Xunit;
namespace FluentTerminal.App.Services.Test
@@ -161,8 +159,7 @@ namespace FluentTerminal.App.Services.Test
};
var currentThemeChangedEventInvoked = false;
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
settingsService.SaveCurrentThemeId(currentThemeId);
@@ -209,8 +206,7 @@ namespace FluentTerminal.App.Services.Test
ShellProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
settingsService.SaveTheme(theme);
@@ -367,8 +363,7 @@ namespace FluentTerminal.App.Services.Test
ShellProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<TerminalOptionsChangedMessage>(this,
message => terminalOptionsChangedEventInvoked = true);
settingsService.TerminalOptionsChanged += (s, e) => terminalOptionsChangedEventInvoked = true;
settingsService.SaveTerminalOptions(terminalOptions);
@@ -435,8 +430,7 @@ namespace FluentTerminal.App.Services.Test
ShellProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this,
a => applicationSettingsChangedEventInvoked = true);
settingsService.ApplicationSettingsChanged += (s, e) => applicationSettingsChangedEventInvoked = true;
settingsService.SaveApplicationSettings(applicationSettings);
@@ -477,6 +471,10 @@ namespace FluentTerminal.App.Services.Test
var commands = _fixture.CreateMany<Command>(5);
var defaultValueProvider = new Mock<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
foreach (var command in commands)
{
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(EnumHelper.GetEnumDescription(command), null)).Returns(new Collection<KeyBinding> { });
}
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
@@ -534,8 +532,7 @@ namespace FluentTerminal.App.Services.Test
ShellProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
@@ -581,8 +578,7 @@ namespace FluentTerminal.App.Services.Test
ShellProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
settingsService.ResetKeyBindings();
@@ -657,7 +653,7 @@ namespace FluentTerminal.App.Services.Test
var result = settingsService.GetShellProfiles();
result.Select(p => p.EqualTo(shellProfiles.FirstOrDefault(x => x.Id == p.Id)).Should().BeTrue());
result.Should().BeEquivalentTo(shellProfiles);
shellProfilesContainer.Verify(x => x.GetAll(), Times.Once);
}
@@ -10,7 +10,6 @@ using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
using Xunit;
namespace FluentTerminal.App.Services.Test
@@ -35,16 +34,16 @@ namespace FluentTerminal.App.Services.Test
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var response = _fixture.Create<CreateTerminalResponse>();
var responseMessage = new ValueSet
var responseMessage = new Dictionary<string, string>
{
[MessageKeys.Type] = _fixture.Create<string>(),
[MessageKeys.Content] = JsonConvert.SerializeObject(response)
};
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<ValueSet>())).Returns(Task.FromResult(responseMessage));
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>();
var id = _fixture.Create<byte>();
var id = _fixture.Create<int>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
@@ -56,7 +55,7 @@ namespace FluentTerminal.App.Services.Test
[Fact]
public async Task ResizeTerminal_Default_SendsResizeTerminalRequest()
{
var terminalId = _fixture.Create<byte>();
var terminalId = _fixture.Create<int>();
var terminalSize = _fixture.Create<TerminalSize>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -70,13 +69,13 @@ namespace FluentTerminal.App.Services.Test
await trayProcessCommunicationService.ResizeTerminal(terminalId, terminalSize);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(ResizeTerminalRequest))), Times.Once);
}
[Fact]
public async Task Write_Default_SendsWriteDataRequest()
{
var terminalId = _fixture.Create<byte>();
var terminalId = _fixture.Create<int>();
var data = _fixture.Create<byte[]>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -90,13 +89,13 @@ namespace FluentTerminal.App.Services.Test
await trayProcessCommunicationService.Write(terminalId, data);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteDataRequest))), Times.Once);
}
[Fact]
public async Task CloseTerminal_Default_SendsTerminalExitedRequest()
{
var terminalId = _fixture.Create<byte>();
var terminalId = _fixture.Create<int>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
@@ -109,19 +108,22 @@ namespace FluentTerminal.App.Services.Test
await trayProcessCommunicationService.CloseTerminal(terminalId);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(TerminalExitedRequest))), Times.Once);
}
[Fact]
public void OnMessageReceived_TerminalExitedRequest_InvokesTerminalExitedEvent()
{
var request = new TerminalExitedRequest(_fixture.Create<byte>(), _fixture.Create<int>());
var receivedExitStatus = (TerminalExitStatus) null;
var message = new ValueSet
var terminalId = _fixture.Create<int>();
var receivedTerminalId = 0;
var terminalExitedEventCalled = false;
var request = new TerminalExitedRequest
{
[MessageKeys.Type] = (byte) MessageIdentifiers.TerminalExitedRequest,
TerminalId = terminalId
};
var message = new Dictionary<string, string>
{
[MessageKeys.Type] = nameof(TerminalExitedRequest),
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
};
var settingsService = new Mock<ISettingsService>();
@@ -133,29 +135,33 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
var appServiceConnection = new Mock<IAppServiceConnection>();
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
trayProcessCommunicationService.TerminalExited += (s, status) =>
trayProcessCommunicationService.TerminalExited += (s, e) =>
{
receivedExitStatus = status;
terminalExitedEventCalled = true;
receivedTerminalId = e;
};
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
receivedExitStatus.Should().NotBeNull();
receivedExitStatus.TerminalId.Should().Be(request.TerminalId);
receivedExitStatus.ExitCode.Should().Be(request.ExitCode);
terminalExitedEventCalled.Should().BeTrue();
receivedTerminalId.Should().Be(terminalId);
}
[Fact]
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
{
var terminalId = _fixture.Create<byte>();
var terminalId = _fixture.Create<int>();
var output = _fixture.Create<byte[]>();
var receivedOutput = default(byte[]);
var message = new ValueSet
var request = new DisplayTerminalOutputRequest
{
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
[MessageKeys.TerminalId] = terminalId,
[MessageKeys.Content] = output
TerminalId = terminalId,
Output = output
};
var message = new Dictionary<string, string>
{
[MessageKeys.Type] = nameof(DisplayTerminalOutputRequest),
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
};
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -20,11 +20,6 @@ namespace FluentTerminal.App.Services.Adapters
_applicationDataContainer.Values.Remove(key);
}
public void Clear()
{
_applicationDataContainer.Values.Clear();
}
public IEnumerable<object> GetAll()
{
return _applicationDataContainer.Values.Select(x => x.Value);
@@ -7,7 +7,5 @@
public IApplicationDataContainer Themes { get; set; }
public IApplicationDataContainer KeyBindings { get; set; }
public IApplicationDataContainer ShellProfiles { get; set; }
public IApplicationDataContainer SshProfiles { get; set; }
public IApplicationDataContainer HistoryContainer { get; set; }
}
}
-6
View File
@@ -5,11 +5,5 @@
public const string ThemesContainerName = "Themes";
public const string KeyBindingsContainerName = "KeyBindings";
public const string ShellProfilesContainerName = "ShellProfiles";
public const string SshProfilesContainerName = "SshProfiles";
public const byte TerminalBufferRequestIdentifier = 0;
public const string SshCommandName = "ssh";
public const string MoshCommandName = "mosh";
public const string ExecutedCommandsContainerName = "HistoryContainer";
public const string TerminalViewModelStateId = "TerminalViewModelState";
}
}
@@ -1,9 +0,0 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IAboutDialog
{
Task ShowAsync();
}
}
@@ -1,10 +0,0 @@
using System.Threading.Tasks;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ICustomCommandDialog
{
Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null);
}
}
@@ -0,0 +1,11 @@
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshConnectionInfo
{
string Host { get; set; }
ushort Port { get; set; }
string Username { get; set; }
}
}
@@ -1,10 +1,9 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshConnectionInfoDialog
{
Task<SshProfile> GetSshConnectionInfoAsync(SshProfile input = null);
Task<ISshConnectionInfo> GetSshConnectionInfoAsync();
}
}
@@ -1,12 +0,0 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshProfileSelectionDialog
{
SshProfile SelectedProfile { get; }
Task<SshProfile> SelectProfile();
}
}
@@ -1,11 +0,0 @@
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.Services.EventArgs
{
public class NewTabRequestedEventArgs : System.EventArgs
{
public DragEventArgs DragEventArgs { get; set; }
public int Position { get; set; }
}
}
@@ -1,10 +1,7 @@
using System;
namespace FluentTerminal.App.Services.EventArgs
namespace FluentTerminal.App.Services.EventArgs
{
public class NewWindowRequestedEventArgs : System.EventArgs
{
public Models.Enums.NewWindowAction Action { get; set; }
public Guid ProfileId { get; set; }
public bool ShowProfileSelection { get; set; }
}
}
@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluentTerminal.App.Services.Exceptions
{
public class SaveTextFileException : Exception
{
public SaveTextFileException() : base()
{
}
public SaveTextFileException(string message) : base(message)
{
}
public SaveTextFileException(string message, Exception innerException) : base(message, innerException)
{
}
protected SaveTextFileException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
@@ -7,11 +7,10 @@
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="PListNet" Version="3.0.0" />
<PackageReference Include="RestSharp" Version="106.6.10" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="PListNet" Version="2.0.3" />
<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>
@@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
namespace FluentTerminal.App.Services
{
public interface IAppServiceConnection
{
event EventHandler<ValueSet> MessageReceived;
event EventHandler<IDictionary<string, string>> MessageReceived;
Task<ValueSet> SendMessageAsync(ValueSet message);
Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message);
}
}
@@ -14,8 +14,6 @@ namespace FluentTerminal.App.Services
void Delete(string key);
void Clear();
IEnumerable<object> GetAll();
}
}
@@ -1,13 +0,0 @@
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface IApplicationLanguageService
{
IEnumerable<string> Languages { get; }
void SetLanguage(string language);
string GetCurrentLanguage();
}
}
@@ -14,7 +14,7 @@ namespace FluentTerminal.App.Services
int Id { get; }
string Title { get; set; }
Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true);
Task RunOnDispatcherThread(Action action);
Task<bool> TryClose();
@@ -1,5 +1,6 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
using FluentTerminal.App.Services.Dialogs;
namespace FluentTerminal.App.Services
{
@@ -18,12 +19,6 @@ namespace FluentTerminal.App.Services
Task<KeyBinding> ShowCreateKeyBindingDialog();
Task<string> ShowInputDialogAsync(string title);
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null);
Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null);
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
Task ShowAboutDialogAsync();
Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync();
}
}
@@ -8,6 +8,5 @@ namespace FluentTerminal.App.Services
void SendCommand(string command);
void DeregisterCommandHandler(string command);
void ClearCommandHandlers();
}
}
@@ -6,37 +6,36 @@ namespace FluentTerminal.App.Services
{
public interface ISettingsService
{
event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
event EventHandler<Guid> CurrentThemeChanged;
event EventHandler KeyBindingsChanged;
event EventHandler<ShellProfile> ShellProfileAdded;
event EventHandler<Guid> ShellProfileDeleted;
event EventHandler<TerminalOptions> TerminalOptionsChanged;
event EventHandler<Guid> ThemeDeleted;
event EventHandler<TerminalTheme> ThemeAdded;
void DeleteShellProfile(Guid id);
void DeleteSshProfile(Guid id);
void DeleteTheme(Guid id);
ApplicationSettings GetApplicationSettings();
TerminalTheme GetCurrentTheme();
Guid GetCurrentThemeId();
ShellProfile GetDefaultShellProfile();
SshProfile GetDefaultSshProfile();
Guid GetDefaultShellProfileId();
Guid GetDefaultSshProfileId();
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
IEnumerable<ShellProfile> GetShellProfiles();
IEnumerable<SshProfile> GetSshProfiles();
IEnumerable<TabTheme> GetTabThemes();
TerminalOptions GetTerminalOptions();
TerminalTheme GetTheme(Guid id);
IEnumerable<TerminalTheme> GetThemes();
void ResetKeyBindings();
void SaveApplicationSettings(ApplicationSettings applicationSettings);
void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id);
void SaveDefaultSshProfileId(Guid id);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
void SaveTerminalOptions(TerminalOptions terminalOptions);
void SaveTheme(TerminalTheme theme, bool newTheme = false);
ShellProfile GetShellProfile(Guid id);
SshProfile GetSshProfile(Guid id);
string ExportSettings();
void ImportSettings(string serializedSettings);
}
}
@@ -1,9 +0,0 @@
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface IShellProfileMigrationService
{
void Migrate(ShellProfile profile);
}
}
@@ -8,41 +8,22 @@ namespace FluentTerminal.App.Services
{
public interface ITrayProcessCommunicationService
{
event EventHandler<TerminalExitStatus> TerminalExited;
event EventHandler<int> TerminalExited;
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause);
Task ResizeTerminal(byte id, TerminalSize size);
Task ResizeTerminal(int id, TerminalSize size);
Task UpdateToggleWindowKeyBindings();
Task Write(byte terminalId, byte[] data);
Task Write(int terminalId, byte[] data);
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback);
void UnsubscribeFromTerminalOutput(byte terminalId);
Task CloseTerminal(byte terminalId);
byte GetNextTerminalId();
Task<string> GetUserName();
Task SaveTextFileAsync(string path, string content);
Task<string> GetSshConfigDirAsync();
Task<string[]> GetFilesFromSshConfigDirAsync();
Task<bool> CheckFileExistsAsync(string path);
void MuteTerminal(bool mute);
void UpdateSettings(ApplicationSettings settings);
Task<string> GetCommandPathAsync(string command);
Task CloseTerminal(int terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
int GetNextTerminalId();
}
}
@@ -1,10 +1,7 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using Windows.Foundation.Metadata;
using Windows.Storage;
namespace FluentTerminal.App.Services.Implementation
{
@@ -12,14 +9,6 @@ namespace FluentTerminal.App.Services.Implementation
{
public ApplicationSettings GetDefaultApplicationSettings()
{
var logDirectoryPath = string.Empty;
try
{
// Accessing ApplicationData.Current during run of unit tests causes
// "System.InvalidOperationException : The process has no package identity" exception.
logDirectoryPath = ApplicationData.Current.LocalCacheFolder.Path;
} catch (InvalidOperationException) { }
return new ApplicationSettings
{
ConfirmClosingTabs = false,
@@ -31,16 +20,11 @@ namespace FluentTerminal.App.Services.Implementation
CopyOnSelect = false,
MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu,
AlwaysShowTabs = false,
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false,
EnableTrayIcon = true,
ShowCustomTitleInTitlebar = true,
UseMoshByDefault = true,
AutoFallbackToWindowsUsernameInLinks = true,
RTrimCopiedLines = true,
MuteTerminalBeeps = true,
EnableLogging = false,
PrintableOutputOnly = true,
LogDirectoryPath = logDirectoryPath
ShowCustomTitleInTitlebar = true
};
}
@@ -54,6 +38,9 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding
{
Command = nameof(Command.ToggleWindow),
Ctrl = false,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Scroll
}
};
@@ -65,6 +52,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NextTab),
Ctrl = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Tab
}
};
@@ -76,6 +65,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.PreviousTab),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.Tab
}
@@ -95,20 +85,10 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding
{
Command = command.ToString(),
Alt = true,
Key = (int)ExtendedVirtualKey.Number1 + (command - Command.SwitchToTerm1)
}
};
case Command.DuplicateTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.DuplicateTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.D
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Number1 + (command - Command.SwitchToTerm1)
}
};
@@ -119,7 +99,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NewTab),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.T
}
};
@@ -131,84 +112,25 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ConfigurableNewTab),
Ctrl = true,
Alt = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewSshTab:
case Command.NewRemoteTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewSshTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Y
}
};
case Command.NewSshWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewSshWindow),
Ctrl = true,
Command = nameof(Command.NewRemoteTab),
Ctrl = false,
Alt = true,
Key = (int)ExtendedVirtualKey.Y
Shift = false,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewCustomCommandTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewCustomCommandTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Q
}
};
case Command.NewCustomCommandWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewCustomCommandWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.Q
}
};
case Command.SavedSshNewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.SavedSshNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
@@ -216,6 +138,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ChangeTabTitle),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.R
}
@@ -228,7 +151,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.CloseTab),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.W
}
};
@@ -240,7 +164,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NewWindow),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.N
}
};
@@ -252,7 +177,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ConfigurableNewWindow),
Ctrl = true,
Alt = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.N
}
};
@@ -264,7 +190,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ShowSettings),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Comma
}
};
@@ -276,7 +203,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Copy),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.C
}
};
@@ -288,7 +216,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Paste),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.V
}
};
@@ -301,7 +230,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.PasteWithoutNewlines),
Ctrl = true,
Alt = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.V
}
};
@@ -313,7 +243,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Search),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.F
}
};
@@ -324,13 +255,10 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding
{
Command = nameof(Command.ToggleFullScreen),
Ctrl = false,
Alt = true,
Shift = false,
Key = (int)ExtendedVirtualKey.Enter
},
new KeyBinding
{
Command = nameof(Command.ToggleFullScreen),
Key = (int)ExtendedVirtualKey.F11
}
};
@@ -341,7 +269,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.SelectAll),
Ctrl = true,
Shift = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.A
}
};
@@ -353,14 +282,14 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Clear),
Ctrl = true,
Alt = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.L
}
};
default:
throw new InvalidOperationException($"Default keybindings for Command '{command}' are missing");
}
return null;
}
public Guid GetDefaultShellProfileId()
@@ -375,7 +304,7 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme
{
Id = 0,
Name = I18N.Translate("TabTheme.None"),
Name = "None",
BackgroundOpacity = double.NaN,
BackgroundPointerOverOpacity = double.NaN,
BackgroundPressedOpacity = double.NaN,
@@ -386,42 +315,41 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme
{
Id = 1,
Name = I18N.Translate("TabTheme.Red"),
Name = "Red",
Color = "#E81123"
},
new TabTheme
{
Id = 2,
Name = I18N.Translate("TabTheme.Green"),
Name = "Green",
Color = "#10893E"
},
new TabTheme
{
Id = 3,
Name = I18N.Translate("TabTheme.Blue"),
Name = "Blue",
Color = "#0078D7"
},
new TabTheme
{
Id = 4,
Name = I18N.Translate("TabTheme.Purple"),
Name = "Purple",
Color = "#881798"
},
new TabTheme
{
Id = 5,
Name = I18N.Translate("TabTheme.Orange"),
Name = "Orange",
Color = "#FF8C00"
},
new TabTheme
{
Id = 6,
Name = I18N.Translate("TabTheme.Teal"),
Name = "Teal",
Color = "#00B7C3"
}
};
}
public TerminalOptions GetDefaultTerminalOptions()
{
return new TerminalOptions
@@ -435,8 +363,7 @@ namespace FluentTerminal.App.Services.Implementation
BoldText = false,
BackgroundOpacity = 0.8,
Padding = 12,
ScrollBackLimit = 1000,
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;"
ScrollBackLimit = 1000
};
}
@@ -453,17 +380,11 @@ namespace FluentTerminal.App.Services.Implementation
{
Id = GetDefaultShellProfileId(),
Name = "Powershell",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
@@ -481,17 +402,11 @@ namespace FluentTerminal.App.Services.Implementation
{
Id = Guid.Parse("ab942a61-7673-4755-9bd8-765aff91d9a3"),
Name = "CMD",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\Windows\System32\cmd.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
@@ -509,17 +424,11 @@ namespace FluentTerminal.App.Services.Implementation
{
Id= Guid.Parse("e5785ad6-584f-40cb-bdcd-d5b3b3953e7f"),
Name = "WSL",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.ToLF,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
LineEndingTranslation = LineEndingStyle.DoNotModify,
KeyBindings = new []
{
new KeyBinding
@@ -12,25 +12,16 @@ namespace FluentTerminal.App.Services.Implementation
private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory;
private readonly Func<IInputDialog> _inputDialogFactory;
private readonly Func<ISshConnectionInfoDialog> _sshConnectionInfoDialogFactory;
private readonly Func<ICustomCommandDialog> _customCommandDialogFactory;
private readonly Func<ISshProfileSelectionDialog> _sshProfileSelectionDialogFactory;
private readonly Func<IAboutDialog> _aboutDialogFactory;
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
Func<ICustomCommandDialog> customCommandDialogFactory,
Func<ISshProfileSelectionDialog> sshProfileSelectionDialogFactory,
Func<IAboutDialog> aboutDialogFactory)
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory)
{
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
_messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory;
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
_customCommandDialogFactory = customCommandDialogFactory;
_sshProfileSelectionDialogFactory = sshProfileSelectionDialogFactory;
_aboutDialogFactory = aboutDialogFactory;
}
public Task<KeyBinding> ShowCreateKeyBindingDialog()
@@ -84,24 +75,7 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.SelectProfile();
}
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) =>
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
_customCommandDialogFactory().GetCustomCommandAsync(input);
public Task<SshProfile> ShowSshProfileSelectionDialogAsync()
{
var dialog = _sshProfileSelectionDialogFactory();
return dialog.SelectProfile();
}
public Task ShowAboutDialogAsync()
{
var dialog = _aboutDialogFactory();
return dialog.ShowAsync();
}
public Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync() =>
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync();
}
}
@@ -51,7 +51,7 @@ namespace FluentTerminal.App.Services.Implementation
public const string RedComponent = "Red Component";
}
public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
{
if (string.IsNullOrWhiteSpace(fileName))
{
@@ -65,13 +65,13 @@ namespace FluentTerminal.App.Services.Implementation
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
return Task.FromResult(new TerminalTheme
return new TerminalTheme
{
Name = Path.GetFileNameWithoutExtension(fileName),
Colors = GetColors(node),
Id = Guid.NewGuid(),
PreInstalled = false
});
};
}
private TerminalColors GetColors(DictionaryNode themeDictionary)
@@ -83,8 +83,6 @@ namespace FluentTerminal.App.Services.Implementation
Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]),
CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]),
Selection = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor], Opacity30Percent),
SelectionForeground = GetColorString(themeDictionary[ITermThemeKeys.SelectedTextColor]),
SelectionBackground = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor]),
Black = GetColorString(themeDictionary[ITermThemeKeys.Ansi0Color]),
Red = GetColorString(themeDictionary[ITermThemeKeys.Ansi1Color]),
Green = GetColorString(themeDictionary[ITermThemeKeys.Ansi2Color]),
@@ -112,6 +110,8 @@ namespace FluentTerminal.App.Services.Implementation
var green = dictionaryNode[ITermThemeColorKeys.GreenComponent] as RealNode ?? throw new ParseThemeException("Green node value was not a real number");
var blue = dictionaryNode[ITermThemeColorKeys.BlueComponent] as RealNode ?? throw new ParseThemeException("Blue node value was not a real number");
var color = $"#{alpha:X2}{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
if (alpha == byte.MaxValue)
{
return $"#{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
@@ -42,10 +42,5 @@ namespace FluentTerminal.App.Services.Implementation
throw new KeyNotFoundException($"No handler registered for command: {command}");
}
public void ClearCommandHandlers()
{
_commandHandlers.Clear();
}
}
}
@@ -5,8 +5,6 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
namespace FluentTerminal.App.Services.Implementation
{
@@ -14,14 +12,12 @@ namespace FluentTerminal.App.Services.Implementation
{
public const string CurrentThemeKey = "CurrentTheme";
public const string DefaultShellProfileKey = "DefaultShellProfile";
public const string DefaultSshProfileKey = "DefaultSshProfile";
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IApplicationDataContainer _keyBindings;
private readonly IApplicationDataContainer _localSettings;
private readonly IApplicationDataContainer _roamingSettings;
private readonly IApplicationDataContainer _shellProfiles;
private readonly IApplicationDataContainer _sshProfiles;
private readonly IApplicationDataContainer _themes;
public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers)
@@ -33,8 +29,6 @@ namespace FluentTerminal.App.Services.Implementation
_themes = containers.Themes;
_keyBindings = containers.KeyBindings;
_shellProfiles = containers.ShellProfiles;
_sshProfiles = containers.SshProfiles;
foreach (var theme in _defaultValueProvider.GetPreInstalledThemes())
{
@@ -53,115 +47,25 @@ namespace FluentTerminal.App.Services.Implementation
}
}
public string ExportSettings()
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = GetCommandKeyBindings(),
TerminalOptions = GetTerminalOptions(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>()
};
public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
foreach (var theme in GetThemes().Where(x => !x.PreInstalled))
{
config.Themes.Add(theme);
}
public event EventHandler<TerminalTheme> ThemeAdded;
public event EventHandler<Guid> CurrentThemeChanged;
public event EventHandler<Guid> ThemeDeleted;
foreach (var profile in GetShellProfiles().Where(x => !x.PreInstalled))
{
config.Profiles.Add(profile);
}
public event EventHandler KeyBindingsChanged;
foreach (var profile in GetSshProfiles())
{
config.SshProfiles.Add(profile);
}
public event EventHandler<ShellProfile> ShellProfileAdded;
public event EventHandler<Guid> ShellProfileDeleted;
return JsonConvert.SerializeObject(config);
}
public void ImportSettings(string serializedSettings)
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = new Dictionary<string, ICollection<KeyBinding>>(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>(),
TerminalOptions = GetTerminalOptions()
};
JsonConvert.PopulateObject(serializedSettings, config);
SaveApplicationSettings(config.App);
// Since we set each command sepaartely, we don't need all existing settings
foreach (var pair in config.KeyBindings)
{
SaveKeyBindings(pair.Key, pair.Value);
}
// Can't create/modify pre-installed themes
foreach (var theme in config.Themes.Where(x => !x.PreInstalled))
{
var existingTheme = GetTheme(theme.Id);
if (existingTheme?.PreInstalled == true)
{
continue;
}
SaveTheme(theme, existingTheme != null);
}
// Can't create pre-installed profiles
foreach (var profile in config.Profiles.Where(x => !x.PreInstalled))
{
var existingProfile = GetShellProfile(profile.Id);
var isNew = existingProfile.EqualTo(default);
// You can only edit certain parts of preinstalled profiles
if (!isNew && existingProfile.PreInstalled)
{
existingProfile.WorkingDirectory = profile.WorkingDirectory;
existingProfile.Arguments = profile.Arguments;
existingProfile.TabThemeId = profile.TabThemeId;
existingProfile.TerminalThemeId = profile.TerminalThemeId;
existingProfile.LineEndingTranslation = profile.LineEndingTranslation;
existingProfile.KeyBindings = profile.KeyBindings;
SaveShellProfile(profile, isNew);
continue;
}
SaveShellProfile(profile, isNew);
}
foreach (var profile in config.SshProfiles)
{
var existingProfile = GetSshProfile(profile.Id);
var isNew = existingProfile.EqualTo(default(SshProfile));
SaveSshProfile(profile, isNew);
}
SaveTerminalOptions(config.TerminalOptions);
}
public event EventHandler<TerminalOptions> TerminalOptionsChanged;
public void DeleteShellProfile(Guid id)
{
_shellProfiles.Delete(id.ToString());
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
ShellProfileDeleted?.Invoke(this, id);
}
public void DeleteSshProfile(Guid id)
{
_sshProfiles.Delete(id.ToString());
Messenger.Default.Send(new SshProfileDeletedMessage(id));
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
public void DeleteTheme(Guid id)
{
_themes.Delete(id.ToString());
@@ -175,7 +79,7 @@ namespace FluentTerminal.App.Services.Implementation
}
}
Messenger.Default.Send(new ThemeDeletedMessage(id));
ThemeDeleted?.Invoke(this, id);
}
public ApplicationSettings GetApplicationSettings()
@@ -217,22 +121,11 @@ namespace FluentTerminal.App.Services.Implementation
}
return profile;
}
public SshProfile GetDefaultSshProfile()
{
var id = GetDefaultSshProfileId();
var profile = _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
return profile;
}
public ShellProfile GetShellProfile(Guid id)
{
return _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
}
public SshProfile GetSshProfile(Guid id)
{
return _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
}
public Guid GetDefaultShellProfileId()
{
@@ -242,14 +135,6 @@ namespace FluentTerminal.App.Services.Implementation
}
return _defaultValueProvider.GetDefaultShellProfileId();
}
public Guid GetDefaultSshProfileId()
{
if (_localSettings.TryGetValue(DefaultSshProfileKey, out object value))
{
return (Guid)value;
}
return System.Guid.Empty;
}
public IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings()
{
@@ -267,19 +152,6 @@ namespace FluentTerminal.App.Services.Implementation
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string)x)).ToList();
}
public IEnumerable<SshProfile> GetSshProfiles()
{
if (_sshProfiles == null)
{
return new List<SshProfile>();
}
else
{
return _sshProfiles.GetAll()
.Select(x => JsonConvert.DeserializeObject<SshProfile>((string)x)).ToList();
}
}
public IEnumerable<TabTheme> GetTabThemes()
{
return _defaultValueProvider.GetDefaultTabThemes();
@@ -307,45 +179,36 @@ namespace FluentTerminal.App.Services.Implementation
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
}
Messenger.Default.Send(new KeyBindingsChangedMessage());
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
}
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
{
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
}
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
{
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
ApplicationSettingsChanged?.Invoke(this, applicationSettings);
}
public void SaveCurrentThemeId(Guid id)
{
_roamingSettings.SetValue(CurrentThemeKey, id);
Messenger.Default.Send(new CurrentThemeChangedMessage(id));
CurrentThemeChanged?.Invoke(this, id);
}
public void SaveDefaultShellProfileId(Guid id)
{
_localSettings.SetValue(DefaultShellProfileKey, id);
}
public void SaveDefaultSshProfileId(Guid id)
{
_localSettings.SetValue(DefaultSshProfileKey, id);
}
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
{
if (!Enum.TryParse<Command>(command, true, out var enumValue))
if (!Enum.IsDefined(typeof(Command), command))
{
throw new InvalidOperationException();
}
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
Messenger.Default.Send(new KeyBindingsChangedMessage());
_keyBindings.WriteValueAsJson(command.ToString(), keyBindings);
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
}
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
@@ -353,30 +216,18 @@ namespace FluentTerminal.App.Services.Implementation
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
Messenger.Default.Send(new KeyBindingsChangedMessage());
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
if (newShell)
{
Messenger.Default.Send(new ShellProfileAddedMessage(shellProfile));
}
}
public void SaveSshProfile(SshProfile sshProfile, bool newShell = false)
{
_sshProfiles.WriteValueAsJson(sshProfile.Id.ToString(), sshProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
Messenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
Messenger.Default.Send(new SshProfileAddedMessage(sshProfile));
ShellProfileAdded?.Invoke(this, shellProfile);
}
}
public void SaveTerminalOptions(TerminalOptions terminalOptions)
{
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
Messenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions));
TerminalOptionsChanged?.Invoke(this, terminalOptions);
}
public void SaveTheme(TerminalTheme theme, bool newTheme = false)
@@ -385,12 +236,12 @@ namespace FluentTerminal.App.Services.Implementation
if (theme.Id == GetCurrentThemeId())
{
Messenger.Default.Send(new CurrentThemeChangedMessage(theme.Id));
CurrentThemeChanged?.Invoke(this, theme.Id);
}
if (newTheme)
{
Messenger.Default.Send(new ThemeAddedMessage(theme));
ThemeAdded?.Invoke(this, theme);
}
}
}
@@ -1,32 +0,0 @@
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services.Implementation
{
public class ShellProfileMigrationService : IShellProfileMigrationService
{
private readonly Dictionary<int, Action<ShellProfile>> _migrationSteps = new Dictionary<int, Action<ShellProfile>>
{
[1] = new Action<ShellProfile>(profile =>
{
profile.EnvironmentVariables["TERM"] = "xterm-256color";
})
};
public void Migrate(ShellProfile profile)
{
while(profile.MigrationVersion < ShellProfile.CurrentMigrationVersion)
{
ApplyMigrationStep(profile, profile.MigrationVersion + 1);
}
}
private void ApplyMigrationStep(ShellProfile profile, int targetVersion)
{
_migrationSteps[targetVersion](profile);
profile.MigrationVersion = targetVersion;
}
}
}
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services.Implementation
{
public class ThemeParserFactory : IThemeParserFactory
{
private readonly IEnumerable<IThemeParser> _parsers;
private IEnumerable<IThemeParser> _parsers;
public IEnumerable<string> SupportedFileTypes { get; }
@@ -1,5 +1,4 @@
using FluentTerminal.App.Services.Exceptions;
using FluentTerminal.Models;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Requests;
using FluentTerminal.Models.Responses;
@@ -7,7 +6,6 @@ using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
namespace FluentTerminal.App.Services.Implementation
{
@@ -15,129 +13,35 @@ namespace FluentTerminal.App.Services.Implementation
{
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers;
private byte _nextTerminalId = 0;
private readonly Dictionary<int, Action<byte[]>> _terminalOutputHandlers;
private int _nextTerminalId = 0;
public event EventHandler<TerminalExitStatus> TerminalExited;
public event EventHandler<int> TerminalExited;
public TrayProcessCommunicationService(ISettingsService settingsService)
{
_settingsService = settingsService;
_terminalOutputHandlers = new Dictionary<byte, Action<byte[]>>();
_terminalOutputHandlers = new Dictionary<int, Action<byte[]>>();
}
public byte GetNextTerminalId()
public int GetNextTerminalId()
{
return _nextTerminalId++;
}
private string _userName;
public async Task<string> GetUserName()
public async Task<GetAvailablePortResponse> GetAvailablePort()
{
if (!string.IsNullOrEmpty(_userName))
{
// Returning the username from cache
return _userName;
}
var request = new GetAvailablePortRequest();
StringValueResponse response;
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
// No need to crash for username, so try/catch
try
{
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new GetUserNameRequest()));
response = JsonConvert.DeserializeObject<StringValueResponse>((string)responseMessage[MessageKeys.Content]);
}
catch (Exception e)
{
Logger.Instance.Error(e, "Error while trying to get username.");
Logger.Instance.Debug("Received GetAvailablePortResponse: {@response}", response);
return null;
}
Logger.Instance.Debug("Received GetUserNameResponse: {@response}", response);
_userName = response.Value;
return _userName;
return response;
}
public async Task SaveTextFileAsync(string path, string content)
{
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new SaveTextFileRequest {Path = path, Content = content}));
var response = JsonConvert.DeserializeObject<CommonResponse>((string)responseMessage[MessageKeys.Content]);
if (!response.Success)
{
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error) ? "Failed to save the file." : response.Error);
}
}
private static string _sshConfigDir;
public async Task<string> GetSshConfigDirAsync()
{
if (string.IsNullOrEmpty(_sshConfigDir))
{
var response = await GetSshConfigFolderAsync(false);
if (response?.Success ?? false)
{
_sshConfigDir = response.Path;
}
}
return _sshConfigDir;
}
public async Task<string[]> GetFilesFromSshConfigDirAsync()
{
var response = await GetSshConfigFolderAsync(false);
if (response == null || !response.Success)
{
return null;
}
if (string.IsNullOrEmpty(_sshConfigDir))
{
_sshConfigDir = response.Path;
}
return response.Files;
}
private async Task<GetSshConfigFolderResponse> GetSshConfigFolderAsync(bool includeContent)
{
var responseMessage =
await _appServiceConnection.SendMessageAsync(CreateMessage(new GetSshConfigFolderRequest
{IncludeContent = includeContent}));
return JsonConvert.DeserializeObject<GetSshConfigFolderResponse>(
(string) responseMessage[MessageKeys.Content]);
}
public async Task<bool> CheckFileExistsAsync(string path)
{
var responseMessage =
await _appServiceConnection.SendMessageAsync(CreateMessage(new CheckFileExistsRequest {Path = path}));
return JsonConvert.DeserializeObject<CommonResponse>((string) responseMessage[MessageKeys.Content]).Success;
}
public void MuteTerminal(bool mute)
{
_appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest { Mute = mute }));
}
public void UpdateSettings(ApplicationSettings settings)
{
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
}
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
public async Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
{
var request = new CreateTerminalRequest
{
@@ -150,63 +54,48 @@ namespace FluentTerminal.App.Services.Implementation
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>((string)responseMessage[MessageKeys.Content]);
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>(responseMessage[MessageKeys.Content]);
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
return response;
}
public async Task<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause)
{
var request = new PauseTerminalOutputRequest
{
Id = id,
Pause = pause
};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
return JsonConvert.DeserializeObject<PauseTerminalOutputResponse>((string)responseMessage[MessageKeys.Content]);
}
public void Initialize(IAppServiceConnection appServiceConnection)
{
_appServiceConnection = appServiceConnection;
_appServiceConnection.MessageReceived += OnMessageReceived;
}
private void OnMessageReceived(object sender, IDictionary<string, object> e)
private void OnMessageReceived(object sender, IDictionary<string, string> e)
{
var messageType = (byte)e[MessageKeys.Type];
var messageType = e[MessageKeys.Type];
var messageContent = e[MessageKeys.Content];
switch (messageType)
if (messageType == nameof(DisplayTerminalOutputRequest))
{
case Constants.TerminalBufferRequestIdentifier:
var terminalId = (byte)e[MessageKeys.TerminalId];
var request = JsonConvert.DeserializeObject<DisplayTerminalOutputRequest>(messageContent);
if (_terminalOutputHandlers.ContainsKey(terminalId))
{
_terminalOutputHandlers[terminalId].Invoke((byte[])messageContent);
}
else
{
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId);
}
break;
case (byte) MessageIdentifiers.TerminalExitedRequest:
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent);
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
if (_terminalOutputHandlers.ContainsKey(request.TerminalId))
{
_terminalOutputHandlers[request.TerminalId].Invoke(request.Output);
}
else
{
Logger.Instance.Error("Received output for unknown terminal Id {id}", request.TerminalId);
}
}
else if (messageType == nameof(TerminalExitedRequest))
{
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
TerminalExited?.Invoke(this, request.ToStatus());
break;
default:
Logger.Instance.Error("Received unknown message type: {messageType}", messageType);
break;
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
TerminalExited?.Invoke(this, request.TerminalId);
}
}
public Task ResizeTerminal(byte id, TerminalSize size)
public Task ResizeTerminal(int id, TerminalSize size)
{
var request = new ResizeTerminalRequest
{
@@ -217,19 +106,11 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
public void SubscribeForTerminalOutput(int terminalId, Action<byte[]> callback)
{
_terminalOutputHandlers[terminalId] = callback;
}
public void UnsubscribeFromTerminalOutput(byte id)
{
if (_terminalOutputHandlers.ContainsKey(id))
{
_terminalOutputHandlers.Remove(id);
}
}
public Task UpdateToggleWindowKeyBindings()
{
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
@@ -242,75 +123,34 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public Task Write(byte id, byte[] data)
public Task Write(int id, byte[] data)
{
var message = new ValueSet
var request = new WriteDataRequest
{
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
[MessageKeys.TerminalId] = id,
[MessageKeys.Content] = data
TerminalId = id,
Data = data
};
return _appServiceConnection.SendMessageAsync(message);
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public Task CloseTerminal(byte terminalId)
public Task CloseTerminal(int terminalId)
{
var request = new TerminalExitedRequest(terminalId, -1);
var request = new TerminalExitedRequest
{
TerminalId = terminalId
};
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
public async Task<string> GetCommandPathAsync(string command)
private IDictionary<string, string> CreateMessage(object content)
{
if (string.IsNullOrWhiteSpace(command))
return new Dictionary<string, string>
{
throw new ArgumentException("Input value is null or empty.", nameof(command));
}
command = command.Trim();
var commandLower = command.ToLowerInvariant();
if (CommandPaths.TryGetValue(commandLower, out var path))
{
return path;
}
if (CommandErrors.TryGetValue(commandLower, out var error))
{
throw new Exception(error);
}
var request = new GetCommandPathRequest {Command = command};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response =
JsonConvert.DeserializeObject<StringValueResponse>((string) responseMessage[MessageKeys.Content]);
if (response.Success)
{
CommandPaths[commandLower] = response.Value;
return response.Value;
}
CommandErrors[commandLower] = response.Error;
throw new Exception(response.Error);
}
private ValueSet CreateMessage(IMessage content)
{
return new ValueSet
{
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Type] = content.GetType().Name,
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
};
}
+1 -3
View File
@@ -35,9 +35,7 @@ namespace FluentTerminal.App.Services
.MinimumLevel.Is((LogEventLevel)configuration.LogLevel)
.CreateLogger();
// Log file is not created during unhandled exception processing.
// Force log file creation with the highest severity initialization record.
Log.Fatal("Initialized");
Log.Information("Initialized");
}
public void Debug(string text, params object[] propertyValues)
+143 -180
View File
@@ -1,181 +1,144 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Text;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public class Terminal
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
private bool _closingFromUI;
private bool _exited;
private readonly bool _requireShellProcessStart;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId();
_requireShellProcessStart = !terminalId.HasValue;
}
private void OnTerminalExited(object sender, TerminalExitStatus status)
{
if (status.TerminalId != Id)
{
return;
}
_exited = true;
Exited?.Invoke(this, status.ExitCode);
if (_closingFromUI || status.ExitCode <= 0)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
_trayProcessCommunicationService.TerminalExited -= OnTerminalExited;
}
public event EventHandler<int> Exited;
/// <summary>
/// To be observed by both view and viewmodel
/// </summary>
public event EventHandler Closed;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> KeyboardCommandReceived;
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<byte[]> OutputReceived;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<TerminalSize> SizeChanged;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public byte Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
{
if (_exited)
{
Closed?.Invoke(this, System.EventArgs.Empty);
return;
}
_closingFromUI = true;
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
}
/// <summary>
/// to be called by viewmodel
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback?.Invoke();
}
/// <summary>
/// to be called by view
/// </summary>
public void ProcessKeyboardCommand(string command)
{
KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be set by view
/// </summary>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
{
_selectedTextCallback = selectedTextCallback;
}
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
SizeChanged?.Invoke(this, size);
}
/// <summary>
/// to be called by view
/// </summary>
public void SetTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// </summary>
/// <param name="shellProfile"></param>
/// <param name="size"></param>
/// <param name="sessionType"></param>
public async Task<TerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
{
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
{
OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState));
}
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
if (_requireShellProcessStart)
{
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
}
return response;
}
else
{
return await _trayProcessCommunicationService.PauseTerminalOutput(Id, false);
}
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.Write(Id, data);
}
/// <summary>
/// Tells the ViewModel that this view failed to launch and can be closed.
/// </summary>
public void ReportLauchFailed()
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public class Terminal
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
Id = _trayProcessCommunicationService.GetNextTerminalId();
}
private void OnTerminalExited(object sender, int e)
{
if (e == Id)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
/// <summary>
/// To be observed by both view and viewmodel
/// </summary>
public event EventHandler Closed;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> KeyboardCommandReceived;
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<byte[]> OutputReceived;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<TerminalSize> SizeChanged;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public int Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
{
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
}
/// <summary>
/// to be called by viewmodel
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback();
}
/// <summary>
/// to be called by view
/// </summary>
public void ProcessKeyboardCommand(string command)
{
KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be set by view
/// </summary>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
{
_selectedTextCallback = selectedTextCallback;
}
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
SizeChanged?.Invoke(this, size);
}
/// <summary>
/// to be called by view
/// </summary>
public void SetTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// </summary>
/// <param name="shellProfile"></param>
/// <param name="size"></param>
/// <param name="sessionType"></param>
public async Task<CreateTerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
{
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
}
return response;
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.Write(Id, data);
}
/// <summary>
/// Tells the ViewModel that this view failed to launch and can be closed.
/// </summary>
public void ReportLauchFailed()
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
}
@@ -1,24 +0,0 @@
using Windows.ApplicationModel.Resources;
namespace FluentTerminal.App.Services.Utilities
{
/// <summary>
/// Get strings in different languages.
/// Author: Zuozishi
/// </summary>
public static class I18N
{
public static string Translate(string resource)
{
var resourceLoader = ResourceLoader.GetForCurrentView();
return resourceLoader.GetString(resource.Replace('.', '/'));
}
public static string TranslateWithFallback(string resource, string fallback)
{
var result = Translate(resource);
return string.IsNullOrEmpty(result) ? fallback : result;
}
}
}
@@ -1,25 +0,0 @@
using GalaSoft.MvvmLight;
using System;
namespace FluentTerminal.App.ViewModels
{
public class EnvironmentVariableViewModel : ViewModelBase
{
private string _name;
private string _value;
public Guid Id { get; } = Guid.NewGuid();
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public string Value
{
get => _value;
set => Set(ref _value, value);
}
}
}
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fleck" Version="1.0.3" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
</ItemGroup>
@@ -1,11 +0,0 @@
namespace FluentTerminal.App.ViewModels
{
public interface ISessionSuccessTracker
{
void SetSuccessfulSessionStart();
void SetOutputReceived();
void SetInvalid();
}
}
@@ -12,7 +12,7 @@ namespace FluentTerminal.App.ViewModels.Infrastructure
bool CanExecute();
}
public interface IAsyncCommand<in T> : ICommand
public interface IAsyncCommand<T> : ICommand
{
Task ExecuteAsync(T parameter);
bool CanExecute(T parameter);
File diff suppressed because it is too large. Load diff
@@ -1,53 +0,0 @@
using FluentTerminal.App.Services;
using GalaSoft.MvvmLight;
using System;
namespace FluentTerminal.App.ViewModels
{
public class OverlayViewModel : ViewModelBase
{
private readonly IDispatcherTimer _overlayTimer;
private bool _showOverlay;
private string _overlayContent;
public OverlayViewModel(IDispatcherTimer dispatcherTimer)
{
_overlayTimer = dispatcherTimer;
_overlayTimer.Interval = new TimeSpan(0, 0, 2);
}
public bool ShowOverlay
{
get => _showOverlay;
set => Set(ref _showOverlay, value);
}
public string OverlayContent
{
get => _overlayContent;
set => Set(ref _overlayContent, value);
}
public void Show(string message)
{
OverlayContent = message;
ShowOverlay = true;
if (_overlayTimer.IsEnabled)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
}
_overlayTimer.Start();
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
}
private void OnResizeOverlayTimerFinished(object sender, object e)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
ShowOverlay = false;
}
}
}
@@ -1,146 +0,0 @@
using System.Threading.Tasks;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels.Profiles
{
/// <summary>
/// Used for common profiles (PowerShell, CMD, WSL).
/// </summary>
public class CommonProfileProviderViewModel : ProfileProviderViewModelBase
{
#region Fields
private readonly IFileSystemService _fileSystemService;
#endregion Fields
#region Properties
public bool PreInstalled { get; private set; }
private string _location;
public string Location
{
get => _location;
set => Set(ref _location, value);
}
private string _workingDirectory;
public string WorkingDirectory
{
get => _workingDirectory;
set => Set(ref _workingDirectory, value);
}
private string _arguments;
public string Arguments
{
get => _arguments;
set => Set(ref _arguments, value);
}
#endregion Properties
#region Commands
public IAsyncCommand BrowseForCustomShellCommand { get; }
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
#endregion Commands
#region Constructor
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
true, original)
{
_fileSystemService = fileSystemService;
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
Initialize(Model);
}
#endregion Constructor
#region Methods
private void Initialize(ShellProfile profile)
{
PreInstalled = profile.PreInstalled;
Location = profile.Location;
Arguments = profile.Arguments;
WorkingDirectory = profile.WorkingDirectory;
}
protected override void LoadFromProfile(ShellProfile profile)
{
base.LoadFromProfile(profile);
Initialize(profile);
}
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile);
profile.Location = _location;
profile.Arguments = _arguments;
profile.WorkingDirectory = _workingDirectory;
}
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync();
if (!string.IsNullOrEmpty(error))
{
return error;
}
if (!string.IsNullOrEmpty(_location))
{
return null;
}
error = I18N.Translate("LocationCantBeEmpty");
return string.IsNullOrEmpty(error) ? "Location cannot be empty." : error;
}
public override bool HasChanges()
{
return base.HasChanges() || !Model.Location.NullableEqualTo(_location) ||
!Model.WorkingDirectory.NullableEqualTo(_workingDirectory) ||
!Model.Arguments.NullableEqualTo(_arguments);
}
private async Task BrowseForCustomShell()
{
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
if (file != null)
{
Location = file.Path;
}
}
private async Task BrowseForWorkingDirectory()
{
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
if (directory != null)
{
WorkingDirectory = directory;
}
}
#endregion Methods
}
}
@@ -1,18 +0,0 @@
using System;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels.Profiles
{
public class ExecutedCommand
{
public string Value { get; set; }
public bool IsProfile { get; set; }
public DateTime LastExecution { get; set; }
public int ExecutionCount { get; set; }
public ShellProfile ShellProfile { get; set; }
}
}
@@ -1,401 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels.Profiles
{
/// <summary>
/// Base class for all profile view model classes. It contains properties shared by all other view models
/// (theme-related properties, line-ending translation, and WinPTY/ConPTY selection).
/// </summary>
public abstract class ProfileProviderViewModelBase : ViewModelBase
{
#region Static
private static readonly LineEndingStyle[] LineEndingStylesArray =
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
#endregion Static
#region Fields
protected readonly ISettingsService SettingsService;
protected readonly IApplicationView ApplicationView;
private readonly bool _strictProfileType;
#endregion Fields
#region Properties
private ShellProfile _model;
public ShellProfile Model
{
get => _model;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (ReferenceEquals(_model, value))
{
return;
}
if (_strictProfileType && _model != null && _model.GetType() != value.GetType())
{
throw new ArgumentException($"The value has to be of type {_model.GetType().Name}.");
}
_model = value;
LoadFromProfile(value);
}
}
public ObservableCollection<TabTheme> TabThemes { get; }
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
private LineEndingStyle _lineEndingTranslation;
public LineEndingStyle LineEndingTranslation
{
get => _lineEndingTranslation;
set => Set(ref _lineEndingTranslation, value);
}
private TabTheme _selectedTabTheme;
public TabTheme SelectedTabTheme
{
get => _selectedTabTheme ?? (_selectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(_tabThemeId)));
set
{
if (value == null)
{
// Ignore attempt setting null.
return;
}
// Ensure that it's from the list
var theme = TabThemes.Contains(value)
? value
: TabThemes.FirstOrDefault(t => t.Id == value.Id) ?? TabThemes.First();
if (Set(ref _selectedTabTheme, theme))
{
_tabThemeId = theme.Id;
}
}
}
private int _tabThemeId;
public int TabThemeId
{
get => _tabThemeId;
set
{
if (Set(ref _tabThemeId, value))
{
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(value));
}
}
}
private TerminalTheme _selectedTerminalTheme;
public TerminalTheme SelectedTerminalTheme
{
get => _selectedTerminalTheme ??
(_selectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(_terminalThemeId)));
set
{
TerminalTheme theme = value;
if (theme == null)
{
// How to handle attempt to setting null theme?
// 1) Throw an exception:
//throw new ArgumentNullException();
// 2) Ignore the attempt:
//return;
// 3) Use default theme (probably the best):
theme = TerminalThemes.First();
}
else if (!TerminalThemes.Contains(theme))
{
// Ensure that it's from the list
theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(theme.Id)) ?? TerminalThemes.First();
}
if (Set(ref _selectedTerminalTheme, theme))
{
_terminalThemeId = theme.Id;
}
}
}
private Guid _terminalThemeId;
public Guid TerminalThemeId
{
get => _terminalThemeId;
set
{
if (Set(ref _terminalThemeId, value))
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
}
}
}
private bool _useConPty;
public bool UseConPty
{
get => _useConPty;
set => Set(ref _useConPty, value);
}
#endregion Properties
#region Constructor
protected ProfileProviderViewModelBase(ISettingsService settingsService, IApplicationView applicationView,
bool strictProfileType, ShellProfile original = null)
{
SettingsService = settingsService;
ApplicationView = applicationView;
_strictProfileType = strictProfileType;
_model = original ?? new ShellProfile();
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
SelectedTabTheme = TabThemes.First();
TerminalThemes = new ObservableCollection<TerminalTheme>(settingsService.GetThemes());
TerminalThemes.Insert(0, new TerminalTheme {Id = Guid.Empty, Name = "Default"});
SelectedTerminalTheme = TerminalThemes.First();
MessengerInstance.Register<ThemeAddedMessage>(this, OnThemeAdded);
MessengerInstance.Register<ThemeDeletedMessage>(this, OnThemeDeleted);
Initialize(Model);
}
#endregion Constructor
#region Methods
private void OnThemeDeleted(ThemeDeletedMessage message)
{
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(message.ThemeId));
if (theme == null)
{
return;
}
ApplicationView.RunOnDispatcherThread(() =>
{
TerminalThemes.Remove(theme);
if (_terminalThemeId.Equals(message.ThemeId))
{
SelectedTerminalTheme = TerminalThemes.First();
}
}, false);
}
private void OnThemeAdded(ThemeAddedMessage message)
{
ApplicationView.RunOnDispatcherThread(() => TerminalThemes.Add(message.Theme), false);
}
private void Initialize(ShellProfile profile)
{
LineEndingTranslation = profile.LineEndingTranslation;
UseConPty = profile.UseConPty;
TerminalThemeId = profile.TerminalThemeId;
TabThemeId = profile.TabThemeId;
}
protected virtual void LoadFromProfile(ShellProfile profile)
{
Initialize(profile);
}
protected virtual Task CopyToProfileAsync(ShellProfile profile)
{
profile.LineEndingTranslation = _lineEndingTranslation;
profile.UseConPty = _useConPty;
profile.TerminalThemeId = _terminalThemeId;
profile.TabThemeId = _tabThemeId;
return Task.CompletedTask;
}
public virtual Task<string> ValidateAsync()
{
return Task.FromResult<string>(null);
}
/// <summary>
/// Returns <c>true</c> if view model values aren't equal to the corresponding values of the
/// underlying <see cref="ShellProfile"/>.
/// </summary>
public virtual bool HasChanges()
{
return Model.LineEndingTranslation != _lineEndingTranslation ||
Model.UseConPty != _useConPty ||
!Model.TerminalThemeId.Equals(_terminalThemeId) ||
Model.TabThemeId != _tabThemeId;
}
/// <summary>
/// Copies values from the view model to the underlying <see cref="ShellProfile"/>.
/// </summary>
/// <param name="acceptIfInvalid">Defines what will happen if the view model data is invalid (if
/// <see cref="ValidateAsync"/> returns non-empty string). If this parameter is set to <c>true</c>, the
/// data will be copied to the underlying <see cref="ShellProfile"/> anyway. If it's set to <c>false</c>,
/// (default) the underlying <see cref="ShellProfile"/> won't be changed at all.</param>
/// <returns><c>null</c> or empty string if the operation was successful (the data is valid), or an error
/// message returned by <see cref="ValidateAsync"/> method.</returns>
public async Task<string> AcceptChangesAsync(bool acceptIfInvalid = false)
{
var error = await ValidateAsync();
if (acceptIfInvalid || string.IsNullOrEmpty(error))
{
await CopyToProfileAsync(Model);
}
return error;
}
public void RejectChanges()
{
ApplicationView.RunOnDispatcherThread(() => LoadFromProfile(Model), false);
}
#endregion Methods
#region Links/shortcuts related
private const string UseConPtyQueryStringName = "conpty";
private const string LineEndingQueryStringName = "lineending";
private const string TerminalThemeIdQueryStringName = "theme";
private const string TabThemeIdQueryStringName = "tab";
private const string ShortcutFileFormat = @"[{{000214A0-0000-0000-C000-000000000046}}]
Prop3=19,0
[InternetShortcut]
IDList=
URL={0}
";
public static string GetShortcutFileContent(string url) => string.Format(ShortcutFileFormat, url);
public static IEnumerable<Tuple<string, string>> ParseParams(string uriOpts, char separator) =>
uriOpts.Split(separator).Select(ParseSshOptionFromUri).Where(p => p != null);
private static Tuple<string, string> ParseSshOptionFromUri(string option)
{
string[] nv = option.Split('=');
if (nv.Length != 2 || string.IsNullOrEmpty(nv[0]))
{
// For now simply ignore invalid options
return null;
//throw new FormatException($"Invalid SSH option '{option}'.");
}
return Tuple.Create(HttpUtility.UrlDecode(nv[0]), HttpUtility.UrlDecode(nv[1]));
}
/// <summary>
/// Generates URL.
/// </summary>
/// <returns><see cref="Tuple"/> whose first item represents success. If the first item is <c>true</c>,
/// the second contains the URL. If the first item is <c>false</c>, the second contains error message.</returns>
public virtual Task<Tuple<bool, string>> GetUrlAsync() =>
Task.FromResult(Tuple.Create(false, "Not supported."));
public string GetBaseQueryString()
{
var queryString =
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
if (_tabThemeId != TabThemes.First().Id)
{
queryString += $"&{TabThemeIdQueryStringName}={_tabThemeId:##########}";
}
if (!_terminalThemeId.Equals(TerminalThemes.First().Id))
{
queryString += $"&{TerminalThemeIdQueryStringName}={_terminalThemeId}";
}
return queryString;
}
public void LoadBaseFromQueryString(IList<Tuple<string, string>> queryStringParams)
{
if (queryStringParams == null)
{
return;
}
var keyValue = queryStringParams.FirstOrDefault(t =>
UseConPtyQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useConPty))
{
UseConPty = useConPty;
}
keyValue = queryStringParams.FirstOrDefault(t =>
LineEndingQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(keyValue?.Item2) && Enum.TryParse(keyValue.Item2, true, out LineEndingStyle lineEndingTranslation))
{
LineEndingTranslation = lineEndingTranslation;
}
keyValue = queryStringParams.FirstOrDefault(t =>
TabThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(keyValue?.Item2) && int.TryParse(keyValue.Item2, out int tabThemeId))
{
TabThemeId = tabThemeId;
}
keyValue = queryStringParams.FirstOrDefault(t =>
TerminalThemeIdQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(keyValue?.Item2) && Guid.TryParse(keyValue.Item2, out Guid terminalThemeId))
{
TerminalThemeId = terminalThemeId;
}
}
#endregion Links/shortcuts related
}
}
@@ -1,521 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels.Profiles
{
/// <summary>
/// View-model for rich-UI based SSH profiles.
/// </summary>
public class SshConnectViewModel : ProfileProviderViewModelBase
{
#region Static
public static string GetErrorString(SshConnectionInfoValidationResult result, string separator = "; ") =>
string.Join(separator, GetErrors(result));
public static IEnumerable<string> GetErrors(SshConnectionInfoValidationResult result)
{
if (result == SshConnectionInfoValidationResult.Valid)
{
yield break;
}
foreach (var value in Enum.GetValues(typeof(SshConnectionInfoValidationResult))
.Cast<SshConnectionInfoValidationResult>().Where(r => r != SshConnectionInfoValidationResult.Valid))
{
if ((value & result) == value)
{
yield return I18N.Translate($"{nameof(SshConnectionInfoValidationResult)}.{value}");
}
}
}
#endregion Static
#region Fields
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IFileSystemService _fileSystemService;
// To prevent validating the existence of the same file multiple times because it's kinda expensive
private string _validatedIdentityFile;
#endregion Fields
#region Properties
private string _host;
public string Host
{
get => _host;
set => Set(ref _host, value);
}
private ushort _sshPort;
public ushort SshPort
{
get => _sshPort;
set => Set(ref _sshPort, value);
}
private string _username;
public string Username
{
get => _username;
set => Set(ref _username, value);
}
private string _identityFile;
public string IdentityFile
{
get => _identityFile;
set => Set(ref _identityFile, value);
}
private bool _useMosh;
public bool UseMosh
{
get => _useMosh;
set => Set(ref _useMosh, value);
}
private ushort _moshPortFrom;
public ushort MoshPortFrom
{
get => _moshPortFrom;
set => Set(ref _moshPortFrom, value);
}
private ushort _moshPortTo;
public ushort MoshPortTo
{
get => _moshPortTo;
set => Set(ref _moshPortTo, value);
}
#endregion Properties
#region Commands
public IAsyncCommand BrowseForIdentityFileCommand { get; }
#endregion Commands
#region Constructor
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
SshProfile original = null) : base(settingsService, applicationView, true,
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
Initialize((SshProfile)Model);
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
}
#endregion Constructor
#region Methods
// Fills the view model properties from the input sshProfile
private void Initialize(SshProfile sshProfile)
{
Host = sshProfile.Host;
SshPort = sshProfile.SshPort;
Username = sshProfile.Username;
if (string.IsNullOrEmpty(Username))
{
_trayProcessCommunicationService.GetUserName().ContinueWith(t =>
{
var username = t.Result;
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
{
ApplicationView.RunOnDispatcherThread(() => Username = username, false);
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
}
IdentityFile = sshProfile.IdentityFile;
UseMosh = sshProfile.UseMosh;
MoshPortFrom = sshProfile.MoshPortFrom;
MoshPortTo = sshProfile.MoshPortTo;
}
private async Task BrowseForIdentityFile()
{
var file = await _fileSystemService.OpenFile(new[] { "*" });
if (file != null)
{
IdentityFile = file.Path;
}
}
private string GetArgumentsString()
{
StringBuilder sb = new StringBuilder();
if (_sshPort != SshProfile.DefaultSshPort)
sb.Append($"-p {_sshPort:#####} ");
if (!string.IsNullOrEmpty(_identityFile))
sb.Append($"-i \"{_identityFile}\" ");
sb.Append($"{_username}@{_host}");
if (_useMosh)
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
return sb.ToString();
}
protected override void LoadFromProfile(ShellProfile profile)
{
base.LoadFromProfile(profile);
Initialize((SshProfile)profile);
}
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile);
SshProfile sshProfile = (SshProfile) profile;
sshProfile.Location = _useMosh ? Constants.MoshCommandName : Constants.SshCommandName;
sshProfile.Arguments = GetArgumentsString();
sshProfile.WorkingDirectory = null;
sshProfile.Host = _host;
sshProfile.SshPort = _sshPort;
sshProfile.Username = _username;
sshProfile.IdentityFile = _identityFile;
sshProfile.UseMosh = _useMosh;
sshProfile.MoshPortFrom = _moshPortFrom;
sshProfile.MoshPortTo = _moshPortTo;
}
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync();
if (!string.IsNullOrEmpty(error))
{
return error;
}
var result = await GetSshInfoValidationResultAsync();
if (result == SshConnectionInfoValidationResult.Valid)
{
return null;
}
error = GetErrorString(result, Environment.NewLine);
if (string.IsNullOrEmpty(error))
{
error = "Invalid input.";
}
return error;
}
public override bool HasChanges()
{
var original = (SshProfile)Model;
return base.HasChanges() || !original.Host.NullableEqualTo(_host) || original.SshPort != _sshPort ||
!original.Username.NullableEqualTo(_username) ||
!original.IdentityFile.NullableEqualTo(_identityFile) || original.UseMosh != _useMosh ||
original.MoshPortFrom != _moshPortFrom || original.MoshPortTo != _moshPortTo;
}
public async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
{
var result = SshConnectionInfoValidationResult.Valid;
if (string.IsNullOrEmpty(_username))
{
result |= SshConnectionInfoValidationResult.UsernameEmpty;
}
if (string.IsNullOrEmpty(_host))
{
result |= SshConnectionInfoValidationResult.HostEmpty;
}
if (_sshPort < 1)
{
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
}
if (!await CheckIdentityFileExistsAsync())
{
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
}
if (!_useMosh)
{
return result;
}
if (_moshPortFrom < 1)
{
result |= SshConnectionInfoValidationResult.MoshPortZeroOrNegative;
}
if (_moshPortFrom > _moshPortTo)
{
result |= SshConnectionInfoValidationResult.MoshPortRangeInvalid;
}
return result;
}
private async Task<bool> CheckIdentityFileExistsAsync()
{
var identityFile = _identityFile;
if (string.IsNullOrEmpty(identityFile) ||
identityFile.Equals(_validatedIdentityFile, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Here we need to take into account that files from ssh config dir can be provided by name, without full path.
string fullPath;
if (Path.IsPathRooted(identityFile))
{
fullPath = identityFile;
}
else
{
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
if (string.IsNullOrEmpty(sshConfigDir))
{
return false;
}
fullPath = Path.Combine(sshConfigDir, identityFile);
}
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
{
_validatedIdentityFile = identityFile;
IdentityFile = fullPath;
return true;
}
return false;
}
public void SetValidatedIdentityFile(string identityFile)
{
_validatedIdentityFile = identityFile;
IdentityFile = identityFile;
}
#endregion Methods
#region Links/shortcuts related
// Resources:
// https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04#section-3
// https://man.openbsd.org/ssh
private const string SshUriScheme = "ssh";
private const string MoshUriScheme = "mosh";
// Constant derived from https://man.openbsd.org/ssh
private const string IdentityFileOptionName = "IdentityFile";
private static readonly string[] ValidMoshPortsNames = { "mosh_ports", "mosh-ports" };
private static readonly Regex MoshRangeRx =
new Regex(@"^(?<from>\d{1,5})[:-](?<to>\d{1,5})$", RegexOptions.Compiled);
public static bool CheckScheme(Uri uri) =>
SshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase) ||
MoshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
public static SshConnectViewModel ParseUri(Uri uri, ISettingsService settingsService,
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
{
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
fileSystemService)
{
Host = uri.Host,
UseMosh = MoshUriScheme.Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase)
};
if (uri.Port >= 0)
vm.SshPort = (ushort)uri.Port;
if (!string.IsNullOrEmpty(uri.UserInfo))
{
string[] parts = uri.UserInfo.Split(';');
if (parts.Length > 2)
throw new FormatException($"UserInfo part contains {parts.Length} elements.");
vm.Username = HttpUtility.UrlDecode(parts[0]);
if (parts.Length > 1)
{
// For now we are only interested in IdentityFile option
Tuple<string, string> identityFileOption = ParseParams(parts[1], ',').FirstOrDefault(p =>
string.Equals(p.Item1, IdentityFileOptionName, StringComparison.OrdinalIgnoreCase));
vm.IdentityFile = identityFileOption?.Item2;
}
}
// ReSharper disable once ConstantConditionalAccessQualifier
string queryString = uri.Query?.Trim();
if (string.IsNullOrEmpty(queryString))
{
return vm;
}
if (queryString.StartsWith("?", StringComparison.Ordinal))
{
queryString = queryString.Substring(1);
}
var queryStringParams = ParseParams(queryString, '&').ToList();
var moshPorts = queryStringParams.FirstOrDefault(p =>
ValidMoshPortsNames.Any(n => string.Equals(p.Item1, n, StringComparison.OrdinalIgnoreCase)));
if (moshPorts != null)
{
Match match = MoshRangeRx.Match(moshPorts.Item2);
if (match.Success)
{
vm.MoshPortFrom = ushort.Parse(match.Groups["from"].Value);
vm.MoshPortTo = ushort.Parse(match.Groups["to"].Value);
}
}
vm.LoadBaseFromQueryString(queryStringParams);
return vm;
}
public override async Task<Tuple<bool, string>> GetUrlAsync()
{
var error = await base.ValidateAsync();
if (!string.IsNullOrEmpty(error))
{
return Tuple.Create(false, error);
}
var result = await GetSshInfoValidationResultAsync();
if (result != SshConnectionInfoValidationResult.Valid &&
// For links we can ignore missing username
result != SshConnectionInfoValidationResult.UsernameEmpty)
{
error = GetErrorString(result, Environment.NewLine);
if (string.IsNullOrEmpty(error))
{
error = "Invalid input.";
}
return Tuple.Create(false, error);
}
StringBuilder sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
sb.Append("://");
bool containsUserInfo = false;
if (!string.IsNullOrEmpty(_username))
{
sb.Append(HttpUtility.UrlEncode(_username));
containsUserInfo = true;
}
if (!string.IsNullOrEmpty(_identityFile))
{
sb.Append(";");
if (!string.IsNullOrEmpty(_identityFile))
{
sb.Append($"{IdentityFileOptionName}={HttpUtility.UrlEncode(_identityFile)}");
}
containsUserInfo = true;
}
if (containsUserInfo)
sb.Append("@");
sb.Append(_host);
if (_sshPort != SshProfile.DefaultSshPort)
{
sb.Append($":{_sshPort:#####}");
}
sb.Append("/");
bool queryStringAdded = false;
if (_useMosh)
{
sb.Append(
$"?{ValidMoshPortsNames[0]}={_moshPortFrom:#####}-{_moshPortTo:#####}");
queryStringAdded = true;
}
var terminalInfoQueryString = GetBaseQueryString();
if (!string.IsNullOrEmpty(terminalInfoQueryString))
{
sb.Append($"{(queryStringAdded ? "&" : "?")}{terminalInfoQueryString}");
}
return Tuple.Create(true, sb.ToString());
}
#endregion Links/shortcuts related
}
}
@@ -1,16 +0,0 @@
using System;
namespace FluentTerminal.App.ViewModels.Profiles
{
[Flags]
public enum SshConnectionInfoValidationResult
{
Valid = 0,
UsernameEmpty = 1,
HostEmpty = 2,
SshPortZeroOrNegative = 4,
MoshPortZeroOrNegative = 8,
MoshPortRangeInvalid = 16,
IdentityFileDoesNotExist = 32
}
}
@@ -9,12 +9,14 @@ namespace FluentTerminal.App.ViewModels.Settings
public class AboutPageViewModel : ViewModelBase
{
private const string BaseUrl = "https://github.com/felixse/FluentTerminal/releases/tag/";
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService;
private string _latestVersion;
private readonly IApplicationView _applicationView;
public AboutPageViewModel(IUpdateService updateService, IApplicationView applicationView)
public AboutPageViewModel(ISettingsService settingsService, IUpdateService updateService, IApplicationView applicationView)
{
_settingsService = settingsService;
_updateService = updateService;
_applicationView = applicationView;
@@ -1,11 +1,8 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
@@ -18,48 +15,23 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly ISettingsService _settingsService;
private readonly IStartupTaskService _startupTaskService;
private bool _canEnableStartupTask;
private bool _editingNewTerminalLocation;
private bool _editingTabsPosition;
private bool _editingInactiveTabColorMode;
private bool _startupTaskEnabled;
private bool _shouldRestartForTrayMessage;
private string _startupTaskErrorMessage;
private bool _needsToRestart;
private readonly IApplicationLanguageService _applicationLanguageService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IFileSystemService _fileSystemService;
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService)
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IStartupTaskService startupTaskService)
{
_settingsService = settingsService;
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_startupTaskService = startupTaskService;
_applicationLanguageService = applicationLanguageService;
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
_applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectory);
}
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
public bool NeedsToRestart
{
get => _needsToRestart;
set => Set(ref _needsToRestart, value);
}
public string SelectedLanguage
{
get => _applicationLanguageService.GetCurrentLanguage();
set
{
_applicationLanguageService.SetLanguage(value);
NeedsToRestart = true;
}
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
}
public async Task OnNavigatedTo()
@@ -68,6 +40,34 @@ namespace FluentTerminal.App.ViewModels.Settings
SetStartupTaskPropertiesForStatus(startupTaskStatus);
}
public bool AlwaysShowTabs
{
get => _applicationSettings.AlwaysShowTabs;
set
{
if (_applicationSettings.AlwaysShowTabs != value)
{
_applicationSettings.AlwaysShowTabs = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool AlwaysUseWinPty
{
get => _applicationSettings.AlwaysUseWinPty;
set
{
if (_applicationSettings.AlwaysUseWinPty != value)
{
_applicationSettings.AlwaysUseWinPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool ShowCustomTitleInTitlebar
{
get => _applicationSettings.ShowCustomTitleInTitlebar;
@@ -119,109 +119,10 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool UseMoshByDefault
{
get => _applicationSettings.UseMoshByDefault;
set
{
if (_applicationSettings.UseMoshByDefault != value)
{
_applicationSettings.UseMoshByDefault = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool AutoFallbackToWindowsUsernameInLinks
{
get => _applicationSettings.AutoFallbackToWindowsUsernameInLinks;
set
{
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks != value)
{
_applicationSettings.AutoFallbackToWindowsUsernameInLinks = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool RTrimCopiedLines
{
get => _applicationSettings.RTrimCopiedLines;
set
{
if (_applicationSettings.RTrimCopiedLines != value)
{
_applicationSettings.RTrimCopiedLines = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool MuteTerminalBeeps
{
get => _applicationSettings.MuteTerminalBeeps;
set
{
if (_applicationSettings.MuteTerminalBeeps != value)
{
_applicationSettings.MuteTerminalBeeps = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
_trayProcessCommunicationService.MuteTerminal(value);
}
}
}
public bool EnableLogging
{
get => _applicationSettings.EnableLogging;
set
{
if (_applicationSettings.EnableLogging != value)
{
_applicationSettings.EnableLogging = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool PrintableOutputOnly
{
get => _applicationSettings.PrintableOutputOnly;
set
{
if (_applicationSettings.PrintableOutputOnly != value)
{
_applicationSettings.PrintableOutputOnly = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public string LogDirectoryPath
{
get => _applicationSettings.LogDirectoryPath;
set
{
if (_applicationSettings.LogDirectoryPath != value)
{
_applicationSettings.LogDirectoryPath = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool BottomIsSelected
{
get => TabsPosition == TabsPosition.Bottom;
set { if (value) TabsPosition = TabsPosition.Bottom; }
set => TabsPosition = TabsPosition.Bottom;
}
public bool CanEnableStartupTask
@@ -263,18 +164,20 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.NewTerminalLocation;
set
{
if (_applicationSettings.NewTerminalLocation != value)
if (_applicationSettings.NewTerminalLocation != value && !_editingNewTerminalLocation)
{
_editingNewTerminalLocation = true;
_applicationSettings.NewTerminalLocation = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(WindowIsSelected));
RaisePropertyChanged(nameof(TabIsSelected));
_editingNewTerminalLocation = false;
}
}
}
public IAsyncCommand RestoreDefaultsCommand { get; }
public IAsyncCommand BrowseLogDirectoryCommand { get; }
public RelayCommand RestoreDefaultsCommand { get; }
public bool StartupTaskEnabled
{
@@ -299,7 +202,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool TabIsSelected
{
get => NewTerminalLocation == NewTerminalLocation.Tab;
set { if (value) NewTerminalLocation = NewTerminalLocation.Tab; }
set => NewTerminalLocation = NewTerminalLocation.Tab;
}
public TabsPosition TabsPosition
@@ -307,11 +210,15 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.TabsPosition;
set
{
if (_applicationSettings.TabsPosition != value)
if (_applicationSettings.TabsPosition != value && !_editingTabsPosition)
{
_editingTabsPosition = true;
_applicationSettings.TabsPosition = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(TopIsSelected));
RaisePropertyChanged(nameof(BottomIsSelected));
_editingTabsPosition = false;
}
}
}
@@ -319,7 +226,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool TopIsSelected
{
get => TabsPosition == TabsPosition.Top;
set { if (value) TabsPosition = TabsPosition.Top; }
set => TabsPosition = TabsPosition.Top;
}
public bool UnderlineSelectedTab
@@ -339,13 +246,13 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool BackgroundIsSelected
{
get => InactiveTabColorMode == InactiveTabColorMode.Background;
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Background; }
set => InactiveTabColorMode = InactiveTabColorMode.Background;
}
public bool UnderlinedIsSelected
{
get => InactiveTabColorMode == InactiveTabColorMode.Underlined;
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Underlined; }
set => InactiveTabColorMode = InactiveTabColorMode.Underlined;
}
public InactiveTabColorMode InactiveTabColorMode
@@ -353,11 +260,15 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.InactiveTabColorMode;
set
{
if (_applicationSettings.InactiveTabColorMode != value)
if (_applicationSettings.InactiveTabColorMode != value && !_editingInactiveTabColorMode)
{
_editingInactiveTabColorMode = true;
_applicationSettings.InactiveTabColorMode = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(BackgroundIsSelected));
RaisePropertyChanged(nameof(UnderlinedIsSelected));
_editingInactiveTabColorMode = false;
}
}
}
@@ -365,12 +276,12 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool WindowIsSelected
{
get => NewTerminalLocation == NewTerminalLocation.Window;
set { if (value) NewTerminalLocation = NewTerminalLocation.Window; }
set => NewTerminalLocation = NewTerminalLocation.Window;
}
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the general settings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -380,16 +291,11 @@ namespace FluentTerminal.App.ViewModels.Settings
UnderlineSelectedTab = defaults.UnderlineSelectedTab;
InactiveTabColorMode = defaults.InactiveTabColorMode;
NewTerminalLocation = defaults.NewTerminalLocation;
AlwaysShowTabs = defaults.AlwaysShowTabs;
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon;
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
UseMoshByDefault = defaults.UseMoshByDefault;
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
RTrimCopiedLines = defaults.RTrimCopiedLines;
MuteTerminalBeeps = defaults.MuteTerminalBeeps;
EnableLogging = defaults.EnableLogging;
PrintableOutputOnly = defaults.PrintableOutputOnly;
LogDirectoryPath = defaults.LogDirectoryPath;
}
}
@@ -411,13 +317,13 @@ namespace FluentTerminal.App.ViewModels.Settings
case StartupTaskStatus.DisabledByUser:
StartupTaskEnabled = false;
StartupTaskErrorMessage = I18N.Translate("DisabledByUser");
StartupTaskErrorMessage = "Disabled by user. Please reactivate it in the Startup tab of the Task Manager.";
CanEnableStartupTask = false;
break;
case StartupTaskStatus.DisabledByPolicy:
StartupTaskEnabled = false;
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy");
StartupTaskErrorMessage = "Disabled by policy.";
CanEnableStartupTask = false;
break;
}
@@ -432,20 +338,10 @@ namespace FluentTerminal.App.ViewModels.Settings
}
else
{
await _startupTaskService.DisableStartupTask();
_startupTaskService.DisableStartupTask();
status = await _startupTaskService.GetStatus();
}
SetStartupTaskPropertiesForStatus(status);
}
private async Task BrowseLogDirectory()
{
var folder = await _fileSystemService.BrowseForDirectory();
if (folder != null)
{
LogDirectoryPath = folder;
}
}
}
}
@@ -1,5 +1,4 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
@@ -119,7 +118,7 @@ namespace FluentTerminal.App.ViewModels.Settings
private async Task Delete()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this keybinding?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -14,14 +14,17 @@ namespace FluentTerminal.App.ViewModels.Settings
{
public class KeyBindingsPageViewModel : ViewModelBase
{
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private IDictionary<string, ICollection<KeyBinding>> _keyBindings;
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, ITrayProcessCommunicationService trayProcessCommunicationService)
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, ITrayProcessCommunicationService trayProcessCommunicationService)
{
_settingsService = settingsService;
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_trayProcessCommunicationService = trayProcessCommunicationService;
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
AddCommand = new RelayCommand<string>(async command => await Add(command).ConfigureAwait(false));
@@ -51,13 +54,15 @@ namespace FluentTerminal.App.ViewModels.Settings
private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings)
{
_keyBindings = keyBindings;
ClearKeyBindings();
foreach (var value in Enum.GetValues(typeof(Command)))
{
var command = (Command)value;
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, I18N.Translate($"{nameof(Command)}.{command}"), true);
foreach (var keyBinding in keyBindings[command.ToString()])
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, EnumHelper.GetEnumDescription(command), true);
foreach (var keyBinding in _keyBindings[command.ToString()])
{
viewModel.Add(keyBinding);
}
@@ -74,7 +79,7 @@ namespace FluentTerminal.App.ViewModels.Settings
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the default keybindings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -1,9 +1,11 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
@@ -14,6 +16,8 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly IDialogService _dialogService;
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly ApplicationSettings _applicationSettings;
private bool _editingMouseRightClickAction;
private bool _editingMouseMiddleClickAction;
public MousePageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider)
{
@@ -44,11 +48,16 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.MouseRightClickAction;
set
{
if (_applicationSettings.MouseRightClickAction != value)
if (_applicationSettings.MouseRightClickAction != value && !_editingMouseRightClickAction)
{
_editingMouseRightClickAction = true;
_applicationSettings.MouseRightClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(MouseRightClickNoneIsSelected));
RaisePropertyChanged(nameof(MouseRightClickContextMenuIsSelected));
RaisePropertyChanged(nameof(MouseRightClickPasteIsSelected));
_editingMouseRightClickAction = false;
}
}
}
@@ -56,19 +65,19 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool MouseRightClickNoneIsSelected
{
get => MouseRightClickAction == MouseAction.None;
set { if (value) MouseRightClickAction = MouseAction.None; }
set => MouseRightClickAction = MouseAction.None;
}
public bool MouseRightClickContextMenuIsSelected
{
get => MouseRightClickAction == MouseAction.ContextMenu;
set { if (value) MouseRightClickAction = MouseAction.ContextMenu; }
set => MouseRightClickAction = MouseAction.ContextMenu;
}
public bool MouseRightClickPasteIsSelected
{
get => MouseRightClickAction == MouseAction.Paste;
set { if (value) MouseRightClickAction = MouseAction.Paste; }
set => MouseRightClickAction = MouseAction.Paste;
}
public MouseAction MouseMiddleClickAction
@@ -76,11 +85,16 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.MouseMiddleClickAction;
set
{
if (_applicationSettings.MouseMiddleClickAction != value)
if (_applicationSettings.MouseMiddleClickAction != value && !_editingMouseMiddleClickAction)
{
_editingMouseMiddleClickAction = true;
_applicationSettings.MouseMiddleClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(MouseMiddleClickNoneIsSelected));
RaisePropertyChanged(nameof(MouseMiddleClickContextMenuIsSelected));
RaisePropertyChanged(nameof(MouseMiddleClickPasteIsSelected));
_editingMouseMiddleClickAction = false;
}
}
}
@@ -88,26 +102,26 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool MouseMiddleClickNoneIsSelected
{
get => MouseMiddleClickAction == MouseAction.None;
set { if (value) MouseMiddleClickAction = MouseAction.None; }
set => MouseMiddleClickAction = MouseAction.None;
}
public bool MouseMiddleClickContextMenuIsSelected
{
get => MouseMiddleClickAction == MouseAction.ContextMenu;
set { if (value) MouseMiddleClickAction = MouseAction.ContextMenu; }
set => MouseMiddleClickAction = MouseAction.ContextMenu;
}
public bool MouseMiddleClickPasteIsSelected
{
get => MouseMiddleClickAction == MouseAction.Paste;
set { if (value) MouseMiddleClickAction = MouseAction.Paste; }
set => MouseMiddleClickAction = MouseAction.Paste;
}
public RelayCommand RestoreDefaultsCommand { get; }
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the mouse settings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -98,18 +98,20 @@ namespace FluentTerminal.App.ViewModels.Settings
private void Clone(ShellProfileViewModel shellProfile)
{
var cloned = shellProfile.ProfileVm.Model.Clone();
cloned.Id = Guid.NewGuid();
cloned.PreInstalled = false;
cloned.Name = $"Copy of {shellProfile.Name}";
cloned.KeyBindings = new List<KeyBinding>();
var cloned = new ShellProfile(shellProfile.Model)
{
Id = Guid.NewGuid(),
PreInstalled = false,
Name = $"Copy of {shellProfile.Name}"
};
AddShellProfile(cloned);
}
private void AddShellProfile(ShellProfile shellProfile)
{
_settingsService.SaveShellProfile(shellProfile, true);
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService, _applicationView, _defaultValueProvider, true);
viewModel.EditCommand.Execute(null);
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
@@ -1,119 +0,0 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
namespace FluentTerminal.App.ViewModels.Settings
{
public class SshProfilesPageViewModel: ViewModelBase
{
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private SshProfileViewModel _selectedShellProfile;
private readonly IApplicationView _applicationView;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IApplicationDataContainer _historyContainer;
public SshProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService,
IFileSystemService fileSystemService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService,
IApplicationDataContainer historyContainer)
{
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_trayProcessCommunicationService = trayProcessCommunicationService;
_historyContainer = historyContainer;
CreateSshProfileCommand = new RelayCommand(CreateSshProfile);
CloneCommand = new RelayCommand<SshProfileViewModel>(Clone);
foreach (var sshProfile in _settingsService.GetSshProfiles())
{
var viewModel = new SshProfileViewModel(sshProfile, settingsService, dialogService, fileSystemService,
applicationView, _trayProcessCommunicationService, historyContainer, false);
viewModel.Deleted += OnSshProfileDeleted;
SshProfiles.Add(viewModel);
}
if (SshProfiles.Count == 0)
{
CreateSshProfile();
}
SelectedSshProfile = SshProfiles.First();
}
private void OnSshProfileDeleted(object sender, EventArgs e)
{
if (sender is SshProfileViewModel shellProfile)
{
if (SelectedSshProfile == shellProfile)
{
SelectedSshProfile = SshProfiles.First();
}
SshProfiles.Remove(shellProfile);
_settingsService.DeleteSshProfile(shellProfile.Id);
}
}
public RelayCommand CreateSshProfileCommand { get; }
public RelayCommand<SshProfileViewModel> CloneCommand { get; }
public void CreateSshProfile()
{
var shellProfile = new SshProfile
{
Id = Guid.NewGuid(),
PreInstalled = false,
Name = "New SSH profile",
KeyBindings = new List<KeyBinding>(),
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault
};
AddSshProfile(shellProfile);
}
private void Clone(SshProfileViewModel shellProfile)
{
var cloned = (SshProfile) shellProfile.ProfileVm.Model.Clone();
cloned.Id = Guid.NewGuid();
cloned.PreInstalled = false;
cloned.Name = $"Copy of {shellProfile.Name}";
cloned.KeyBindings = new List<KeyBinding>();
foreach (KeyBinding keyBinding in cloned.KeyBindings)
{
keyBinding.Command = cloned.Id.ToString();
}
AddSshProfile(cloned);
}
private void AddSshProfile(SshProfile sshProfile)
{
var viewModel = new SshProfileViewModel(sshProfile, _settingsService, _dialogService, _fileSystemService,
_applicationView, _trayProcessCommunicationService, _historyContainer, true);
viewModel.EditCommand.Execute(null);
viewModel.Deleted += OnSshProfileDeleted;
SshProfiles.Add(viewModel);
SelectedSshProfile = viewModel;
}
public SshProfileViewModel SelectedSshProfile
{
get => _selectedShellProfile;
set => Set(ref _selectedShellProfile, value);
}
public ObservableCollection<SshProfileViewModel> SshProfiles { get; } = new ObservableCollection<SshProfileViewModel>();
}
}
@@ -1,6 +1,4 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
@@ -17,41 +15,43 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly ISettingsService _settingsService;
private readonly IDialogService _dialogService;
private readonly IDefaultValueProvider _defaultValueProvider;
private bool _isEditingCursorStyle;
private bool _isEditingScrollBarStyle;
public bool BarIsSelected
{
get => CursorStyle == CursorStyle.Bar;
set { if (value) CursorStyle = CursorStyle.Bar; }
set => CursorStyle = CursorStyle.Bar;
}
public bool BlockIsSelected
{
get => CursorStyle == CursorStyle.Block;
set { if (value) CursorStyle = CursorStyle.Block; }
set => CursorStyle = CursorStyle.Block;
}
public bool UnderlineIsSelected
{
get => CursorStyle == CursorStyle.Underline;
set { if (value) CursorStyle = CursorStyle.Underline; }
set => CursorStyle = CursorStyle.Underline;
}
public bool HiddenIsSelected
{
get => ScrollBarStyle == ScrollBarStyle.Hidden;
set { if (value) ScrollBarStyle = ScrollBarStyle.Hidden; }
set => ScrollBarStyle = ScrollBarStyle.Hidden;
}
public bool AutoHidingIsSelected
{
get => ScrollBarStyle == ScrollBarStyle.AutoHiding;
set { if (value) ScrollBarStyle = ScrollBarStyle.AutoHiding; }
set => ScrollBarStyle = ScrollBarStyle.AutoHiding;
}
public bool VisibleIsSelected
{
get => ScrollBarStyle == ScrollBarStyle.Visible;
set { if (value) ScrollBarStyle = ScrollBarStyle.Visible; }
set => ScrollBarStyle = ScrollBarStyle.Visible;
}
public bool CursorBlink
@@ -115,6 +115,7 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.ScrollBackLimit.ToString();
set
{
var oldValue = _terminalOptions.ScrollBackLimit;
if (uint.TryParse(value, out uint intValue))
{
if (intValue == uint.MinValue)
@@ -135,21 +136,6 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public string WordSeparator
{
get => WebViewSpecialCharEncoder.Decode(_terminalOptions.WordSeparator);
set
{
value = WebViewSpecialCharEncoder.Encode(value);
if (_terminalOptions.WordSeparator != value)
{
_terminalOptions.WordSeparator = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public bool BoldText
{
get => _terminalOptions.BoldText;
@@ -164,20 +150,6 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool ShowTextCopied
{
get => _terminalOptions.ShowTextCopied;
set
{
if (_terminalOptions.ShowTextCopied != value)
{
_terminalOptions.ShowTextCopied = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public IEnumerable<FontInfo> Fonts { get; }
public int FontSize
@@ -203,11 +175,16 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.CursorStyle;
set
{
if (_terminalOptions.CursorStyle != value)
if (_terminalOptions.CursorStyle != value && !_isEditingCursorStyle)
{
_isEditingCursorStyle = true;
_terminalOptions.CursorStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
RaisePropertyChanged(nameof(BlockIsSelected));
RaisePropertyChanged(nameof(BarIsSelected));
RaisePropertyChanged(nameof(UnderlineIsSelected));
_isEditingCursorStyle = false;
}
}
}
@@ -217,18 +194,23 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.ScrollBarStyle;
set
{
if (_terminalOptions.ScrollBarStyle != value)
if (_terminalOptions.ScrollBarStyle != value && !_isEditingScrollBarStyle)
{
_isEditingScrollBarStyle = true;
_terminalOptions.ScrollBarStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
RaisePropertyChanged(nameof(HiddenIsSelected));
RaisePropertyChanged(nameof(AutoHidingIsSelected));
RaisePropertyChanged(nameof(VisibleIsSelected));
_isEditingScrollBarStyle = false;
}
}
}
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the default terminal options?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -241,8 +223,6 @@ namespace FluentTerminal.App.ViewModels.Settings
BoldText = defaults.BoldText;
BackgroundOpacity = defaults.BackgroundOpacity;
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
ShowTextCopied = defaults.ShowTextCopied;
WordSeparator = defaults.WordSeparator;
}
}
@@ -1,14 +1,10 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.Models.Messages;
namespace FluentTerminal.App.ViewModels.Settings
{
@@ -34,10 +30,10 @@ namespace FluentTerminal.App.ViewModels.Settings
_fileSystemService = fileSystemService;
CreateThemeCommand = new RelayCommand(CreateTheme);
ImportThemeCommand = new AsyncCommand(ImportTheme);
ImportThemeCommand = new RelayCommand(ImportTheme);
CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
BackgroundOpacity = _settingsService.GetTerminalOptions().BackgroundOpacity;
@@ -59,7 +55,7 @@ namespace FluentTerminal.App.ViewModels.Settings
}
public RelayCommand CreateThemeCommand { get; }
public IAsyncCommand ImportThemeCommand { get; }
public RelayCommand ImportThemeCommand { get; }
public RelayCommand<ThemeViewModel> CloneCommand { get; set; }
public double BackgroundOpacity
@@ -113,7 +109,7 @@ namespace FluentTerminal.App.ViewModels.Settings
AddTheme(theme);
}
private async Task ImportTheme()
private async void ImportTheme()
{
var file = await _fileSystemService.OpenFile(_themeParserFactory.SupportedFileTypes).ConfigureAwait(true);
if (file != null)
@@ -122,7 +118,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (parser == null)
{
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
await _dialogService.ShowMessageDialogAsnyc("Import theme failed", "No suitable parser found", DialogButton.OK).ConfigureAwait(false);
return;
}
@@ -134,7 +130,7 @@ namespace FluentTerminal.App.ViewModels.Settings
}
catch (Exception exception)
{
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK).ConfigureAwait(false);
await _dialogService.ShowMessageDialogAsnyc("Import theme failed", exception.Message, DialogButton.OK).ConfigureAwait(false);
}
}
}
@@ -183,9 +179,9 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
BackgroundOpacity = message.TerminalOptions.BackgroundOpacity;
BackgroundOpacity = e.BackgroundOpacity;
}
private void OnSelectedThemeBackgroundChanged(object sender, string e)
@@ -9,28 +9,32 @@ namespace FluentTerminal.App.ViewModels
{
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IApplicationView applicationView,
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers)
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView applicationView)
{
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, trayProcessCommunicationService);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService, fileSystemService);
Profiles = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
About = new AboutPageViewModel(settingsService, updateService, applicationView);
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, defaultValueProvider, trayProcessCommunicationService);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService);
Shell = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService);
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService);
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService,
applicationView, trayProcessCommunicationService, containers.HistoryContainer);
}
public event EventHandler Closed;
public event EventHandler AboutPageRequested;
public GeneralPageViewModel General { get; }
public KeyBindingsPageViewModel KeyBindings { get; }
public ProfilesPageViewModel Profiles { get; }
public ProfilesPageViewModel Shell { get; }
public TerminalPageViewModel Terminal { get; }
public ThemesPageViewModel Themes { get; }
public MousePageViewModel Mouse { get; }
public SshProfilesPageViewModel SshProfiles { get; }
public AboutPageViewModel About { get; }
public void NavigateToAboutPage()
{
AboutPageRequested?.Invoke(this, EventArgs.Empty);
}
public void Close()
{
@@ -1,29 +1,152 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.App.ViewModels.Profiles;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Extends <see cref="ProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
/// </summary>
public class ShellProfileViewModel : ProfileViewModelBase<CommonProfileProviderViewModel>
public class ShellProfileViewModel : ViewModelBase
{
#region Fields
private readonly IDialogService _dialogService;
private readonly IFileSystemService _fileSystemService;
private readonly ISettingsService _settingsService;
private string _arguments;
private ShellProfile _fallbackProfile;
private bool _inEditMode;
private bool _isDefault;
private string _location;
private LineEndingStyle _lineEndingStyle;
private string _name;
private bool _isNew;
private TabTheme _selectedTabTheme;
private TerminalTheme _selectedTerminalTheme;
private string _workingDirectory;
private readonly IApplicationView _applicationView;
private readonly IDefaultValueProvider _defaultValueProvider;
#endregion Fields
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider, Boolean isNew)
{
Model = shellProfile;
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_defaultValueProvider = defaultValueProvider;
_isNew = isNew;
#region Properties
_settingsService.ThemeAdded += OnThemeAdded;
_settingsService.ThemeDeleted += OnThemeDeleted;
private bool _isDefault;
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
TerminalThemes = new ObservableCollection<TerminalTheme>();
TerminalThemes.Add(new TerminalTheme
{
Id = Guid.Empty,
Name = "Default"
});
foreach (var theme in _settingsService.GetThemes())
{
TerminalThemes.Add(theme);
}
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), _dialogService, string.Empty, false);
InitializeViewModelProperties(shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
SaveChangesCommand = new RelayCommand(SaveChanges);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
}
private void InitializeViewModelProperties(ShellProfile shellProfile)
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == shellProfile.TerminalThemeId);
Id = shellProfile.Id;
Name = shellProfile.Name;
Arguments = shellProfile.Arguments;
Location = shellProfile.Location;
WorkingDirectory = shellProfile.WorkingDirectory;
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == shellProfile.TabThemeId);
PreInstalled = shellProfile.PreInstalled;
LineEndingStyle = shellProfile.LineEndingTranslation;
KeyBindings.Clear();
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
}
private void OnThemeAdded(object sender, TerminalTheme e)
{
_applicationView.RunOnDispatcherThread(() =>
{
TerminalThemes.Add(e);
});
}
private void OnThemeDeleted(object sender, Guid e)
{
if (SelectedTerminalTheme.Id == e)
{
_applicationView.RunOnDispatcherThread(() =>
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(x => x.Id == Guid.Empty);
Model.TerminalThemeId = Guid.Empty;
if (_fallbackProfile != null)
{
_fallbackProfile.TerminalThemeId = Guid.Empty;
}
});
}
}
public event EventHandler Deleted;
public event EventHandler SetAsDefault;
public IAsyncCommand BrowseForCustomShellCommand { get; }
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
public IAsyncCommand CancelEditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public RelayCommand SaveChangesCommand { get; }
public RelayCommand SetDefaultCommand { get; }
public IAsyncCommand RestoreDefaultsCommand { get; }
public IAsyncCommand AddKeyboardShortcutCommand { get; }
public ObservableCollection<TabTheme> TabThemes { get; }
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ShellProfile Model { get; private set; }
public bool PreInstalled { get; private set; }
public string Arguments
{
get => _arguments;
set => Set(ref _arguments, value);
}
public Guid Id { get; private set; }
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
}
public bool IsDefault
{
@@ -31,79 +154,223 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _isDefault, value);
}
#endregion Properties
#region Events
public event EventHandler SetAsDefault;
#endregion Events
#region Commands
public RelayCommand SetDefaultCommand { get; }
public IAsyncCommand RestoreDefaultsCommand { get; }
#endregion Commands
#region Constrcutor
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService,
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
IDefaultValueProvider defaultValueProvider, bool isNew) : base(shellProfile, settingsService, dialogService,
isNew)
public string Location
{
_defaultValueProvider = defaultValueProvider;
ProfileVm = new CommonProfileProviderViewModel(settingsService, applicationView, fileSystemService,
shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
get => _location;
set => Set(ref _location, value);
}
#endregion Constrcutor
public string Name
{
get => _name;
set => Set(ref _name, value);
}
#region Methods
public TabTheme SelectedTabTheme
{
get => _selectedTabTheme;
set
{
if (value != null)
{
Set(ref _selectedTabTheme, value);
}
}
}
public TerminalTheme SelectedTerminalTheme
{
get => _selectedTerminalTheme;
set
{
if (value != null)
{
Set(ref _selectedTerminalTheme, value);
}
}
}
public string WorkingDirectory
{
get => _workingDirectory;
set => Set(ref _workingDirectory, value);
}
public LineEndingStyle LineEndingStyle
{
get => _lineEndingStyle;
set => Set(ref _lineEndingStyle, value);
}
public bool DoNotModifyIsSelected
{
get => LineEndingStyle == LineEndingStyle.DoNotModify;
set { if (value) LineEndingStyle = LineEndingStyle.DoNotModify; }
}
public bool ToCRLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCRLF;
set { if (value) LineEndingStyle = LineEndingStyle.ToCRLF; }
}
public bool ToLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToLF;
set { if (value) LineEndingStyle = LineEndingStyle.ToLF; }
}
public bool ToCRIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCR;
set { if (value) LineEndingStyle = LineEndingStyle.ToCR; }
}
public void SaveChanges()
{
Model.Arguments = Arguments;
Model.Location = Location;
Model.Name = Name;
Model.WorkingDirectory = WorkingDirectory;
Model.TabThemeId = SelectedTabTheme.Id;
Model.TerminalThemeId = SelectedTerminalTheme.Id;
Model.LineEndingTranslation = _lineEndingStyle;
Model.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
_settingsService.SaveShellProfile(Model);
KeyBindings.Editable = false;
InEditMode = false;
_isNew = false;
}
private async Task RestoreDefaults()
{
if (InEditMode || !ProfileVm.PreInstalled)
if (InEditMode || !PreInstalled)
{
throw new InvalidOperationException();
}
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore this profile?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
var profile = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id.Equals(Id));
if (profile == null)
{
// Should not happen ever, but...
return;
}
ProfileVm.Model = profile;
Initialize(profile);
SettingsService.SaveShellProfile(profile);
Model = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id == Model.Id);
InitializeViewModelProperties(Model);
_settingsService.SaveShellProfile(Model);
}
}
protected override bool CanDelete()
public Task AddKeyboardShortcut()
{
return !ProfileVm.PreInstalled;
return KeyBindings.ShowAddKeyBindingDialog();
}
private async Task BrowseForCustomShell()
{
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
if (file != null)
{
Location = file.Path;
}
}
private async Task BrowseForWorkingDirectory()
{
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
if (directory != null)
{
WorkingDirectory = directory;
}
}
private async Task CancelEdit()
{
if (_isNew)
{
await Delete();
}
else
{
ShellProfile changedProfile = new ShellProfile
{
Arguments = Arguments,
Location = Location,
Name = Name,
WorkingDirectory = WorkingDirectory,
TabThemeId = SelectedTabTheme.Id,
TerminalThemeId = SelectedTerminalTheme.Id,
LineEndingTranslation = _lineEndingStyle,
KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList()
};
if (!_fallbackProfile.Equals(changedProfile))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Arguments = _fallbackProfile.Arguments;
Location = _fallbackProfile.Location;
Name = _fallbackProfile.Name;
WorkingDirectory = _fallbackProfile.WorkingDirectory;
LineEndingStyle = _fallbackProfile.LineEndingTranslation;
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TerminalThemeId);
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TabThemeId);
KeyBindings.KeyBindings.Clear();
foreach (var keyBinding in Model.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
}
}
private bool CanDelete()
{
return !Model.PreInstalled;
}
private async Task Delete()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this theme?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Deleted?.Invoke(this, EventArgs.Empty);
}
}
private void Edit()
{
// todo write copy ctor
_fallbackProfile = new ShellProfile
{
Arguments = Model.Arguments,
LineEndingTranslation = Model.LineEndingTranslation,
Location = Model.Location,
Name = Model.Name,
WorkingDirectory = Model.WorkingDirectory,
TabThemeId = Model.TabThemeId,
TerminalThemeId = Model.TerminalThemeId
};
KeyBindings.Editable = true;
InEditMode = true;
}
private void SetDefault()
{
SetAsDefault?.Invoke(this, EventArgs.Empty);
}
#endregion Methods
}
}
@@ -1,249 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class ProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
{
#region Fields
protected readonly IDialogService DialogService;
protected readonly ISettingsService SettingsService;
private List<KeyBinding> _bindingsBeforeEdit;
private Dictionary<string, string> _environmentVariablesBeforeEdit;
private string _nameBeforeEdit;
#endregion Fields
#region Properties
protected bool IsNew { get; private set; }
public Guid Id { get; private set; }
private bool _inEditMode;
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
}
private string _name;
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<EnvironmentVariableViewModel> EnvironmentVariables { get; } = new ObservableCollection<EnvironmentVariableViewModel>();
public T ProfileVm { get; protected set; }
#endregion Properties
#region Events
public event EventHandler Deleted;
#endregion Events
#region Commands
public IAsyncCommand CancelEditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public IAsyncCommand SaveChangesCommand { get; }
public IAsyncCommand AddKeyboardShortcutCommand { get; }
#endregion Commands
#region Constructor
protected ProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
{
SettingsService = settingsService;
DialogService = dialogService;
IsNew = isNew;
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), dialogService, string.Empty, false);
foreach (var environmentVariable in shellProfile.EnvironmentVariables)
{
EnvironmentVariables.Add(new EnvironmentVariableViewModel
{
Name = environmentVariable.Key,
Value = environmentVariable.Value
});
}
Initialize(shellProfile);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
}
#endregion Constructor
#region Methods
// Loads view model properties from the input shellProfile
protected void Initialize(ShellProfile shellProfile)
{
Id = shellProfile.Id;
Name = shellProfile.Name;
KeyBindings.Clear();
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
}
protected virtual bool HasChanges()
{
return ProfileVm.HasChanges()
|| !_nameBeforeEdit.NullableEqualTo(_name)
|| !(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ?? false)
|| !(_environmentVariablesBeforeEdit?.SequenceEqual(EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value)) ?? false);
}
private async Task Delete()
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Deleted?.Invoke(this, EventArgs.Empty);
}
}
protected abstract bool CanDelete();
private void Edit()
{
if (InEditMode)
{
return;
}
_bindingsBeforeEdit = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
_environmentVariablesBeforeEdit = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
_nameBeforeEdit = _name;
KeyBindings.Editable = true;
InEditMode = true;
}
private async Task CancelEdit()
{
if (IsNew)
{
await Delete();
}
else
{
if (HasChanges())
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK)
{
// Cancelled, so rollback
ProfileVm.RejectChanges();
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
}
}
public virtual async Task SaveChangesAsync()
{
var error = await ProfileVm.AcceptChangesAsync();
if (!string.IsNullOrEmpty(error))
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), error, DialogButton.OK);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Any(x => string.IsNullOrWhiteSpace(x)))
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK);
return;
}
var profile = ProfileVm.Model;
profile.Id = Id;
profile.Name = _name;
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
profile.EnvironmentVariables = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
if (profile is SshProfile sshProfile)
{
SettingsService.SaveSshProfile(sshProfile, IsNew);
}
else
{
SettingsService.SaveShellProfile(profile, IsNew);
}
KeyBindings.Editable = false;
InEditMode = false;
IsNew = false;
}
public Task AddKeyboardShortcut()
{
return KeyBindings.ShowAddKeyBindingDialog();
}
#endregion Methods
}
}
@@ -0,0 +1,32 @@
using FluentTerminal.App.Services.Dialogs;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels
{
public class SshConnectionInfoViewModel : ViewModelBase, ISshConnectionInfo
{
private string _host = string.Empty;
public string Host
{
get => _host;
set => Set(ref _host, value);
}
private ushort _port = 22;
public ushort Port
{
get => _port;
set => Set(ref _port, value);
}
private string _username = string.Empty;
public string Username
{
get => _username;
set => Set(ref _username, value);
}
}
}
@@ -1,36 +0,0 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Extends <see cref="ProfileViewModelBase{T}"/>, and doesn't implement any additional logic because in
/// case of SSH profiles no additional logic is needed.
/// </summary>
public class SshProfileViewModel : ProfileViewModelBase<SshConnectViewModel>
{
#region Constructor
public SshProfileViewModel(SshProfile sshProfile, ISettingsService settingsService,
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService,
IApplicationDataContainer historyContainer, bool isNew) : base(sshProfile, settingsService, dialogService,
isNew)
{
ProfileVm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
fileSystemService, sshProfile);
}
#endregion Constructor
#region Methods
protected override bool CanDelete()
{
return true;
}
#endregion Methods
}
}
@@ -1,109 +1,42 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Newtonsoft.Json;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using FluentTerminal.Models.Messages;
namespace FluentTerminal.App.ViewModels
{
public class TerminalViewModel : ViewModelBase
{
#region Static
private static readonly Regex SshTitleRx = new Regex(@"^\w+@\S+:", RegexOptions.Compiled);
#endregion Static
#region Serialize terminal state
private class TerminalState
{
public bool HasCustomTitle { get; set; }
public string ShellTitle { get; set; }
public string TabTitle { get; set; }
public TerminalTheme TerminalTheme { get; set; }
public TabTheme TabTheme { get; set; }
public bool ShowSearchPanel { get; set; }
public string SearchText { get; set; }
public string XtermBufferState { get; set; }
public byte TerminalId { get; set; }
public ShellProfile ShellProfile { get; set; }
}
public async Task<string> Serialize()
{
TerminalState state = new TerminalState
{
HasCustomTitle = _hasCustomTitle,
ShellTitle = ShellTitle,
TabTitle = TabTitle,
TerminalTheme = TerminalTheme,
TabTheme = TabTheme,
ShowSearchPanel = ShowSearchPanel,
SearchText = SearchText,
XtermBufferState = await SerializeXtermState(),
TerminalId = Terminal.Id,
ShellProfile = ShellProfile
};
return JsonConvert.SerializeObject(state);
}
public void Restore(string data)
{
TerminalState state = JsonConvert.DeserializeObject<TerminalState>(data);
if (state != null)
{
_hasCustomTitle = state.HasCustomTitle;
ShellTitle = state.ShellTitle;
TabTitle = state.TabTitle;
TerminalTheme = state.TerminalTheme;
TabTheme = state.TabTheme;
ShowSearchPanel = state.ShowSearchPanel;
SearchText = state.SearchText;
XtermBufferState = state.XtermBufferState;
_terminalId = state.TerminalId;
ShellProfile = state.ShellProfile;
}
}
#endregion Serialize terminal state
private readonly IKeyboardCommandService _keyboardCommandService;
private readonly IDispatcherTimer _resizeOverlayTimer;
private bool _isSelected;
private bool _isHovered;
private bool _hasNewOutput;
private bool _hasExitedWithError;
private string _resizeOverlayContent;
private string _searchText;
private bool _showResizeOverlay;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private TerminalTheme _terminalTheme;
private TerminalOptions _terminalOptions;
private string _tabTitle;
private string _shellTitle;
private bool _hasCustomTitle;
private byte? _terminalId;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, string terminalState = null)
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
{
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
SettingsService = settingsService;
_terminalOptions = SettingsService.GetTerminalOptions();
SettingsService.CurrentThemeChanged += OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged += OnKeyBindingsChanged;
TrayProcessCommunicationService = trayProcessCommunicationService;
@@ -119,58 +52,35 @@ namespace FluentTerminal.App.ViewModels
TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
CloseCommand = new AsyncCommand(CloseTab, CanExecuteCommand);
CloseLeftTabsCommand = new RelayCommand(CloseLeftTabs, CanExecuteCommand);
CloseRightTabsCommand = new RelayCommand(CloseRightTabs, CanExecuteCommand);
CloseOtherTabsCommand = new RelayCommand(CloseOtherTabs, CanExecuteCommand);
FindNextCommand = new RelayCommand(FindNext, CanExecuteCommand);
FindPreviousCommand = new RelayCommand(FindPrevious, CanExecuteCommand);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel, CanExecuteCommand);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme, CanExecuteCommand);
EditTitleCommand = new AsyncCommand(EditTitle, CanExecuteCommand);
DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand);
_resizeOverlayTimer = dispatcherTimer;
_resizeOverlayTimer.Interval = new TimeSpan(0, 0, 2);
_resizeOverlayTimer.Tick += OnResizeOverlayTimerFinished;
if (!String.IsNullOrEmpty(terminalState))
{
Restore(terminalState);
}
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
FindNextCommand = new RelayCommand(FindNext);
FindPreviousCommand = new RelayCommand(FindPrevious);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
EditTitleCommand = new AsyncCommand(EditTitle);
Terminal = new Terminal(TrayProcessCommunicationService, _terminalId);
Terminal = new Terminal(TrayProcessCommunicationService);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
Terminal.OutputReceived += Terminal_OutputReceived;
Terminal.SizeChanged += Terminal_SizeChanged;
Terminal.TitleChanged += Terminal_TitleChanged;
Terminal.Exited += Terminal_Exited;
Terminal.Closed += Terminal_Closed;
Overlay = new OverlayViewModel(dispatcherTimer);
}
private bool _disposalRequested;
public void DisposalPrepare()
{
_disposalRequested = true;
CloseCommand = null;
TerminalView.DisposalPrepare();
TerminalView = null;
Terminal = null;
Overlay = null;
}
public event EventHandler Activated;
public event EventHandler Closed;
public event EventHandler<string> FindNextRequested;
public event EventHandler<string> FindPreviousRequested;
public event EventHandler KeyBindingsChanged;
public event EventHandler<TerminalOptions> OptionsChanged;
public event EventHandler SearchStarted;
public event EventHandler<TerminalTheme> ThemeChanged;
public event EventHandler<string> ShellTitleChanged;
public event EventHandler<string> CustomTitleChanged;
public event EventHandler CloseLeftTabsRequested;
public event EventHandler CloseRightTabsRequested;
public event EventHandler CloseOtherTabsRequested;
public event EventHandler DuplicateTabRequested;
public ApplicationSettings ApplicationSettings { get; private set; }
@@ -187,15 +97,7 @@ namespace FluentTerminal.App.ViewModels
public IClipboardService ClipboardService { get; }
public string XtermBufferState { get; private set; }
public AsyncCommand CloseCommand { get; private set; }
public RelayCommand CloseRightTabsCommand { get; }
public RelayCommand CloseLeftTabsCommand { get; }
public RelayCommand CloseOtherTabsCommand { get; }
public RelayCommand CloseCommand { get; }
public RelayCommand CloseSearchPanelCommand { get; }
@@ -207,8 +109,6 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand FindPreviousCommand { get; }
public RelayCommand DuplicateTabCommand { get; }
public bool IsSelected
{
get => _isSelected;
@@ -222,28 +122,10 @@ namespace FluentTerminal.App.ViewModels
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
RaisePropertyChanged(nameof(ShowCloseButton));
}
}
}
public bool IsHovered
{
get => _isHovered;
set
{
if (Set(ref _isHovered, value))
{
RaisePropertyChanged(nameof(ShowCloseButton));
}
}
}
public bool ShowCloseButton
{
get => IsHovered || IsSelected;
}
public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) ||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null);
@@ -253,16 +135,10 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _hasNewOutput, value);
}
public bool HasExitedWithError
public string ResizeOverlayContent
{
get => _hasExitedWithError;
set
{
if (Set(ref _hasExitedWithError, value) && value)
{
HasNewOutput = false;
}
}
get => _resizeOverlayContent;
set => Set(ref _resizeOverlayContent, value);
}
public string SearchText
@@ -275,7 +151,24 @@ namespace FluentTerminal.App.ViewModels
public ISettingsService SettingsService { get; }
public ShellProfile ShellProfile { get; private set; }
public ShellProfile ShellProfile { get; }
public bool ShowResizeOverlay
{
get => _showResizeOverlay;
set
{
Set(ref _showResizeOverlay, value);
if (value)
{
if (_resizeOverlayTimer.IsEnabled)
{
_resizeOverlayTimer.Stop();
}
_resizeOverlayTimer.Start();
}
}
}
public bool ShowSearchPanel
{
@@ -298,45 +191,20 @@ namespace FluentTerminal.App.ViewModels
public Terminal Terminal { get; private set; }
public OverlayViewModel Overlay { get; private set; }
public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}
public bool Initialized { get; set; }
public string TabTitle
{
get => _tabTitle;
set
{
var title = value?.Trim() ?? string.Empty;
if (ShellProfile is SshProfile)
if (Set(ref _tabTitle, value))
{
// For SshProfile we are adjusting title.
if (title.Equals("ssh", StringComparison.OrdinalIgnoreCase) ||
title.EndsWith("\\ssh.exe", StringComparison.OrdinalIgnoreCase))
{
title = $"[ssh] {I18N.Translate("Authenticate")}";
}
else if (title.Equals("mosh", StringComparison.OrdinalIgnoreCase) ||
title.EndsWith("\\mosh.exe", StringComparison.OrdinalIgnoreCase))
{
title = $"[mosh] {I18N.Translate("Authenticate")}";
}
else if (SshTitleRx.IsMatch(title))
{
title = $"[ssh] {title}";
}
}
if (Set(ref _tabTitle, title))
{
CustomTitleChanged?.Invoke(this, title);
CustomTitleChanged?.Invoke(this, value);
}
}
}
@@ -357,23 +225,21 @@ namespace FluentTerminal.App.ViewModels
public Task Close()
{
MessengerInstance.Unregister(this);
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
return Terminal.Close();
}
public void CopyText(string text)
{
ClipboardService.SetText(text);
if (_terminalOptions.ShowTextCopied)
{
Overlay.Show(I18N.Translate("TextCopied"));
}
}
public async Task EditTitle()
{
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"));
var result = await DialogService.ShowInputDialogAsync("Edit Title");
if (result != null)
{
if (string.IsNullOrWhiteSpace(result))
@@ -401,112 +267,64 @@ namespace FluentTerminal.App.ViewModels
FocusTerminal();
}
private bool CanExecuteCommand<T>(T a)
{
return CanExecuteCommand();
}
private bool CanExecuteCommand()
{
return Initialized && !_disposalRequested;
}
private async Task CloseTab()
{
await TryClose().ConfigureAwait(false);
}
private void CloseLeftTabs()
{
CloseLeftTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void CloseRightTabs()
{
CloseRightTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void CloseOtherTabs()
{
CloseOtherTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void FindNext()
{
FindNextRequested?.Invoke(this, SearchText);
}
public ITerminalView TerminalView { get; set; }
public async Task<string> SerializeXtermState()
{
return await TerminalView?.SerializeXtermState();
}
private void FindPrevious()
{
FindPreviousRequested?.Invoke(this, SearchText);
}
private void DuplicateTab()
{
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
}
private async void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
await ApplicationView.RunOnDispatcherThread(() =>
{
ApplicationSettings = message.ApplicationSettings;
ApplicationSettings = e;
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
});
}
private async void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
private async void OnCurrentThemeChanged(object sender, Guid e)
{
await ApplicationView.RunOnDispatcherThread(() =>
{
// only change theme if not overwritten by profile
if (ShellProfile.TerminalThemeId == Guid.Empty)
{
var currentTheme = SettingsService.GetTheme(message.ThemeId);
var currentTheme = SettingsService.GetTheme(e);
TerminalTheme = currentTheme;
ThemeChanged?.Invoke(this, currentTheme);
}
});
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
private async void OnKeyBindingsChanged(object sender, EventArgs e)
{
_terminalOptions = message.TerminalOptions;
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
}
private void SelectTabTheme(string id)
private void OnResizeOverlayTimerFinished(object sender, object e)
{
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
_resizeOverlayTimer.Stop();
ShowResizeOverlay = false;
}
private void Terminal_Exited(object sender, int exitCode)
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker && exitCode != 0)
{
tracker.SetInvalid();
}
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
}
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
private void SelectTabTheme(string name)
{
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
}
private void Terminal_Closed(object sender, EventArgs e)
{
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
Terminal.KeyboardCommandReceived -= Terminal_KeyboardCommandReceived;
Terminal.OutputReceived -= Terminal_OutputReceived;
Terminal.SizeChanged -= Terminal_SizeChanged;
Terminal.TitleChanged -= Terminal_TitleChanged;
Terminal.Exited -= Terminal_Exited;
Terminal.Closed -= Terminal_Closed;
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
@@ -516,7 +334,7 @@ namespace FluentTerminal.App.ViewModels
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
CopyText(selection);
ClipboardService.SetText(selection);
break;
}
case nameof(Command.Paste):
@@ -558,11 +376,6 @@ namespace FluentTerminal.App.ViewModels
private void Terminal_OutputReceived(object sender, byte[] e)
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
tracker.SetOutputReceived();
}
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
@@ -571,7 +384,8 @@ namespace FluentTerminal.App.ViewModels
private void Terminal_SizeChanged(object sender, TerminalSize e)
{
Overlay.Show($"{e.Columns} x {e.Rows}");
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
ShowResizeOverlay = true;
}
private void Terminal_TitleChanged(object sender, string e)
@@ -582,14 +396,9 @@ namespace FluentTerminal.App.ViewModels
private async Task TryClose()
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
tracker.SetInvalid();
}
if (ApplicationSettings.ConfirmClosingTabs)
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), String.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await DialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.Cancel)
{
@@ -344,7 +344,7 @@ namespace FluentTerminal.App.ViewModels
if (!_fallbackTheme.Equals(changedTheme))
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -392,7 +392,7 @@ namespace FluentTerminal.App.ViewModels
private async Task Delete()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteTheme"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this theme?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -1,15 +0,0 @@
namespace FluentTerminal.App.ViewModels.Utilities
{
public static class WebViewSpecialCharEncoder
{
public static string Encode(string value)
{
return value.Replace("\"", "&quot;").Replace("'", "&squo;").Replace("\\", "&bsol;");
}
public static string Decode(string value)
{
return value.Replace("&quot;", "\"").Replace("&squo;", "'").Replace("&bsol;", "\\");
}
}
}
@@ -12,7 +12,7 @@ namespace FluentTerminal.App.Adapters
{
private readonly AppServiceConnection _appServiceConnection;
public event EventHandler<ValueSet> MessageReceived;
public event EventHandler<IDictionary<string, string>> MessageReceived;
public AppServiceConnectionAdapter(AppServiceConnection appServiceConnection)
{
@@ -22,16 +22,35 @@ namespace FluentTerminal.App.Adapters
private void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
MessageReceived?.Invoke(this, args.Request.Message);
var messageType = (string)args.Request.Message[MessageKeys.Type];
var messageContent = (string)args.Request.Message[MessageKeys.Content];
MessageReceived?.Invoke(this, new Dictionary<string, string>
{
[MessageKeys.Type] = messageType,
[MessageKeys.Content] = messageContent
});
}
public async Task<ValueSet> SendMessageAsync(ValueSet message)
public async Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message)
{
var response = await _appServiceConnection.SendMessageAsync(message).AsTask();
var valueSet = new ValueSet();
foreach (var pair in message)
{
valueSet.Add(pair.Key, pair.Value);
}
var response = await _appServiceConnection.SendMessageAsync(valueSet).AsTask();
if (response.Status == AppServiceResponseStatus.Success)
{
return response.Message;
var responseDict = new Dictionary<string, string>();
foreach (var pair in response.Message)
{
responseDict.Add(pair.Key, (string)pair.Value);
}
return responseDict;
}
return null;
}
@@ -31,7 +31,6 @@ namespace FluentTerminal.App.Adapters
private void _applicationView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
_applicationView.Consolidated -= _applicationView_Consolidated;
Closed?.Invoke(this, EventArgs.Empty);
}
@@ -48,15 +47,8 @@ namespace FluentTerminal.App.Adapters
return ApiInformation.IsApiContractPresent(api, version);
}
public Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true)
public Task RunOnDispatcherThread(Action action)
{
if (!enforceNewSchedule && _dispatcher.HasThreadAccess)
{
action();
return Task.CompletedTask;
}
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
}
@@ -11,11 +11,12 @@ namespace FluentTerminal.App.Adapters
public DispatcherTimerAdapter()
{
_dispatcherTimer = new DispatcherTimer();
_dispatcherTimer.Tick += OnDispatcherTimerTick;
}
private void OnDispatcherTimerTick(object sender, object e)
{
Tick?.Invoke(this, EventArgs.Empty);
Tick?.Invoke(this, null);
}
public TimeSpan Interval
@@ -30,14 +31,12 @@ namespace FluentTerminal.App.Adapters
public void Start()
{
_dispatcherTimer.Tick += OnDispatcherTimerTick;
_dispatcherTimer.Start();
}
public void Stop()
{
_dispatcherTimer.Stop();
_dispatcherTimer.Tick -= OnDispatcherTimerTick;
}
}
}
@@ -1,6 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.Utilities;
using System;
using System.Threading.Tasks;
using Windows.UI.Popups;
@@ -30,19 +29,14 @@ namespace FluentTerminal.App.Adapters
public void AddButton(DialogButton button)
{
var command = new UICommand
{
Label = I18N.Translate(button.ToString()),
Id = button
};
_messageDialog.Commands.Add(command);
_messageDialog.Commands.Add(new UICommand(button.ToString()));
}
public async Task<DialogButton> ShowAsync()
{
var result = await _messageDialog.ShowAsync();
return (DialogButton)result.Id;
return Enum.Parse<DialogButton>(result.Label);
}
}
}
+1 -3
View File
@@ -6,7 +6,6 @@
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="Styles\Custom.xaml" />
</ResourceDictionary.MergedDictionaries>
<LinearGradientBrush x:Key="MissingColorSelectionBrush" StartPoint="0,0" EndPoint="1,1">
@@ -27,7 +26,6 @@
<converters:FalseToVisibleConverter x:Key="FalseToVisibleConverter" />
<converters:TrueToVisibleConverter x:Key="TrueToVisibleConverter" />
<converters:TabColorFallbackConverter x:Key="TabColorFallbackConverter" />
<converters:TextMiddleEllipsisConverter x:Key="TextMiddleEllipsisConverter" />
<converters:IntToVisibilityConverter
x:Key="ZeroToCollapsedConverter"
Else="Visible"
@@ -39,8 +37,8 @@
<converters:EnumValueToVisibiltyConverter x:Key="EnumValueToVisibiltyConverter" />
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
<converters:EnumToDescriptionConverter x:Key="EnumToDescriptionConverter" />
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
<converters:I18NConverter x:Key="I18NConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
Executable → Regular
+52 -402
View File
@@ -28,28 +28,19 @@ using Windows.ApplicationModel.Background;
using Windows.ApplicationModel.Core;
using Windows.Storage;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using FluentTerminal.App.Utilities;
using IContainer = Autofac.IContainer;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
namespace FluentTerminal.App
{
// ReSharper disable once RedundantExtendsListEntry
public sealed partial class App : Application
{
private readonly TaskCompletionSource<int> _trayReady = new TaskCompletionSource<int>();
public TaskCompletionSource<int> _trayReady = new TaskCompletionSource<int>();
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IDialogService _dialogService;
private bool _alreadyLaunched;
private bool _isLaunching;
private ApplicationSettings _applicationSettings;
private readonly IContainer _container;
private readonly List<MainViewModel> _mainViewModels;
@@ -58,7 +49,6 @@ namespace FluentTerminal.App
private IAppServiceConnection _appServiceConnection;
private BackgroundTaskDeferral _appServiceDeferral;
private Parser _commandLineParser;
private int? _activeWindowId;
public App()
{
@@ -74,10 +64,9 @@ namespace FluentTerminal.App
RoamingSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings),
KeyBindings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.KeyBindingsContainerName, ApplicationDataCreateDisposition.Always)),
ShellProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings.CreateContainer(Constants.ShellProfilesContainerName, ApplicationDataCreateDisposition.Always)),
Themes = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ThemesContainerName, ApplicationDataCreateDisposition.Always)),
SshProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.SshProfilesContainerName, ApplicationDataCreateDisposition.Always)),
HistoryContainer = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ExecutedCommandsContainerName, ApplicationDataCreateDisposition.Always))
Themes = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ThemesContainerName, ApplicationDataCreateDisposition.Always))
};
var builder = new ContainerBuilder();
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
builder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>().SingleInstance();
@@ -95,43 +84,22 @@ namespace FluentTerminal.App
builder.RegisterType<FileSystemService>().As<IFileSystemService>().SingleInstance();
builder.RegisterType<SystemFontService>().As<ISystemFontService>().SingleInstance();
builder.RegisterType<ShellProfileSelectionDialog>().As<IShellProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<SshProfileSelectionDialog>().As<ISshProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<CreateKeyBindingDialog>().As<ICreateKeyBindingDialog>().InstancePerDependency();
builder.RegisterType<InputDialog>().As<IInputDialog>().InstancePerDependency();
builder.RegisterType<AboutDialog>().As<IAboutDialog>().InstancePerDependency();
builder.RegisterType<MessageDialogAdapter>().As<IMessageDialog>().InstancePerDependency();
builder.RegisterType<SshInfoDialog>().As<ISshConnectionInfoDialog>().InstancePerDependency();
builder.RegisterType<CustomCommandDialog>().As<ICustomCommandDialog>().InstancePerDependency();
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
builder.RegisterType<ApplicationLanguageService>().As<IApplicationLanguageService>().SingleInstance();
builder.RegisterType<ShellProfileMigrationService>().As<IShellProfileMigrationService>().SingleInstance();
builder.RegisterInstance(applicationDataContainers);
_container = builder.Build();
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
_settingsService = _container.Resolve<ISettingsService>();
var shellProfileMigrationService = _container.Resolve<IShellProfileMigrationService>();
foreach (var profile in _settingsService.GetShellProfiles())
{
shellProfileMigrationService.Migrate(profile);
_settingsService.SaveShellProfile(profile);
}
foreach (var profile in _settingsService.GetSshProfiles())
{
shellProfileMigrationService.Migrate(profile);
_settingsService.SaveSshProfile(profile);
}
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_trayProcessCommunicationService = _container.Resolve<ITrayProcessCommunicationService>();
_dialogService = _container.Resolve<IDialogService>();
_applicationSettings = _settingsService.GetApplicationSettings();
JsonConvert.DefaultSettings = () =>
@@ -181,200 +149,20 @@ namespace FluentTerminal.App
}
}
protected override async void OnActivated(IActivatedEventArgs args)
protected override void OnActivated(IActivatedEventArgs args)
{
if (args is ProtocolActivatedEventArgs protocolActivated)
{
if (protocolActivated.Uri == new Uri("ftcmd://fluent.terminal?focus"))
{
await ShowOrCreateWindow(protocolActivated.ViewSwitcher);
return;
}
MainViewModel mainViewModel = null;
// IApplicationView to use for creating view models
IApplicationView applicationView;
if (_alreadyLaunched)
{
applicationView =
(_mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId) ??
_mainViewModels.Last()).ApplicationView;
}
else
{
// App wasn't launched before double clicking a shortcut, so we have to create a window
// in order to be able to communicate with user.
mainViewModel = _container.Resolve<MainViewModel>();
await CreateMainView(typeof(MainPage), mainViewModel, true);
applicationView = mainViewModel.ApplicationView;
}
bool isSsh;
try
{
isSsh = SshConnectViewModel.CheckScheme(protocolActivated.Uri);
}
catch (Exception ex)
{
await new MessageDialog(
$"{I18N.TranslateWithFallback("InvalidLink", "Invalid link.")} {ex.Message}",
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
return;
}
if (isSsh)
{
SshConnectViewModel vm;
try
{
vm = SshConnectViewModel.ParseUri(protocolActivated.Uri, _settingsService, applicationView,
_trayProcessCommunicationService, _container.Resolve<IFileSystemService>(),
_container.Resolve<ApplicationDataContainers>().HistoryContainer);
}
catch (Exception ex)
{
await new MessageDialog(
$"{I18N.TranslateWithFallback("InvalidLink", "Invalid link.")} {ex.Message}",
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
return;
}
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks && string.IsNullOrEmpty(vm.Username))
{
vm.Username = await _trayProcessCommunicationService.GetUserName();
}
var error = await vm.AcceptChangesAsync(true);
SshProfile profile = (SshProfile) vm.Model;
if (!string.IsNullOrEmpty(error))
{
// Link is valid, but incomplete (i.e. username missing), so we need to show dialog.
profile = await _dialogService.ShowSshConnectionInfoDialogAsync(profile);
if (profile == null)
{
// User clicked "Cancel" in the dialog.
mainViewModel?.ApplicationView.TryClose();
return;
}
}
if (mainViewModel == null)
await CreateTerminal(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
else
await mainViewModel.AddTerminalAsync(profile);
return;
}
if (CommandProfileProviderViewModel.CheckScheme(protocolActivated.Uri))
{
CommandProfileProviderViewModel vm;
try
{
vm = CommandProfileProviderViewModel.ParseUri(protocolActivated.Uri, _settingsService,
applicationView, _trayProcessCommunicationService,
_container.Resolve<ApplicationDataContainers>().HistoryContainer);
}
catch (Exception ex)
{
await new MessageDialog(
$"{I18N.TranslateWithFallback("InvalidLink", "Invalid link.")} {ex.Message}",
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
return;
}
var error = await vm.AcceptChangesAsync(true);
var profile = vm.Model;
if (!string.IsNullOrEmpty(error))
{
// Link is valid, but incomplete, so we need to show dialog.
profile = await _dialogService.ShowCustomCommandDialogAsync(profile);
if (profile == null)
{
// User clicked "Cancel" in the dialog.
mainViewModel?.ApplicationView.TryClose();
return;
}
}
if (mainViewModel == null)
{
await CreateTerminal(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
}
else
{
await mainViewModel.AddTerminalAsync(profile);
}
return;
}
await new MessageDialog(
$"{I18N.TranslateWithFallback("InvalidLink", "Invalid link.")} {protocolActivated.Uri}",
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
return;
}
if (args is CommandLineActivatedEventArgs commandLineActivated)
{
var arguments = commandLineActivated.Operation.Arguments;
if (string.IsNullOrWhiteSpace(arguments))
if (string.IsNullOrWhiteSpace(commandLineActivated.Operation.Arguments))
{
arguments = "new";
return;
}
_commandLineParser.ParseArguments(SplitArguments(arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
_commandLineParser.ParseArguments(SplitArguments(commandLineActivated.Operation.Arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
{
if (verb is SettingsVerb settingsVerb)
if (verb is SettingsVerb)
{
if (!settingsVerb.Import && !settingsVerb.Export)
{
await ShowSettings().ConfigureAwait(true);
}
else if (settingsVerb.Export)
{
var exportFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var settings = _settingsService.ExportSettings();
await FileIO.WriteTextAsync(exportFile, settings);
await new MessageDialog($"{I18N.Translate("SettingsExported")} {exportFile.Path}").ShowAsync();
}
else if (settingsVerb.Import)
{
var file = await ApplicationData.Current.LocalFolder.GetFileAsync("config.json");
var content = await FileIO.ReadTextAsync(file);
_settingsService.ImportSettings(content);
await new MessageDialog($"{I18N.Translate("SettingsImported")} {file.Path}").ShowAsync();
}
await ShowSettings().ConfigureAwait(true);
}
else if (verb is NewVerb newVerb)
{
@@ -389,14 +177,10 @@ namespace FluentTerminal.App
profile = _settingsService.GetDefaultShellProfile();
}
if (!string.IsNullOrWhiteSpace(newVerb.Directory) && newVerb.Directory != ".")
if (!string.IsNullOrWhiteSpace(newVerb.Directory))
{
profile.WorkingDirectory = newVerb.Directory;
}
else
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
@@ -438,78 +222,34 @@ namespace FluentTerminal.App
}
}
private async Task ShowOrCreateWindow(ActivationViewSwitcher viewSwitcher)
{
if (_mainViewModels.Count == 0)
{
await CreateTerminal(_settingsService.GetDefaultShellProfile(), NewTerminalLocation.Tab, viewSwitcher);
}
else
{
var viewModel = _mainViewModels.Find(o => o.ApplicationView.Id == _activeWindowId) ?? _mainViewModels.Last();
await ShowAsStandaloneAsync(viewModel, viewSwitcher);
}
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
if (_isLaunching)
{
return;
}
_isLaunching = true;
if (!_alreadyLaunched)
{
await InitializeLogger();
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);
// TODO: Check the reason for such strange using of tasks!
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
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>();
if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
await viewModel.AddLocalTabOrWindowAsync(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
}
else
{
await viewModel.AddLocalTabAsync();
}
viewModel.AddTerminal();
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
Window.Current.Activate();
}
else if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
else if (_mainViewModels.Count == 0)
{
var location = _applicationSettings.NewTerminalLocation;
var profile = _settingsService.GetShellProfile(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
await CreateTerminal(profile, location, args.ViewSwitcher).ConfigureAwait(true);
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await viewModel.AddLocalTabAsync();
}
_isLaunching = false;
}
private static async Task InitializeLogger()
{
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);
}
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
@@ -536,17 +276,15 @@ namespace FluentTerminal.App
_appServiceDeferral = null;
_appServiceConnection = null;
// ReSharper disable once ArrangeStaticMemberQualifier
Application.Current.Exit();
}
private async Task CreateMainView(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
{
ApplicationViewSwitcher.DisableSystemViewActivationPolicy();
await StartSystemTray().ConfigureAwait(true);
if (!(Window.Current.Content is Frame rootFrame))
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
rootFrame = new Frame();
Window.Current.Content = rootFrame;
@@ -561,8 +299,7 @@ namespace FluentTerminal.App
mainViewModel.Closed += OnMainViewModelClosed;
mainViewModel.NewWindowRequested += OnNewWindowRequested;
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ActivatedMv += OnMainViewActivated;
mainViewModel.TabTearedOff += OnTabTearOff;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(mainViewModel);
}
@@ -578,14 +315,13 @@ namespace FluentTerminal.App
viewModel.Closed += OnMainViewModelClosed;
viewModel.NewWindowRequested += OnNewWindowRequested;
viewModel.ShowSettingsRequested += OnShowSettingsRequested;
viewModel.ActivatedMv += OnMainViewActivated;
viewModel.TabTearedOff += OnTabTearOff;
viewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(viewModel);
return viewModel;
}
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool extendViewIntoTitleBar)
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar)
{
int windowId = 0;
TViewModel viewModel = default;
@@ -593,7 +329,7 @@ namespace FluentTerminal.App
{
viewModel = _container.Resolve<TViewModel>();
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = ExtendViewIntoTitleBar;
var frame = new Frame();
frame.Navigate(pageType, viewModel);
Window.Current.Content = frame;
@@ -614,10 +350,9 @@ namespace FluentTerminal.App
return viewModel;
}
private void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
private void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
_applicationSettings = message.ApplicationSettings;
_trayProcessCommunicationService.UpdateSettings(message.ApplicationSettings);
_applicationSettings = e;
}
private void OnMainViewModelClosed(object sender, EventArgs e)
@@ -629,150 +364,65 @@ namespace FluentTerminal.App
viewModel.Closed -= OnMainViewModelClosed;
viewModel.NewWindowRequested -= OnNewWindowRequested;
viewModel.ShowSettingsRequested -= OnShowSettingsRequested;
viewModel.ActivatedMv -= OnMainViewActivated;
viewModel.TabTearedOff -= OnTabTearOff;
if (_activeWindowId == viewModel.ApplicationView.Id)
{
_activeWindowId = 0;
}
viewModel.ShowAboutRequested -= OnShowAboutRequested;
_mainViewModels.Remove(viewModel);
Window.Current.Content = null;
}
}
private void OnMainViewActivated(object sender, EventArgs e)
{
if (sender is MainViewModel viewModel)
{
Logger.Instance.Debug("MainViewModel with ApplicationView Id: {@id} activated.", viewModel.ApplicationView.Id);
_activeWindowId = viewModel.ApplicationView.Id;
}
}
private async void OnTabTearOff(object sender, TerminalViewModel model)
{
Logger.Instance.Debug("App.xaml.cs on tab tear off");
var newViewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await newViewModel.AddTerminalAsync(await model.Serialize(), 0);
}
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
ShellProfile profile = null;
switch (e.Action)
{
case NewWindowAction.StartDefaultLocalTerminal:
break;
case NewWindowAction.ShowProfileSelection:
profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshProfileSelection:
profile = await _dialogService.ShowSshProfileSelectionDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowCustomCommandDialog:
profile = await _dialogService.ShowCustomCommandDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshInfoDialog:
profile = await _dialogService.ShowSshConnectionInfoDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.StartLocalTerminal:
profile = _settingsService.GetShellProfile(e.ProfileId);
break;
case NewWindowAction.StartSshTerminal:
profile = _settingsService.GetSshProfile(e.ProfileId);
break;
default:
throw new NotImplementedException("this ProfileSelection not implemented yet");
}
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
if (profile == null)
if (e.ShowProfileSelection)
{
await viewModel.AddLocalTabAsync();
await viewModel.AddConfigurableTerminal().ConfigureAwait(true);
}
else
{
await viewModel.AddTerminalAsync(profile);
viewModel.AddTerminal();
}
}
private void OnSettingsClosed(object sender, EventArgs e)
{
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
_settingsViewModel.Closed -= OnSettingsClosed;
_settingsViewModel = null;
_settingsWindowId = null;
}
private void OnShowAboutRequested(object sender, EventArgs e)
{
ShowAbout().ConfigureAwait(true);
}
private async void OnShowSettingsRequested(object sender, EventArgs e)
{
await ShowSettings().ConfigureAwait(true);
}
private async Task ShowAsStandaloneAsync(MainViewModel viewModel, ActivationViewSwitcher viewSwitcher = null)
private async Task ShowAbout()
{
int viewId = viewModel.ApplicationView.Id;
if (viewSwitcher != null)
{
await viewModel.ApplicationView.RunOnDispatcherThread(async () => await viewSwitcher.ShowAsStandaloneAsync(viewId));
}
else
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId);
}
await ShowSettings().ConfigureAwait(true);
_settingsViewModel.NavigateToAboutPage();
}
private async Task CreateTerminal(ShellProfile profile, NewTerminalLocation location, ActivationViewSwitcher viewSwitcher = null)
private async Task CreateTerminal(ShellProfile profile, NewTerminalLocation location)
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminalAsync(profile);
viewModel.AddTerminal(profile);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
}
else if (location == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
{
MainViewModel item = _mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId);
if (item == null)
{
item = _mainViewModels.Last();
}
await item.AddTerminalAsync(profile);
await ShowAsStandaloneAsync(item, viewSwitcher);
_mainViewModels.Last().AddTerminal(profile);
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await viewModel.AddTerminalAsync(profile);
await ShowAsStandaloneAsync(viewModel, viewSwitcher);
viewModel.AddTerminal(profile);
}
}
@@ -800,4 +450,4 @@ namespace FluentTerminal.App
_trayProcessCommunicationService.Initialize(_appServiceConnection);
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Loaded 100 of 409 files, more files were not shown because too many files have changed in this diff. Show more