Compare commits

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

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

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

* Add width to Themes text input

* Normalize margins in settings

* Add default display values in Terminal settings

... for Arguments and Working Directory

* Generalize TemplateSelector for empty strings

* Add default display value for profile keybindings

* Update margins and fontsize in theme preview

* Update 'loading...' message in About

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

* No popup when cancelling profile edit no changes

* Delete profile when new and cancelled

* Open new and cloned themes in edit mode

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

* No popup when cancelling theme edit no changes

* Add resources for margin definitions

* Check for null Author in TerminalTheme

* Add resource for header margin definition

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

* Close new configurable window on cancel

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

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

* Added UI elements for choosing line ending style.

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

* Added "Paste without newlines" command.

* Adding missing converters to project file.

* A few fixes after cherry-picks.

* Adding missing Command property to PasteWithoutNewlines keybinding.

* Cleaned up some unnecessary property settings.

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

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

* rephrased comment

* removed css that enforced autohiding

* simplified js code handling scrollbar size

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

... As if the primary button is pressed.

* Renew certificate

* Update README.md

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

* Add option to use bold text in terminals

... as per #182

* Add BoldText to setting defaults

* Remove AppVeyor files

* Update README.md

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

* Add keybinding for change the (current) tab title

Default binding is Ctrl+Shift+N

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

* Experimental ConPTY support (#199)

* First steps of ConPty support

* Added resize functionality

* fix cwd, args, hide console window

* Handle shell process exited

* Make ConPTY optional

* Fix automerge errors

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

* Fixed spaxcng on the settings page.

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

* Added resize functionality

* fix cwd, args, hide console window

* Handle shell process exited

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

Default binding is Ctrl+Shift+N

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

... as per #182

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

* rephrased comment

* removed css that enforced autohiding

* simplified js code handling scrollbar size
2019-01-14 21:47:35 +01:00
FelixSe 8131140cb0 Increase version 2019-01-08 19:51:54 +01:00
FelixSe 304314230c update xterm to 3.10.1 2019-01-08 19:27:47 +01:00
FelixSe 0ce605f122 Fix DialogService tests 2019-01-07 22:46:26 +01:00
FelixSe d4aadd7dd4 Added option to change the title of a tab 2019-01-07 22:35:14 +01:00
FelixSe 3cf3e0a330 Themes can now be cloned 2019-01-07 19:37:21 +01:00
FelixSe ff2cfc36b6 Fix problems with selectAll, clear and Ctrl+C 2019-01-04 18:15:24 +01:00
FelixSe b118c66959 Increase version 2019-01-02 22:15:02 +01:00
FelixSe 131e3536f1 Fix theme not updating for profiles without theme 2018-12-30 22:56:11 +01:00
FelixSe b47b544065 Added option to restore preinstalled profiles 2018-12-30 19:00:24 +01:00
FelixSe b13c40e7e8 Clone profiles 2018-12-30 16:20:56 +01:00
Felix 4e47fcef72 Added option to specify theme in profile (#158) 2018-12-30 15:12:04 +01:00
Mike 8acf618fd4 Per-shell-profile keyboard shortcuts (#85)
* Added new command wrapper, abstracts static and dynamic commands

- All uses of the `Command` are replaced with either `AppCommand`, for a command that is static to the scope of the application, or a reference to an ICommand (specifically, an `EnumCommand<AppCommand>()` instance), which provides the abstract wrapper.
- Functionality seems intact.
- Next step is to have the MainViewModel add the per-shell commands via SettingsService.CreateShellKeyboardShortcuts(), and have the SettingsService.ParseCommandString() function parse the command strings passed around properly.
- Finally, add the keyboard chord input to each shell's settings page.

* First kick at adding a new terminal keyboard command.

- It isn't presented anywhere, but references to keybindings should be using the ICommand key as their identifier for recognizing anything, and when xterm passes back a keyboard command event, the SettingsService is asked to convert the string into an ICommand.

* Renaming ICommand to Command.

It's not longer an interface, so this makes more sense.

* Renaming back to Command, and new class is AbstractCommand.

Going to simplify the PR diff considerably.

* Shortcuts for default shells work. Need to get the shell profile edit UI elements working.

* Added keyboard shortcut UI elements to shell profile pages.

* New and deleted shell profiles propagate to all terminal views.

* A bit of cleanup.

* Camel-casing command text to fix Ctrl+C and copy behaviour.

* Fixed failing test.

The new on disk store uses a .ToString(), not the Enum description, since the command isn't an Enum, it's a class now in general, and those semantics make more sense.

* Adding default value for null keybindings in shell profiles.

* Merge branch 'shell_commands' of https://github.com/Riebart/FluentTerminal into pr/85

Improved default value for keybinding property

* Renamed KeyBinding collection to KeyBindings

* Use strings as Command parameter of KeyBinding

* Added missing Commands to keyBindings of default shellProfiles

* fix build and unit tests after merge with master

* improve UI

* Split OnShellProfileCollectionChanged event
2018-12-29 13:20:24 +01:00
Eric Cornelissen 49e09f3f8b Add "Show in system tray" option (#142)
* Add "Show in system tray" option to the SettingsView

* Implement "Show in system tray" requiring restart

* Add warning to restart program when toggling tray

* Update warning text for toggling tray option
2018-12-26 16:31:28 +01:00
Fauzie 9bf8284d9f Add Padding setting (#155)
* add padding setting

* `=` instead of `:`

Co-Authored-By: fauzie811 <fauzie811@yahoo.com>

* call setPadding in createTerminal function

* Add "px" suffix to padding slider tooltip

Based on this tutorial: 
https://www.codeproject.com/Tips/715438/Slider-Thumb-Tooltip

* term.fit() after setPadding()

* move back term.fit() below term.open()

* Changed max value, added throttling, moved converter
2018-12-26 14:41:16 +01:00
FelixSe 0f8aac81de add throttle for background opacity binding 2018-12-26 14:18:54 +01:00
FelixSe abec73b868 update xterm to 3.9.1 2018-12-25 15:50:07 +01:00
Zed 86486c99f2 fixed open here dir param. (#145) 2018-12-04 07:30:32 +01:00
Abhishek Goenka d9b7052e87 Prerequisite added (#139) 2018-11-19 17:18:44 +01:00
Meir Blachman 0efe105321 added missing test for KeyboardCommandService (#124)
added missing test for KeyboardCommandService for Command.ToggleWindow
2018-11-12 21:15:57 +01:00
Eric Cornelissen ed0dad960d Fix display issue when settings window is too narrow (#115)
Closes #100
2018-11-07 07:05:16 +01:00
Eric Cornelissen 2f58ed63de Add About page to the settings window (#103)
* create "About" page in settings

* add content to About page

* Get latest version from GitHub

* Add NotificationService to servies

* Enable actions on notifications

* Get latest version asynchroniously

* Handle when latest version is not found and clean code

* Remove old UpdateService

* Remove old NotificationService

* Add icon for about tab

* Update About page styling

* Make About menu item a footer menu item

* Update About page layout

Based on suggestions by @felixse

* Instatiate temporary navigation item only once

This seems to solve display issues when opening the "About" page multiple times where the main navigation list is being rerendered for no apparent reason.

* Revert unintended changes

* Added shortcut to about page in hamburger menu

* Move About page properties to the AboutPageViewModel

* Make "loading..." and "Not found" italic to indicate non-default behaviour

* move update check back to systemtray

* Made IUpdateService async
2018-10-28 13:55:36 +01:00
125 changed files with 4708 additions and 1382 deletions

No files matched your search

@@ -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);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>);
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>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
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>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
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>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
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>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
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>);
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
dialogService.ShowProfileSelectionDialogAsync();
@@ -20,7 +20,7 @@ namespace FluentTerminal.App.Services.Test
[Fact]
public void RegisterCommandHandler_HandlerIsNull_ThrowsArgumentNullException()
{
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
Action handler = null;
var keyboardCommandService = new KeyboardCommandService();
@@ -32,7 +32,7 @@ namespace FluentTerminal.App.Services.Test
[Fact]
public void RegisterCommandHandler_CommandAlreadyRegisted_ThrowsInvalidOperationException()
{
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
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);
@@ -45,7 +45,7 @@ namespace FluentTerminal.App.Services.Test
[Fact]
public void SendCommand_CommandIsRegistered_HandlerGetsInvoked()
{
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var handlerCalled = false;
var keyboardCommandService = new KeyboardCommandService();
keyboardCommandService.RegisterCommandHandler(command, () => handlerCalled = true);
@@ -58,12 +58,23 @@ namespace FluentTerminal.App.Services.Test
[Fact]
public void SendCommand_CommandIsNotRegisted_KeyNotFoundExceptionIsThrown()
{
var command = (Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length);
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.SendCommand(command);
invoke.Should().Throw<KeyNotFoundException>();
}
[Fact]
public void SendCommand_CommandIsToggleWindow_ShouldNotThrow()
{
var command = nameof(Command.ToggleWindow);
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.SendCommand(command);
invoke.Should().NotThrow();
}
}
}
}
@@ -1,6 +1,7 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Moq;
@@ -439,12 +440,12 @@ namespace FluentTerminal.App.Services.Test
[Fact]
public void GetKeyBindings_KeyBindingsInKeyBindingsContainer_ReturnsKeyBindingsFromKeyBindingsContainer()
{
var keyBindings = _fixture.CreateMany<KeyBinding>(5);
var commands = _fixture.CreateMany<Command>(5);
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
foreach (var keyBinding in keyBindings)
foreach (var command in commands)
{
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(keyBinding.Command.ToString(), null)).Returns(new Collection<KeyBinding> { keyBinding });
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null)).Returns(new Collection<KeyBinding> { });
}
var applicationDataContainers = new ApplicationDataContainers
{
@@ -456,23 +457,23 @@ namespace FluentTerminal.App.Services.Test
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetKeyBindings();
var result = settingsService.GetCommandKeyBindings();
foreach (var keyBinding in keyBindings)
foreach (var command in commands)
{
keyBindingsContainer.Verify(x => x.ReadValueFromJson<Collection<KeyBinding>>(keyBinding.Command.ToString(), null), Times.Once);
keyBindingsContainer.Verify(x => x.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null), Times.Once);
}
}
[Fact]
public void GetKeyBindings_KeyBindingsNotInKeyBindingsContainer_ReturnsKeyBindingsFromDefaultValueProvider()
{
var keyBindings = _fixture.CreateMany<KeyBinding>(5);
var commands = _fixture.CreateMany<Command>(5);
var defaultValueProvider = new Mock<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
foreach (var keyBinding in keyBindings)
foreach (var command in commands)
{
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(keyBinding.Command.ToString(), null)).Returns(new Collection<KeyBinding> { keyBinding });
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(EnumHelper.GetEnumDescription(command), null)).Returns(new Collection<KeyBinding> { });
}
var applicationDataContainers = new ApplicationDataContainers
{
@@ -484,14 +485,10 @@ namespace FluentTerminal.App.Services.Test
};
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
var result = settingsService.GetKeyBindings();
var result = settingsService.GetCommandKeyBindings();
foreach (Command command in Enum.GetValues(typeof(Command)))
{
if (keyBindings.Select(x => x.Command).Contains(command))
{
continue;
}
defaultValueProvider.Verify(x => x.GetDefaultKeyBindings(command), Times.Once);
}
}
@@ -513,7 +510,7 @@ namespace FluentTerminal.App.Services.Test
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.SaveKeyBindings(command, keyBindings);
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
keyBindingsContainer.Verify(x => x.WriteValueAsJson(command.ToString(), keyBindings), Times.Once);
}
@@ -537,7 +534,7 @@ namespace FluentTerminal.App.Services.Test
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
settingsService.SaveKeyBindings(command, keyBindings);
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
keyBindingsChangedEventInvoked.Should().BeTrue();
}
@@ -28,9 +28,9 @@ namespace FluentTerminal.App.Services.Test
{
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[Command.ToggleWindow] = keyBindings.ToList()
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var response = _fixture.Create<CreateTerminalResponse>();
@@ -42,10 +42,11 @@ namespace FluentTerminal.App.Services.Test
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile);
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile, sessionType);
result.Should().BeEquivalentTo(response);
}
@@ -57,9 +58,9 @@ namespace FluentTerminal.App.Services.Test
var terminalSize = _fixture.Create<TerminalSize>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[Command.ToggleWindow] = keyBindings.ToList()
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
@@ -77,9 +78,9 @@ namespace FluentTerminal.App.Services.Test
var text = _fixture.Create<string>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[Command.ToggleWindow] = keyBindings.ToList()
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
@@ -96,9 +97,9 @@ namespace FluentTerminal.App.Services.Test
var terminalId = _fixture.Create<int>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[Command.ToggleWindow] = keyBindings.ToList()
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
@@ -126,9 +127,9 @@ namespace FluentTerminal.App.Services.Test
};
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[Command.ToggleWindow] = keyBindings.ToList()
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
var appServiceConnection = new Mock<IAppServiceConnection>();
@@ -163,9 +164,9 @@ namespace FluentTerminal.App.Services.Test
};
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetKeyBindings()).Returns(new Dictionary<Command, ICollection<KeyBinding>>
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[Command.ToggleWindow] = keyBindings.ToList()
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
var appServiceConnection = new Mock<IAppServiceConnection>();
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IInputDialog
{
void SetTitle(string title);
Task<string> GetInput();
}
}
@@ -0,0 +1,7 @@
namespace FluentTerminal.App.Services.EventArgs
{
public class NewWindowRequestedEventArgs : System.EventArgs
{
public bool ShowProfileSelection { get; set; }
}
}
@@ -6,8 +6,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="PListNet" Version="2.0.3" />
<PackageReference Include="RestSharp" Version="106.5.4" />
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
@@ -17,5 +17,6 @@ namespace FluentTerminal.App.Services
Task<bool> TryClose();
bool ToggleFullScreen();
bool IsApiContractPresent(string api, ushort version);
}
}
@@ -16,5 +16,6 @@ namespace FluentTerminal.App.Services
Task<ShellProfile> ShowProfileSelectionDialogAsync();
Task<KeyBinding> ShowCreateKeyBindingDialog();
Task<string> ShowInputDialogAsync(string title);
}
}
@@ -1,12 +1,12 @@
using FluentTerminal.Models.Enums;
using System;
using System;
namespace FluentTerminal.App.Services
{
public interface IKeyboardCommandService
{
void RegisterCommandHandler(Command command, Action handler);
void RegisterCommandHandler(string command, Action handler);
void SendCommand(Command command);
void SendCommand(string command);
void DeregisterCommandHandler(string command);
}
}
@@ -0,0 +1,7 @@
namespace FluentTerminal.App.Services
{
public interface INotificationService
{
void ShowNotification(string title, string content, string url = null);
}
}
@@ -1,5 +1,4 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
@@ -10,7 +9,12 @@ namespace FluentTerminal.App.Services
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 DeleteTheme(Guid id);
ApplicationSettings GetApplicationSettings();
@@ -18,7 +22,7 @@ namespace FluentTerminal.App.Services
Guid GetCurrentThemeId();
ShellProfile GetDefaultShellProfile();
Guid GetDefaultShellProfileId();
IDictionary<Command, ICollection<KeyBinding>> GetKeyBindings();
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
IEnumerable<ShellProfile> GetShellProfiles();
IEnumerable<TabTheme> GetTabThemes();
TerminalOptions GetTerminalOptions();
@@ -28,9 +32,10 @@ namespace FluentTerminal.App.Services
void SaveApplicationSettings(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id);
void SaveKeyBindings(Command command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveTerminalOptions(TerminalOptions terminalOptions);
void SaveTheme(TerminalTheme theme);
void SaveTheme(TerminalTheme theme, bool newTheme = false);
ShellProfile GetShellProfile(Guid id);
}
}
@@ -1,39 +0,0 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface ITerminalView
{
event EventHandler<TerminalSize> TerminalSizeChanged;
event EventHandler<string> TerminalTitleChanged;
event EventHandler<Command> KeyboardCommandReceived;
Task ChangeTheme(TerminalColors theme);
Task ChangeOptions(TerminalOptions options);
Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings);
Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings);
Task ConnectToSocket(string url);
void Close();
Task FocusTerminal();
Task<string> GetSelection();
Task FindNext(string searchText);
Task FindPrevious(string searchText);
void FocusSearchTextBox();
}
}
@@ -1,4 +1,5 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Threading.Tasks;
@@ -11,7 +12,7 @@ namespace FluentTerminal.App.Services
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile);
Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task ResizeTerminal(int id, TerminalSize size);
@@ -22,5 +23,7 @@ namespace FluentTerminal.App.Services
void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
Task CloseTerminal(int terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
int GetNextTerminalId();
}
}
@@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IUpdateService
{
Task CheckForUpdate(bool notifyNoUpdate = false);
Version GetCurrentVersion();
Task<Version> GetLatestVersionAsync();
}
}
@@ -21,7 +21,9 @@ namespace FluentTerminal.App.Services.Implementation
MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu,
AlwaysShowTabs = false,
ShowNewOutputIndicator = false
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false,
EnableTrayIcon = true
};
}
@@ -34,7 +36,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.ToggleWindow,
Command = nameof(Command.ToggleWindow),
Ctrl = false,
Alt = false,
Shift = false,
@@ -47,7 +49,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.NextTab,
Command = nameof(Command.NextTab),
Ctrl = true,
Alt = false,
Shift = false,
@@ -60,7 +62,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.PreviousTab,
Command = nameof(Command.PreviousTab),
Ctrl = true,
Alt = false,
Shift = true,
@@ -81,7 +83,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = command,
Command = command.ToString(),
Ctrl = true,
Alt = false,
Shift = false,
@@ -94,7 +96,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.NewTab,
Command = nameof(Command.NewTab),
Ctrl = true,
Alt = false,
Shift = false,
@@ -107,7 +109,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.ConfigurableNewTab,
Command = nameof(Command.ConfigurableNewTab),
Ctrl = true,
Alt = false,
Shift = true,
@@ -115,12 +117,25 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ChangeTabTitle),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.R
}
};
case Command.CloseTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = Command.CloseTab,
Command = nameof(Command.CloseTab),
Ctrl = true,
Alt = false,
Shift = false,
@@ -133,7 +148,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.NewWindow,
Command = nameof(Command.NewWindow),
Ctrl = true,
Alt = false,
Shift = false,
@@ -141,12 +156,25 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ConfigurableNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ConfigurableNewWindow),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ShowSettings:
return new List<KeyBinding>
{
new KeyBinding
{
Command = Command.ShowSettings,
Command = nameof(Command.ShowSettings),
Ctrl = true,
Alt = false,
Shift = false,
@@ -159,7 +187,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.Copy,
Command = nameof(Command.Copy),
Ctrl = true,
Alt = false,
Shift = false,
@@ -172,7 +200,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.Paste,
Command = nameof(Command.Paste),
Ctrl = true,
Alt = false,
Shift = false,
@@ -180,12 +208,26 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.PasteWithoutNewlines:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.PasteWithoutNewlines),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.V
}
};
case Command.Search:
return new List<KeyBinding>
{
new KeyBinding
{
Command = Command.Search,
Command = nameof(Command.Search),
Ctrl = true,
Alt = false,
Shift = false,
@@ -198,7 +240,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.ToggleFullScreen,
Command = nameof(Command.ToggleFullScreen),
Ctrl = false,
Alt = true,
Shift = false,
@@ -211,7 +253,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.SelectAll,
Command = nameof(Command.SelectAll),
Ctrl = true,
Alt = false,
Shift = false,
@@ -224,7 +266,7 @@ namespace FluentTerminal.App.Services.Implementation
{
new KeyBinding
{
Command = Command.Clear,
Command = nameof(Command.Clear),
Ctrl = true,
Alt = false,
Shift = false,
@@ -304,7 +346,9 @@ namespace FluentTerminal.App.Services.Implementation
ScrollBarStyle = ScrollBarStyle.Hidden,
FontFamily = "Consolas",
FontSize = 13,
BoldText = false,
BackgroundOpacity = 0.8,
Padding = 12,
ScrollBackLimit = 1000
};
}
@@ -325,7 +369,20 @@ namespace FluentTerminal.App.Services.Implementation
Arguments = string.Empty,
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
PreInstalled = true,
WorkingDirectory = string.Empty
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
KeyBindings = new []
{
new KeyBinding
{
Command = GetDefaultShellProfileId().ToString(),
Ctrl=true,
Alt=true,
Shift=false,
Meta=false,
Key=(int)ExtendedVirtualKey.Number1
}
}
},
new ShellProfile
{
@@ -334,7 +391,20 @@ namespace FluentTerminal.App.Services.Implementation
Arguments = string.Empty,
Location = @"C:\Windows\System32\cmd.exe",
PreInstalled = true,
WorkingDirectory = string.Empty
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
KeyBindings = new []
{
new KeyBinding
{
Command = "ab942a61-7673-4755-9bd8-765aff91d9a3",
Ctrl=true,
Alt=true,
Shift=false,
Meta=false,
Key=(int)ExtendedVirtualKey.Number2
}
}
},
new ShellProfile
{
@@ -343,7 +413,20 @@ namespace FluentTerminal.App.Services.Implementation
Arguments = string.Empty,
Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true,
WorkingDirectory = string.Empty
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
KeyBindings = new []
{
new KeyBinding
{
Command = "e5785ad6-584f-40cb-bdcd-d5b3b3953e7f",
Ctrl=true,
Alt=true,
Shift=false,
Meta=false,
Key=(int)ExtendedVirtualKey.Number3
}
}
}
};
}
@@ -513,4 +596,4 @@ namespace FluentTerminal.App.Services.Implementation
return new[] { defaultXterm, powerShell, homebrew, tomorrow, ubuntu };
}
}
}
}
@@ -10,12 +10,14 @@ namespace FluentTerminal.App.Services.Implementation
private readonly Func<IShellProfileSelectionDialog> _shellProfileSelectionDialogFactory;
private readonly Func<IMessageDialog> _messageDialogFactory;
private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory;
private readonly Func<IInputDialog> _inputDialogFactory;
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory, Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory)
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory, Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory, Func<IInputDialog> inputDialogFactory)
{
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
_messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory;
}
public Task<KeyBinding> ShowCreateKeyBindingDialog()
@@ -54,6 +56,14 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.ShowAsync();
}
public Task<string> ShowInputDialogAsync(string title)
{
var dialog = _inputDialogFactory.Invoke();
dialog.SetTitle(title);
return dialog.GetInput();
}
public Task<ShellProfile> ShowProfileSelectionDialogAsync()
{
var dialog = _shellProfileSelectionDialogFactory();
@@ -1,38 +1,46 @@
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services.Implementation
{
public class KeyboardCommandService : IKeyboardCommandService
{
private readonly Dictionary<Command, Action> _commandHandlers = new Dictionary<Command, Action>();
public void RegisterCommandHandler(Command command, Action handler)
{
if (_commandHandlers.ContainsKey(command))
{
throw new InvalidOperationException("command already registered");
}
_commandHandlers[command] = handler ?? throw new ArgumentNullException(nameof(handler));
}
public void SendCommand(Command command)
{
if (_commandHandlers.TryGetValue(command, out Action handler))
{
handler.Invoke();
return;
}
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services.Implementation
{
public class KeyboardCommandService : IKeyboardCommandService
{
private readonly Dictionary<string, Action> _commandHandlers = new Dictionary<string, Action>();
public void RegisterCommandHandler(string command, Action handler)
{
if (_commandHandlers.ContainsKey(command))
{
throw new InvalidOperationException("command already registered");
}
_commandHandlers[command] = handler ?? throw new ArgumentNullException(nameof(handler));
}
public void DeregisterCommandHandler(string command)
{
if (_commandHandlers.ContainsKey(command))
{
_commandHandlers.Remove(command);
}
}
public void SendCommand(string command)
{
if (_commandHandlers.TryGetValue(command, out Action handler))
{
handler.Invoke();
return;
}
// already registered by SystemTray's ToggleWindowService
if (command == Command.ToggleWindow)
if (command == nameof(Command.ToggleWindow))
{
return;
}
throw new KeyNotFoundException($"No handler registered for command: {command}");
}
}
}
throw new KeyNotFoundException($"No handler registered for command: {command}");
}
}
}
@@ -2,9 +2,9 @@
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;
namespace FluentTerminal.SystemTray.Services
namespace FluentTerminal.App.Services.Implementation
{
public class NotificationService
public class NotificationService : INotificationService
{
public void ShowNotification(string title, string content, string url = null)
{
@@ -19,6 +19,7 @@ namespace FluentTerminal.App.Services.Implementation
private readonly IApplicationDataContainer _roamingSettings;
private readonly IApplicationDataContainer _shellProfiles;
private readonly IApplicationDataContainer _themes;
public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers)
{
_defaultValueProvider = defaultValueProvider;
@@ -48,19 +49,37 @@ namespace FluentTerminal.App.Services.Implementation
public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
public event EventHandler<TerminalTheme> ThemeAdded;
public event EventHandler<Guid> CurrentThemeChanged;
public event EventHandler<Guid> ThemeDeleted;
public event EventHandler KeyBindingsChanged;
public event EventHandler<ShellProfile> ShellProfileAdded;
public event EventHandler<Guid> ShellProfileDeleted;
public event EventHandler<TerminalOptions> TerminalOptionsChanged;
public void DeleteShellProfile(Guid id)
{
_shellProfiles.Delete(id.ToString());
ShellProfileDeleted?.Invoke(this, id);
}
public void DeleteTheme(Guid id)
{
_themes.Delete(id.ToString());
foreach (var profile in GetShellProfiles())
{
if (profile.TerminalThemeId == id)
{
profile.TerminalThemeId = Guid.Empty;
SaveShellProfile(profile);
}
}
ThemeDeleted?.Invoke(this, id);
}
public ApplicationSettings GetApplicationSettings()
@@ -117,14 +136,14 @@ namespace FluentTerminal.App.Services.Implementation
return _defaultValueProvider.GetDefaultShellProfileId();
}
public IDictionary<Command, ICollection<KeyBinding>> GetKeyBindings()
public IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings()
{
var keyBindings = new Dictionary<Command, ICollection<KeyBinding>>();
var keyBindings = new Dictionary<string, ICollection<KeyBinding>>();
foreach (Command command in Enum.GetValues(typeof(Command)))
{
keyBindings.Add(command, _keyBindings.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null) ?? _defaultValueProvider.GetDefaultKeyBindings(command));
keyBindings.Add(command.ToString(), _keyBindings.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null) ?? _defaultValueProvider.GetDefaultKeyBindings(command));
}
return keyBindings;
}
@@ -181,15 +200,28 @@ namespace FluentTerminal.App.Services.Implementation
_localSettings.SetValue(DefaultShellProfileKey, id);
}
public void SaveKeyBindings(Command command, ICollection<KeyBinding> keyBindings)
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
{
if (!Enum.IsDefined(typeof(Command), command))
{
throw new InvalidOperationException();
}
_keyBindings.WriteValueAsJson(command.ToString(), keyBindings);
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
}
public void SaveShellProfile(ShellProfile shellProfile)
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
{
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
if (newShell)
{
ShellProfileAdded?.Invoke(this, shellProfile);
}
}
public void SaveTerminalOptions(TerminalOptions terminalOptions)
@@ -198,7 +230,7 @@ namespace FluentTerminal.App.Services.Implementation
TerminalOptionsChanged?.Invoke(this, terminalOptions);
}
public void SaveTheme(TerminalTheme theme)
public void SaveTheme(TerminalTheme theme, bool newTheme = false)
{
_themes.WriteValueAsJson(theme.Id.ToString(), theme);
@@ -206,6 +238,11 @@ namespace FluentTerminal.App.Services.Implementation
{
CurrentThemeChanged?.Invoke(this, theme.Id);
}
if (newTheme)
{
ThemeAdded?.Invoke(this, theme);
}
}
}
}
@@ -14,7 +14,8 @@ namespace FluentTerminal.App.Services.Implementation
{
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private Dictionary<int, Action<string>> _terminalOutputHandlers;
private readonly Dictionary<int, Action<string>> _terminalOutputHandlers;
private int _nextTerminalId = 0;
public event EventHandler<int> TerminalExited;
@@ -24,12 +25,28 @@ namespace FluentTerminal.App.Services.Implementation
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
}
public async Task<CreateTerminalResponse> CreateTerminal(TerminalSize size, ShellProfile shellProfile)
public int GetNextTerminalId()
{
return _nextTerminalId++;
}
public async Task<GetAvailablePortResponse> GetAvailablePort()
{
var request = new GetAvailablePortRequest();
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
return JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
}
public async Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
{
var request = new CreateTerminalRequest
{
Id = id,
Size = size,
Profile = shellProfile
Profile = shellProfile,
SessionType = sessionType
};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
@@ -87,7 +104,7 @@ namespace FluentTerminal.App.Services.Implementation
public Task UpdateToggleWindowKeyBindings()
{
var keyBindings = _settingsService.GetKeyBindings()[Command.ToggleWindow];
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
var request = new SetToggleWindowKeyBindingsRequest
{
@@ -0,0 +1,56 @@
using System;
using Windows.ApplicationModel;
using Newtonsoft.Json;
using RestSharp;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Implementation
{
public class UpdateService : IUpdateService
{
private const string apiEndpoint = "https://api.github.com";
private readonly INotificationService _notificationService;
public UpdateService(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task CheckForUpdate(bool notifyNoUpdate = false)
{
var latest = await GetLatestVersionAsync();
if (latest > GetCurrentVersion())
{
_notificationService.ShowNotification("Update available",
"Click to open the releases page.", "https://github.com/felixse/FluentTerminal/releases");
}
else if (notifyNoUpdate)
{
_notificationService.ShowNotification("No update available", "You're up to date!");
}
}
public Version GetCurrentVersion()
{
var currentVersion = Package.Current.Id.Version;
return new Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build, currentVersion.Revision);
}
public async Task<Version> GetLatestVersionAsync()
{
var restClient = new RestClient(apiEndpoint);
var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET);
var restResponse = await restClient.ExecuteTaskAsync(restRequest);
if (restResponse.IsSuccessful)
{
dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content);
string tag = restResponseData[0].tag_name;
var latestVersion = new Version(tag);
return new Version(latestVersion.Major, latestVersion.Minor, latestVersion.Build, latestVersion.Revision);
}
return new Version(0, 0, 0, 0);
}
}
}
+126
View File
@@ -0,0 +1,126 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public class Terminal
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
Id = _trayProcessCommunicationService.GetNextTerminalId();
}
/// <summary>
/// To be observed by both view and viewmodel
/// </summary>
public event EventHandler Closed;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> KeyboardCommandReceived;
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<string> OutputReceived;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<TerminalSize> SizeChanged;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public int Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
{
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
Closed?.Invoke(this, System.EventArgs.Empty);
}
/// <summary>
/// to be called by viewmodel
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback();
}
/// <summary>
/// to be called by view
/// </summary>
public void ProcessKeyboardCommand(string command)
{
KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be set by view
/// </summary>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
{
_selectedTextCallback = selectedTextCallback;
}
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
SizeChanged?.Invoke(this, size);
}
/// <summary>
/// to be called by view
/// </summary>
public void SetTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// </summary>
/// <param name="shellProfile"></param>
/// <param name="size"></param>
/// <param name="sessionType"></param>
public async Task StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
{
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
}
}
public Task Write(string text)
{
return _trayProcessCommunicationService.WriteText(Id, text);
}
}
}
@@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="Fleck" Version="1.0.3" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
<PackageReference Include="ReactiveUI" Version="9.4.1" />
</ItemGroup>
<ItemGroup>
+72 -19
View File
@@ -34,30 +34,43 @@ namespace FluentTerminal.App.ViewModels
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
_settingsService.ShellProfileAdded += OnShellProfileAdded;
_settingsService.ShellProfileDeleted += OnShellProfileDeleted;
_trayProcessCommunicationService = trayProcessCommunicationService;
_dialogService = dialogService;
_applicationView = applicationView;
_dispatcherTimer = dispatcherTimer;
_clipboardService = clipboardService;
_keyboardCommandService = keyboardCommandService;
_keyboardCommandService.RegisterCommandHandler(Command.NewTab, () => AddTerminal(null, false));
_keyboardCommandService.RegisterCommandHandler(Command.ConfigurableNewTab, () => AddTerminal(null, true));
_keyboardCommandService.RegisterCommandHandler(Command.CloseTab, CloseCurrentTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), () => AddTerminal(null, false, Guid.Empty));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewTab), () => AddTerminal(null, true, Guid.Empty));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), () => SelectedTerminal.EditTitle());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
// Add all of the commands for switching to a tab of a given ID, if there's one open there
for (int i = 0; i < 9; i++)
{
Command switchCmd = Command.SwitchToTerm1 + i;
var switchCmd = Command.SwitchToTerm1 + i;
int tabNumber = i;
Action handler = () => SelectTabNumber(tabNumber);
_keyboardCommandService.RegisterCommandHandler(switchCmd, handler);
void handler() => SelectTabNumber(tabNumber);
_keyboardCommandService.RegisterCommandHandler(switchCmd.ToString(), handler);
}
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NextTab), SelectNextTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.PreviousTab), SelectPreviousTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), () => NewWindow(false));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewWindow), () => NewWindow(true));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ShowSettings), ShowSettings);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ToggleFullScreen), ToggleFullScreen);
foreach (ShellProfile profile in _settingsService.GetShellProfiles())
{
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), () => AddTerminal(profile.WorkingDirectory, false, profile.Id));
}
_keyboardCommandService.RegisterCommandHandler(Command.NextTab, SelectNextTab);
_keyboardCommandService.RegisterCommandHandler(Command.PreviousTab, SelectPreviousTab);
_keyboardCommandService.RegisterCommandHandler(Command.NewWindow, NewWindow);
_keyboardCommandService.RegisterCommandHandler(Command.ShowSettings, ShowSettings);
_keyboardCommandService.RegisterCommandHandler(Command.ToggleFullScreen, ToggleFullScreen);
var currentTheme = _settingsService.GetCurrentTheme();
var options = _settingsService.GetTerminalOptions();
Background = currentTheme.Colors.Background;
@@ -65,13 +78,24 @@ namespace FluentTerminal.App.ViewModels
_applicationSettings = _settingsService.GetApplicationSettings();
TabsPosition = _applicationSettings.TabsPosition;
AddTerminalCommand = new RelayCommand(() => AddTerminal(null, false));
AddTerminalCommand = new RelayCommand(() => AddTerminal(null, false, Guid.Empty));
ShowAboutCommand = new RelayCommand(ShowAbout);
ShowSettingsCommand = new RelayCommand(ShowSettings);
_applicationView.CloseRequested += OnCloseRequest;
Terminals.CollectionChanged += OnTerminalsCollectionChanged;
}
private void OnShellProfileDeleted(object sender, Guid e)
{
_keyboardCommandService.DeregisterCommandHandler(e.ToString());
}
private void OnShellProfileAdded(object sender, ShellProfile e)
{
_keyboardCommandService.RegisterCommandHandler(e.Id.ToString(), () => AddTerminal(e.WorkingDirectory, false, e.Id));
}
private void OnTerminalsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged(nameof(ShowTabsOnTop));
@@ -80,10 +104,12 @@ namespace FluentTerminal.App.ViewModels
public event EventHandler Closed;
public event EventHandler NewWindowRequested;
public event EventHandler<NewWindowRequestedEventArgs> NewWindowRequested;
public event EventHandler ShowSettingsRequested;
public event EventHandler ShowAboutRequested;
public RelayCommand AddTerminalCommand { get; }
public bool ShowTabsOnTop
@@ -128,6 +154,8 @@ namespace FluentTerminal.App.ViewModels
}
}
public RelayCommand ShowAboutCommand { get; }
public RelayCommand ShowSettingsCommand { get; }
public TabsPosition TabsPosition
@@ -138,24 +166,39 @@ namespace FluentTerminal.App.ViewModels
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
public Task AddTerminal(string startupDirectory, bool showProfileSelection)
/// <summary>
/// Add a new terminal window, either with a specified terminal profile, with the default, or by showing the profile selection dialog.
/// </summary>
/// <returns></returns>
public Task AddTerminal(string startupDirectory, bool showProfileSelection, Guid profileId)
{
return _applicationView.RunOnDispatcherThread(async () =>
{
ShellProfile profile = null;
if (showProfileSelection)
{
profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (profile == null)
{
if (Terminals.Count == 0)
{
Closed?.Invoke(this, EventArgs.Empty);
_applicationView.TryClose();
}
return;
}
}
else
else if (profileId == Guid.Empty)
{
profile = _settingsService.GetDefaultShellProfile();
}
else
{
profile = _settingsService.GetShellProfile(profileId);
}
var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService,
_applicationSettings, startupDirectory, profile, _applicationView, _dispatcherTimer, _clipboardService);
@@ -170,7 +213,7 @@ namespace FluentTerminal.App.ViewModels
{
foreach (var terminal in Terminals)
{
terminal.CloseView();
terminal.Close();
}
}
@@ -179,9 +222,14 @@ namespace FluentTerminal.App.ViewModels
SelectedTerminal?.CloseCommand.Execute(null);
}
private void NewWindow()
private void NewWindow(bool showProfileSelection)
{
NewWindowRequested?.Invoke(this, EventArgs.Empty);
var args = new NewWindowRequestedEventArgs
{
ShowProfileSelection = showProfileSelection
};
NewWindowRequested?.Invoke(this, args);
}
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
@@ -231,7 +279,7 @@ namespace FluentTerminal.App.ViewModels
{
if (sender is TerminalViewModel terminal)
{
terminal.CloseView();
terminal.Close();
if (SelectedTerminal == terminal)
{
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
@@ -276,6 +324,11 @@ namespace FluentTerminal.App.ViewModels
SelectedTerminal = Terminals[previousIndex];
}
private void ShowAbout()
{
ShowAboutRequested?.Invoke(this, EventArgs.Empty);
}
private void ShowSettings()
{
ShowSettingsRequested?.Invoke(this, EventArgs.Empty);
@@ -0,0 +1,79 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using GalaSoft.MvvmLight;
using System;
using System.Threading.Tasks;
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(ISettingsService settingsService, IUpdateService updateService, IApplicationView applicationView)
{
_settingsService = settingsService;
_updateService = updateService;
_applicationView = applicationView;
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdate(true));
}
public IAsyncCommand CheckForUpdatesCommand { get; }
public string CurrentVersion
{
get
{
var version = _updateService.GetCurrentVersion();
return ConvertVersionToString(version);
}
}
public string CurrentVersionReleaseNotesURL => BaseUrl + CurrentVersion;
public string LatestVersion
{
get => _latestVersion;
set
{
if (Set(ref _latestVersion, value))
{
RaisePropertyChanged(nameof(LatestVersionFound));
RaisePropertyChanged(nameof(LatestVersionLoading));
RaisePropertyChanged(nameof(LatestVersionNotFound));
RaisePropertyChanged(nameof(LatestVersionReleaseNotesURL));
}
}
}
public bool LatestVersionFound => !LatestVersionNotFound && !LatestVersionLoading;
public bool LatestVersionLoading => LatestVersion == null;
public bool LatestVersionNotFound => LatestVersion == "0.0.0.0";
public string LatestVersionReleaseNotesURL => BaseUrl + LatestVersion;
public Task OnNavigatedTo()
{
return CheckForUpdate(false);
}
public async Task CheckForUpdate(bool notifyNoUpdate)
{
var version = ConvertVersionToString(await _updateService.GetLatestVersionAsync());
await _applicationView.RunOnDispatcherThread(() => LatestVersion = version);
await _updateService.CheckForUpdate(notifyNoUpdate);
}
private string ConvertVersionToString(Version version)
{
return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
}
}
}
@@ -19,6 +19,7 @@ namespace FluentTerminal.App.ViewModels.Settings
private bool _editingTabsPosition;
private bool _editingInactiveTabColorMode;
private bool _startupTaskEnabled;
private bool _shouldRestartForTrayMessage;
private string _startupTaskErrorMessage;
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IStartupTaskService startupTaskService)
@@ -53,6 +54,43 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool AlwaysUseWinPty
{
get => _applicationSettings.AlwaysUseWinPty;
set
{
if (_applicationSettings.AlwaysUseWinPty != value)
{
_applicationSettings.AlwaysUseWinPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool EnableTrayIcon
{
get => _applicationSettings.EnableTrayIcon;
set
{
if (_applicationSettings.EnableTrayIcon != value)
{
_applicationSettings.EnableTrayIcon = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
// Toggle message telling user to restart the program
ShouldRestartForTrayMessage = !ShouldRestartForTrayMessage;
}
}
}
public bool ShouldRestartForTrayMessage
{
get => _shouldRestartForTrayMessage;
set => Set(ref _shouldRestartForTrayMessage, value);
}
public bool ShowNewOutputIndicator
{
get => _applicationSettings.ShowNewOutputIndicator;
@@ -240,7 +278,9 @@ namespace FluentTerminal.App.ViewModels.Settings
InactiveTabColorMode = defaults.InactiveTabColorMode;
NewTerminalLocation = defaults.NewTerminalLocation;
AlwaysShowTabs = defaults.AlwaysShowTabs;
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon;
}
}
@@ -1,7 +1,7 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Threading.Tasks;
@@ -11,18 +11,21 @@ namespace FluentTerminal.App.ViewModels.Settings
{
private readonly IDialogService _dialogService;
public KeyBindingViewModel(KeyBinding keyBinding, IDialogService dialogService)
public KeyBindingViewModel(KeyBinding keyBinding, IDialogService dialogService, KeyBindingsViewModel parent)
{
Model = keyBinding;
Parent = parent;
_dialogService = dialogService;
EditCommand = new RelayCommand(async () => await Edit().ConfigureAwait(false));
DeleteCommand = new RelayCommand(async () => await Delete().ConfigureAwait(false));
EditCommand = new AsyncCommand(Edit);
DeleteCommand = new AsyncCommand(Delete);
}
public event EventHandler Deleted;
public event EventHandler Edited;
public KeyBindingsViewModel Parent { get; }
public bool Meta
{
get => Model.Meta;
@@ -62,8 +65,8 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public RelayCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public IAsyncCommand EditCommand { get; }
public int Key
{
@@ -1,4 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
@@ -17,7 +18,7 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private IDictionary<Command, ICollection<KeyBinding>> _keyBindings;
private IDictionary<string, ICollection<KeyBinding>> _keyBindings;
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, ITrayProcessCommunicationService trayProcessCommunicationService)
{
@@ -26,19 +27,19 @@ namespace FluentTerminal.App.ViewModels.Settings
_defaultValueProvider = defaultValueProvider;
_trayProcessCommunicationService = trayProcessCommunicationService;
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
AddCommand = new RelayCommand<Command>(async command => await Add(command).ConfigureAwait(false));
AddCommand = new RelayCommand<string>(async command => await Add(command).ConfigureAwait(false));
Initialize(_settingsService.GetKeyBindings());
Initialize(_settingsService.GetCommandKeyBindings());
}
public RelayCommand<Command> AddCommand { get; }
public RelayCommand<string> AddCommand { get; }
public ObservableCollection<KeyBindingsViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingsViewModel>();
public RelayCommand RestoreDefaultsCommand { get; }
private async Task Add(Command command)
private async Task Add(string command)
{
var keyBinding = KeyBindings.FirstOrDefault(k => k.Command == command);
await keyBinding?.Add();
await keyBinding?.ShowAddKeyBindingDialog();
}
private void ClearKeyBindings()
@@ -51,7 +52,7 @@ namespace FluentTerminal.App.ViewModels.Settings
KeyBindings.Clear();
}
private void Initialize(IDictionary<Command, ICollection<KeyBinding>> keyBindings)
private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings)
{
_keyBindings = keyBindings;
@@ -60,13 +61,17 @@ namespace FluentTerminal.App.ViewModels.Settings
foreach (var value in Enum.GetValues(typeof(Command)))
{
var command = (Command)value;
var viewModel = new KeyBindingsViewModel(command, _keyBindings[command], _dialogService);
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, EnumHelper.GetEnumDescription(command), true);
foreach (var keyBinding in _keyBindings[command.ToString()])
{
viewModel.Add(keyBinding);
}
viewModel.Edited += OnEdited;
KeyBindings.Add(viewModel);
}
}
private void OnEdited(Command command, ICollection<KeyBinding> keyBindings)
private void OnEdited(string command, ICollection<KeyBinding> keyBindings)
{
_settingsService.SaveKeyBindings(command, keyBindings);
_trayProcessCommunicationService.UpdateToggleWindowKeyBindings();
@@ -79,7 +84,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (result == DialogButton.OK)
{
_settingsService.ResetKeyBindings();
Initialize(_settingsService.GetKeyBindings());
Initialize(_settingsService.GetCommandKeyBindings());
}
}
}
@@ -1,11 +1,10 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
@@ -13,62 +12,73 @@ namespace FluentTerminal.App.ViewModels.Settings
public class KeyBindingsViewModel : ViewModelBase
{
private readonly IDialogService _dialogService;
private readonly ICollection<KeyBinding> _keyBindings;
private bool _editable;
public KeyBindingsViewModel(Command command, ICollection<KeyBinding> keyBindings, IDialogService dialogService)
public KeyBindingsViewModel(string command, IDialogService dialogService, string commandName, bool editable)
{
Command = command;
_keyBindings = keyBindings;
_dialogService = dialogService;
CommandName = EnumHelper.GetEnumDescription(command);
foreach (var binding in keyBindings)
{
var viewModel = new KeyBindingViewModel(binding, _dialogService);
viewModel.Deleted += ViewModel_Deleted;
viewModel.Edited += ViewModel_Edited;
KeyBindings.Add(viewModel);
}
Command = command;
CommandName = commandName;
Editable = editable;
}
public delegate void EditedEvent(Command command, ICollection<KeyBinding> keyBindings);
public delegate void EditedEvent(string command, ICollection<KeyBinding> keyBindings);
public event EditedEvent Edited;
public Command Command { get; }
public string Command { get; }
public string CommandName { get; }
/// <summary>
/// Whether or not the "Edit" and "Delete" links appear next to the key binding list.
/// </summary>
public bool Editable
{
get => _editable;
set => Set(ref _editable, value);
}
public ObservableCollection<KeyBindingViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingViewModel>();
public async Task Add()
public async Task ShowAddKeyBindingDialog()
{
var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService);
var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService, this);
if (await newKeyBinding.Edit().ConfigureAwait(true))
{
newKeyBinding.Deleted += ViewModel_Deleted;
newKeyBinding.Edited += ViewModel_Edited;
KeyBindings.Add(newKeyBinding);
_keyBindings.Add(newKeyBinding.Model);
Edited?.Invoke(Command, _keyBindings);
Add(newKeyBinding.Model);
}
}
public void Clear()
{
KeyBindings.Clear();
}
public void Add(KeyBinding keyBinding)
{
var viewModel = new KeyBindingViewModel(keyBinding, _dialogService, this);
viewModel.Deleted += ViewModel_Deleted;
viewModel.Edited += ViewModel_Edited;
KeyBindings.Add(viewModel);
Edited?.Invoke(Command, KeyBindings.Select(x => x.Model).ToList());
}
private void ViewModel_Deleted(object sender, EventArgs e)
{
if (sender is KeyBindingViewModel keyBinding)
{
_keyBindings.Remove(keyBinding.Model);
KeyBindings.Remove(keyBinding);
Edited?.Invoke(Command, _keyBindings);
Edited?.Invoke(Command, KeyBindings.Select(x => x.Model).ToList());
}
}
private void ViewModel_Edited(object sender, EventArgs e)
{
Edited?.Invoke(Command, _keyBindings);
Edited?.Invoke(Command, KeyBindings.Select(x => x.Model).ToList());
}
}
}
@@ -4,6 +4,7 @@ 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
@@ -15,20 +16,23 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private ShellProfileViewModel _selectedShellProfile;
private readonly IApplicationView _applicationView;
public ProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IFileSystemService fileSystemService)
public ProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IFileSystemService fileSystemService, IApplicationView applicationView)
{
_settingsService = settingsService;
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
CreateShellProfileCommand = new RelayCommand(CreateShellProfile);
CloneCommand = new RelayCommand<ShellProfileViewModel>(Clone);
var defaultShellProfileId = _settingsService.GetDefaultShellProfileId();
foreach (var shellProfile in _settingsService.GetShellProfiles())
{
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService);
var viewModel = new ShellProfileViewModel(shellProfile, settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider, false);
viewModel.Deleted += OnShellProfileDeleted;
viewModel.SetAsDefault += OnShellProfileSetAsDefault;
@@ -40,6 +44,7 @@ namespace FluentTerminal.App.ViewModels.Settings
}
SelectedShellProfile = ShellProfiles.First(p => p.IsDefault);
}
private void OnShellProfileSetAsDefault(object sender, EventArgs e)
@@ -76,18 +81,38 @@ namespace FluentTerminal.App.ViewModels.Settings
public RelayCommand CreateShellProfileCommand { get; }
public RelayCommand<ShellProfileViewModel> CloneCommand { get; }
private void CreateShellProfile()
{
var shellProfile = new ShellProfile
{
Id = Guid.NewGuid(),
PreInstalled = false,
Name = "New profile"
Name = "New profile",
KeyBindings = new List<KeyBinding>()
};
_settingsService.SaveShellProfile(shellProfile);
AddShellProfile(shellProfile);
}
var viewModel = new ShellProfileViewModel(shellProfile, _settingsService, _dialogService, _fileSystemService);
private void Clone(ShellProfileViewModel shellProfile)
{
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;
viewModel.Deleted += OnShellProfileDeleted;
@@ -1,15 +1,17 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
{
public class TerminalPageViewModel : ViewModelBase
public class TerminalPageViewModel : ReactiveObject
{
private readonly TerminalOptions _terminalOptions;
private readonly ISettingsService _settingsService;
@@ -63,7 +65,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.CursorBlink = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
this.RaisePropertyChanged();
}
}
}
@@ -77,7 +79,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontFamily = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
this.RaisePropertyChanged();
}
}
}
@@ -90,8 +92,20 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_terminalOptions.BackgroundOpacity != value)
{
_terminalOptions.BackgroundOpacity = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
this.RaisePropertyChanged();
}
}
}
public int Padding
{
get => _terminalOptions.Padding;
set
{
if (_terminalOptions.Padding != value)
{
_terminalOptions.Padding = value;
this.RaisePropertyChanged();
}
}
}
@@ -112,12 +126,26 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.ScrollBackLimit = intValue;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
this.RaisePropertyChanged();
}
}
else
{
RaisePropertyChanged();
this.RaisePropertyChanged();
}
}
}
public bool BoldText
{
get => _terminalOptions.BoldText;
set
{
if (_terminalOptions.BoldText != value)
{
_terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
}
}
}
@@ -133,7 +161,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontSize = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
this.RaisePropertyChanged();
}
}
}
@@ -152,10 +180,10 @@ namespace FluentTerminal.App.ViewModels.Settings
_isEditingCursorStyle = true;
_terminalOptions.CursorStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
RaisePropertyChanged(nameof(BlockIsSelected));
RaisePropertyChanged(nameof(BarIsSelected));
RaisePropertyChanged(nameof(UnderlineIsSelected));
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(BlockIsSelected));
this.RaisePropertyChanged(nameof(BarIsSelected));
this.RaisePropertyChanged(nameof(UnderlineIsSelected));
_isEditingCursorStyle = false;
}
}
@@ -171,10 +199,10 @@ namespace FluentTerminal.App.ViewModels.Settings
_isEditingScrollBarStyle = true;
_terminalOptions.ScrollBarStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
RaisePropertyChanged(nameof(HiddenIsSelected));
RaisePropertyChanged(nameof(AutoHidingIsSelected));
RaisePropertyChanged(nameof(VisibleIsSelected));
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(HiddenIsSelected));
this.RaisePropertyChanged(nameof(AutoHidingIsSelected));
this.RaisePropertyChanged(nameof(VisibleIsSelected));
_isEditingScrollBarStyle = false;
}
}
@@ -192,6 +220,7 @@ namespace FluentTerminal.App.ViewModels.Settings
ScrollBarStyle = defaults.ScrollBarStyle;
FontFamily = defaults.FontFamily;
FontSize = defaults.FontSize;
BoldText = defaults.BoldText;
BackgroundOpacity = defaults.BackgroundOpacity;
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
}
@@ -209,6 +238,15 @@ namespace FluentTerminal.App.ViewModels.Settings
Sizes = Enumerable.Range(1, 72);
_terminalOptions = _settingsService.GetTerminalOptions();
this.ObservableForProperty(x => x.BackgroundOpacity).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
{
_settingsService.SaveTerminalOptions(_terminalOptions);
});
this.ObservableForProperty(x => x.Padding).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
{
_settingsService.SaveTerminalOptions(_terminalOptions);
});
}
}
}
@@ -31,6 +31,7 @@ namespace FluentTerminal.App.ViewModels.Settings
CreateThemeCommand = new RelayCommand(CreateTheme);
ImportThemeCommand = new RelayCommand(ImportTheme);
CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme);
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
@@ -39,7 +40,7 @@ namespace FluentTerminal.App.ViewModels.Settings
var activeThemeId = _settingsService.GetCurrentThemeId();
foreach (var theme in _settingsService.GetThemes())
{
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService, false);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
@@ -55,6 +56,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public RelayCommand CreateThemeCommand { get; }
public RelayCommand ImportThemeCommand { get; }
public RelayCommand<ThemeViewModel> CloneCommand { get; set; }
public double BackgroundOpacity
{
@@ -81,6 +83,18 @@ namespace FluentTerminal.App.ViewModels.Settings
public ObservableCollection<ThemeViewModel> Themes { get; } = new ObservableCollection<ThemeViewModel>();
private void CloneTheme(ThemeViewModel theme)
{
var cloned = new TerminalTheme(theme.Model)
{
Id = Guid.NewGuid(),
PreInstalled = false,
Name = $"Copy of {theme.Name}"
};
AddTheme(cloned);
}
private void CreateTheme()
{
var defaultTheme = _settingsService.GetTheme(_defaultValueProvider.GetDefaultThemeId());
@@ -92,13 +106,7 @@ namespace FluentTerminal.App.ViewModels.Settings
Colors = new TerminalColors(defaultTheme.Colors)
};
_settingsService.SaveTheme(theme);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
Themes.Add(viewModel);
SelectedTheme = viewModel;
AddTheme(theme);
}
private async void ImportTheme()
@@ -118,13 +126,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
var theme = await parser.Parse(file.Name, file.Content).ConfigureAwait(true);
_settingsService.SaveTheme(theme);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
Themes.Add(viewModel);
SelectedTheme = viewModel;
AddTheme(theme);
}
catch (Exception exception)
{
@@ -133,6 +135,18 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
private void AddTheme(TerminalTheme theme)
{
_settingsService.SaveTheme(theme, true);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, true);
viewModel.EditCommand.Execute(null);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
Themes.Add(viewModel);
SelectedTheme = viewModel;
}
private void OnThemeActivated(object sender, EventArgs e)
{
if (sender is ThemeViewModel activatedTheme)
@@ -8,17 +8,20 @@ namespace FluentTerminal.App.ViewModels
public class SettingsViewModel : ViewModelBase
{
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService, IFileSystemService fileSystemService, IStartupTaskService startupTaskService)
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView 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);
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);
}
public event EventHandler Closed;
public event EventHandler AboutPageRequested;
public GeneralPageViewModel General { get; }
public KeyBindingsPageViewModel KeyBindings { get; }
@@ -26,6 +29,12 @@ namespace FluentTerminal.App.ViewModels
public TerminalPageViewModel Terminal { get; }
public ThemesPageViewModel Themes { get; }
public MousePageViewModel Mouse { get; }
public AboutPageViewModel About { get; }
public void NavigateToAboutPage()
{
AboutPageRequested?.Invoke(this, EventArgs.Empty);
}
public void Close()
{
@@ -1,6 +1,8 @@
using FluentTerminal.App.Services;
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;
@@ -15,29 +17,65 @@ namespace FluentTerminal.App.ViewModels
private readonly IDialogService _dialogService;
private readonly IFileSystemService _fileSystemService;
private readonly ISettingsService _settingsService;
private readonly ShellProfile _shellProfile;
private string _arguments;
private string _fallbackArguments;
private string _fallbackLocation;
private string _fallbackName;
private int _fallbackTabThemeId;
private string _fallbackWorkingDirectory;
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;
private bool _editingLineEndingStyle;
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService)
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider, Boolean isNew)
{
_shellProfile = shellProfile;
Model = shellProfile;
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_defaultValueProvider = defaultValueProvider;
_isNew = isNew;
_settingsService.ThemeAdded += OnThemeAdded;
_settingsService.ThemeDeleted += OnThemeDeleted;
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;
@@ -45,14 +83,37 @@ namespace FluentTerminal.App.ViewModels
WorkingDirectory = shellProfile.WorkingDirectory;
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == shellProfile.TabThemeId);
PreInstalled = shellProfile.PreInstalled;
LineEndingStyle = shellProfile.LineEndingTranslation;
SetDefaultCommand = new RelayCommand(SetDefault);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
SaveChangesCommand = new RelayCommand(SaveChanges);
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
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;
@@ -65,10 +126,14 @@ namespace FluentTerminal.App.ViewModels
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 bool PreInstalled { get; }
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ShellProfile Model { get; private set; }
public bool PreInstalled { get; private set; }
public string Arguments
{
@@ -76,7 +141,7 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _arguments, value);
}
public Guid Id { get; }
public Guid Id { get; private set; }
public bool InEditMode
{
@@ -114,23 +179,97 @@ namespace FluentTerminal.App.ViewModels
}
}
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
{
if (_lineEndingStyle != value && !_editingLineEndingStyle)
{
_editingLineEndingStyle = true;
_lineEndingStyle = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(DoNotModifyIsSelected));
RaisePropertyChanged(nameof(ToCRLFIsSelected));
RaisePropertyChanged(nameof(ToLFIsSelected));
_editingLineEndingStyle = false;
}
}
}
public bool DoNotModifyIsSelected
{
get => LineEndingStyle == LineEndingStyle.DoNotModify;
set => LineEndingStyle = LineEndingStyle.DoNotModify;
}
public bool ToCRLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCRLF;
set => LineEndingStyle = LineEndingStyle.ToCRLF;
}
public bool ToLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToLF;
set => LineEndingStyle = LineEndingStyle.ToLF;
}
public void SaveChanges()
{
_shellProfile.Arguments = Arguments;
_shellProfile.Location = Location;
_shellProfile.Name = Name;
_shellProfile.WorkingDirectory = WorkingDirectory;
_shellProfile.TabThemeId = SelectedTabTheme.Id;
_settingsService.SaveShellProfile(_shellProfile);
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 || !PreInstalled)
{
throw new InvalidOperationException();
}
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)
{
Model = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id == Model.Id);
InitializeViewModelProperties(Model);
_settingsService.SaveShellProfile(Model);
}
}
public Task AddKeyboardShortcut()
{
return KeyBindings.ShowAddKeyBindingDialog();
}
private async Task BrowseForCustomShell()
@@ -153,23 +292,59 @@ namespace FluentTerminal.App.ViewModels
private async Task CancelEdit()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
if (_isNew)
{
Arguments = _fallbackArguments;
Location = _fallbackLocation;
Name = _fallbackName;
WorkingDirectory = _fallbackWorkingDirectory;
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackTabThemeId);
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()
};
InEditMode = false;
if (!_fallbackProfile.Equals(changedProfile))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Arguments = _fallbackProfile.Arguments;
Location = _fallbackProfile.Location;
Name = _fallbackProfile.Name;
WorkingDirectory = _fallbackProfile.WorkingDirectory;
LineEndingStyle = _fallbackProfile.LineEndingTranslation;
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TerminalThemeId);
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == _fallbackProfile.TabThemeId);
KeyBindings.KeyBindings.Clear();
foreach (var keyBinding in Model.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
}
}
private bool CanDelete()
{
return !_shellProfile.PreInstalled;
return !Model.PreInstalled;
}
private async Task Delete()
@@ -184,11 +359,18 @@ namespace FluentTerminal.App.ViewModels
private void Edit()
{
_fallbackArguments = _shellProfile.Arguments;
_fallbackLocation = _shellProfile.Location;
_fallbackName = _shellProfile.Name;
_fallbackWorkingDirectory = _shellProfile.WorkingDirectory;
_fallbackTabThemeId = _shellProfile.TabThemeId;
// todo write copy ctor
_fallbackProfile = new ShellProfile
{
Arguments = Model.Arguments,
Location = Model.Location,
Name = Model.Name,
WorkingDirectory = Model.WorkingDirectory,
TabThemeId = Model.TabThemeId,
TerminalThemeId = Model.TerminalThemeId
};
KeyBindings.Editable = true;
InEditMode = true;
}
+169 -188
View File
@@ -1,30 +1,20 @@
using Fleck;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public class TerminalViewModel : ViewModelBase
{
private readonly IApplicationView _applicationView;
private readonly IClipboardService _clipboardService;
private readonly ManualResetEventSlim _connectedEvent;
private readonly IDialogService _dialogService;
private readonly IKeyboardCommandService _keyboardCommandService;
private readonly IDispatcherTimer _resizeOverlayTimer;
private readonly ISettingsService _settingsService;
private readonly ShellProfile _shellProfile;
private readonly string _startupDirectory;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private bool _isSelected;
private bool _newOutput;
private string _resizeOverlayContent;
@@ -32,75 +22,94 @@ namespace FluentTerminal.App.ViewModels
private bool _showResizeOverlay;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private int _terminalId;
private ITerminalView _terminalView;
private TerminalTheme _terminalTheme;
private string _title;
private IWebSocketConnection _webSocket;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
{
Title = DefaultTitle;
SettingsService = settingsService;
SettingsService.CurrentThemeChanged += OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged += OnKeyBindingsChanged;
_connectedEvent = new ManualResetEventSlim(false);
TrayProcessCommunicationService = trayProcessCommunicationService;
_settingsService = settingsService;
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_settingsService.KeyBindingsChanged += OnKeyBindingsChanged;
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
_dialogService = dialogService;
DialogService = dialogService;
_keyboardCommandService = keyboardCommandService;
ApplicationSettings = applicationSettings;
_startupDirectory = startupDirectory;
_shellProfile = shellProfile;
_applicationView = applicationView;
_clipboardService = clipboardService;
StartupDirectory = startupDirectory;
ApplicationView = applicationView;
ClipboardService = clipboardService;
TabThemes = new ObservableCollection<TabTheme>(_settingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == _shellProfile.TabThemeId);
ShellProfile = shellProfile;
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? SettingsService.GetCurrentTheme() : SettingsService.GetTheme(shellProfile.TerminalThemeId);
TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
_resizeOverlayTimer = dispatcherTimer;
_resizeOverlayTimer.Interval = new TimeSpan(0, 0, 2);
_resizeOverlayTimer.Tick += OnResizeOverlayTimerFinished;
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
FindNextCommand = new RelayCommand(async () => await FindNext().ConfigureAwait(false));
FindPreviousCommand = new RelayCommand(async () => await FindPrevious().ConfigureAwait(false));
FindNextCommand = new RelayCommand(FindNext);
FindPreviousCommand = new RelayCommand(FindPrevious);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
EditTitleCommand = new AsyncCommand(EditTitle);
if (!string.IsNullOrWhiteSpace(StartupDirectory))
{
ShellProfile.WorkingDirectory = StartupDirectory;
}
Terminal = new Terminal(TrayProcessCommunicationService);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
Terminal.OutputReceived += Terminal_OutputReceived;
Terminal.SizeChanged += Terminal_SizeChanged;
Terminal.TitleChanged += Terminal_TitleChanged;
Terminal.Closed += Terminal_Closed;
}
public event EventHandler Activated;
public event EventHandler Closed;
public event EventHandler<string> FindNextRequested;
public event EventHandler<string> FindPreviousRequested;
public event EventHandler KeyBindingsChanged;
public event EventHandler<TerminalOptions> OptionsChanged;
public event EventHandler SearchStarted;
public event EventHandler<TerminalTheme> ThemeChanged;
public ApplicationSettings ApplicationSettings { get; private set; }
public IApplicationView ApplicationView { get; }
public TabTheme BackgroundTabTheme
{
// The effective background theme depends on whether it is selected (use the theme), or if it is inactive
// (if we're set to underline inactive tabs, use the null theme).
get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ?
_tabTheme :
_settingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
}
public IClipboardService ClipboardService { get; }
public RelayCommand CloseCommand { get; }
public RelayCommand CloseSearchPanelCommand { get; }
public string DefaultTitle { get; private set; } = string.Empty;
public IDialogService DialogService { get; }
public IAsyncCommand EditTitleCommand { get; }
public RelayCommand FindNextCommand { get; }
public RelayCommand FindPreviousCommand { get; }
public bool Initialized { get; private set; }
public bool IsSelected
{
get => _isSelected;
@@ -110,8 +119,8 @@ namespace FluentTerminal.App.ViewModels
{
if (IsSelected)
{
_applicationView.Title = Title;
_applicationView.RunOnDispatcherThread(() => NewOutput = false);
ApplicationView.Title = Title ?? string.Empty;
NewOutput = false;
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
@@ -142,6 +151,10 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand<string> SelectTabThemeCommand { get; }
public ISettingsService SettingsService { get; }
public ShellProfile ShellProfile { get; }
public bool ShowResizeOverlay
{
get => _showResizeOverlay;
@@ -165,6 +178,8 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _showSearchPanel, value);
}
public string StartupDirectory { get; }
public TabTheme TabTheme
{
get => _tabTheme;
@@ -178,118 +193,72 @@ namespace FluentTerminal.App.ViewModels
public ObservableCollection<TabTheme> TabThemes { get; }
public Terminal Terminal { get; private set; }
public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}
public string Title
{
get => _title;
set
{
value = string.IsNullOrWhiteSpace(value) ? DefaultTitle : value;
if (Set(ref _title, value) && IsSelected)
{
_applicationView.Title = Title;
ApplicationView.Title = Title;
}
}
}
public async Task CloseView()
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task Close()
{
_terminalView.Close();
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
return Terminal.Close();
}
public void CopyText(string text)
{
_clipboardService.SetText(text);
ClipboardService.SetText(text);
}
public Task FocusTerminal()
public async Task EditTitle()
{
return _terminalView?.FocusTerminal();
var result = await DialogService.ShowInputDialogAsync("Edit Title");
if (result != null)
{
Title = result;
}
}
public async Task OnViewIsReady(ITerminalView terminalView)
public void FocusTerminal()
{
_terminalView = terminalView;
var options = _settingsService.GetTerminalOptions();
var theme = _settingsService.GetCurrentTheme();
var keyBindings = _settingsService.GetKeyBindings();
var size = await _terminalView.CreateTerminal(options, theme.Colors, FlattenKeyBindings(keyBindings)).ConfigureAwait(true);
if (!string.IsNullOrWhiteSpace(_startupDirectory))
{
_shellProfile.WorkingDirectory = _startupDirectory;
}
var response = await _trayProcessCommunicationService.CreateTerminal(size, _shellProfile).ConfigureAwait(true);
if (response.Success)
{
_terminalId = response.Id;
_terminalView.TerminalSizeChanged += OnTerminalSizeChanged;
_terminalView.TerminalTitleChanged += OnTerminalTitleChanged;
_terminalView.KeyboardCommandReceived += OnKeyboardCommandReceived;
_trayProcessCommunicationService.SubscribeForTerminalOutput(_terminalId, t =>
{
_connectedEvent.Wait();
_webSocket.Send(t);
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
_applicationView.RunOnDispatcherThread(() => NewOutput = true);
}
});
DefaultTitle = response.ShellExecutableName;
Title = DefaultTitle;
var webSocketUrl = "ws://127.0.0.1:" + response.Id;
var webSocketServer = new WebSocketServer(webSocketUrl);
webSocketServer.Start(socket =>
{
_webSocket = socket;
socket.OnOpen = () => _connectedEvent.Set();
socket.OnMessage = message => _trayProcessCommunicationService.WriteText(_terminalId, message);
});
await _terminalView.ConnectToSocket(webSocketUrl).ConfigureAwait(true);
Initialized = true;
}
else
{
await _dialogService.ShowMessageDialogAsnyc("Error", response.Error, DialogButton.OK).ConfigureAwait(true);
}
await FocusTerminal().ConfigureAwait(true);
Activated?.Invoke(this, EventArgs.Empty);
}
private void CloseSearchPanel()
{
SearchText = string.Empty;
ShowSearchPanel = false;
_terminalView.FocusTerminal();
FocusTerminal();
}
private Task FindNext()
private void FindNext()
{
return _terminalView.FindNext(SearchText);
FindNextRequested?.Invoke(this, SearchText);
}
private Task FindPrevious()
private void FindPrevious()
{
return _terminalView.FindPrevious(SearchText);
}
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<Command, ICollection<KeyBinding>> keyBindings)
{
return keyBindings.Values.SelectMany(k => k);
FindPreviousRequested?.Invoke(this, SearchText);
}
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
await _applicationView.RunOnDispatcherThread(() =>
await ApplicationView.RunOnDispatcherThread(() =>
{
ApplicationSettings = e;
RaisePropertyChanged(nameof(IsUnderlined));
@@ -299,52 +268,21 @@ namespace FluentTerminal.App.ViewModels
private async void OnCurrentThemeChanged(object sender, Guid e)
{
await _applicationView.RunOnDispatcherThread(async () =>
await ApplicationView.RunOnDispatcherThread(() =>
{
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
var currentTheme = _settingsService.GetTheme(e);
await _terminalView.ChangeTheme(currentTheme.Colors).ConfigureAwait(true);
// only change theme if not overwritten by profile
if (ShellProfile.TerminalThemeId == Guid.Empty)
{
var currentTheme = SettingsService.GetTheme(e);
TerminalTheme = currentTheme;
ThemeChanged?.Invoke(this, currentTheme);
}
});
}
private async void OnKeyBindingsChanged(object sender, EventArgs e)
private void OnKeyBindingsChanged(object sender, EventArgs e)
{
var keyBindings = _settingsService.GetKeyBindings();
await _applicationView.RunOnDispatcherThread(async () =>
{
await _terminalView.ChangeKeyBindings(FlattenKeyBindings(keyBindings)).ConfigureAwait(false);
});
}
private async void OnKeyboardCommandReceived(object sender, Command e)
{
if (e == Command.Copy)
{
var selection = await _terminalView.GetSelection().ConfigureAwait(true);
_clipboardService.SetText(selection);
}
else if (e == Command.Paste)
{
var content = await _clipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
await _trayProcessCommunicationService.WriteText(_terminalId, content).ConfigureAwait(true);
}
}
else if (e == Command.Search)
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
_terminalView.FocusSearchTextBox();
}
}
else
{
_keyboardCommandService.SendCommand(e);
}
KeyBindingsChanged?.Invoke(this, EventArgs.Empty);
}
private void OnResizeOverlayTimerFinished(object sender, object e)
@@ -353,36 +291,9 @@ namespace FluentTerminal.App.ViewModels
ShowResizeOverlay = false;
}
private void OnTerminalExited(object sender, int e)
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
if (e == _terminalId)
{
_applicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
}
}
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
await _applicationView.RunOnDispatcherThread(async () =>
{
await _terminalView.ChangeOptions(e).ConfigureAwait(true);
});
}
private async void OnTerminalSizeChanged(object sender, TerminalSize e)
{
if (!Initialized)
{
return;
}
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
ShowResizeOverlay = true;
await _trayProcessCommunicationService.ResizeTerminal(_terminalId, e).ConfigureAwait(true);
}
private void OnTerminalTitleChanged(object sender, string e)
{
Title = e;
OptionsChanged?.Invoke(this, e);
}
private void SelectTabTheme(string name)
@@ -390,11 +301,82 @@ namespace FluentTerminal.App.ViewModels
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
}
private void Terminal_Closed(object sender, EventArgs e)
{
Closed?.Invoke(this, EventArgs.Empty);
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
{
switch (e)
{
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
ClipboardService.SetText(selection);
break;
}
case nameof(Command.Paste):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(content).ConfigureAwait(true);
}
break;
}
case nameof(Command.PasteWithoutNewlines):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await Terminal.Write(content).ConfigureAwait(true);
}
break;
}
case nameof(Command.Search):
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
SearchStarted?.Invoke(this, EventArgs.Empty);
}
break;
}
default:
{
_keyboardCommandService.SendCommand(e);
break;
}
}
}
private void Terminal_OutputReceived(object sender, string e)
{
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
NewOutput = true;
}
}
private void Terminal_SizeChanged(object sender, TerminalSize e)
{
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
ShowResizeOverlay = true;
}
private void Terminal_TitleChanged(object sender, string e)
{
Title = e;
}
private async Task TryClose()
{
if (ApplicationSettings.ConfirmClosingTabs)
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await DialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.Cancel)
{
@@ -402,8 +384,7 @@ namespace FluentTerminal.App.ViewModels
}
}
await _trayProcessCommunicationService.CloseTerminal(_terminalId).ConfigureAwait(true);
Closed?.Invoke(this, EventArgs.Empty);
await Close().ConfigureAwait(true);
}
}
}
+132 -86
View File
@@ -29,58 +29,57 @@ namespace FluentTerminal.App.ViewModels
private string _cursor;
private string _cursorAccent;
private string _cyan;
private string _fallBackAuthor;
private TerminalColors _fallBackColors;
private string _fallBackName;
private TerminalTheme _fallbackTheme;
private string _foreground;
private string _green;
private bool _inEditMode;
private bool _isActive;
private string _magenta;
private string _name;
private bool _isNew;
private string _red;
private string _selection;
private readonly TerminalTheme _theme;
private string _white;
private string _yellow;
private readonly IFileSystemService _fileSystemService;
public event EventHandler<string> BackgroundChanged;
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService)
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, bool isNew)
{
_theme = theme;
Model = theme;
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_isNew = isNew;
Name = _theme.Name;
Author = _theme.Author;
Id = _theme.Id;
Name = Model.Name;
Author = Model.Author;
Id = Model.Id;
Black = _theme.Colors.Black;
Red = _theme.Colors.Red;
Green = _theme.Colors.Green;
Yellow = _theme.Colors.Yellow;
Blue = _theme.Colors.Blue;
Magenta = _theme.Colors.Magenta;
Cyan = _theme.Colors.Cyan;
White = _theme.Colors.White;
Black = Model.Colors.Black;
Red = Model.Colors.Red;
Green = Model.Colors.Green;
Yellow = Model.Colors.Yellow;
Blue = Model.Colors.Blue;
Magenta = Model.Colors.Magenta;
Cyan = Model.Colors.Cyan;
White = Model.Colors.White;
BrightBlack = _theme.Colors.BrightBlack;
BrightRed = _theme.Colors.BrightRed;
BrightGreen = _theme.Colors.BrightGreen;
BrightYellow = _theme.Colors.BrightYellow;
BrightBlue = _theme.Colors.BrightBlue;
BrightMagenta = _theme.Colors.BrightMagenta;
BrightCyan = _theme.Colors.BrightCyan;
BrightWhite = _theme.Colors.BrightWhite;
BrightBlack = Model.Colors.BrightBlack;
BrightRed = Model.Colors.BrightRed;
BrightGreen = Model.Colors.BrightGreen;
BrightYellow = Model.Colors.BrightYellow;
BrightBlue = Model.Colors.BrightBlue;
BrightMagenta = Model.Colors.BrightMagenta;
BrightCyan = Model.Colors.BrightCyan;
BrightWhite = Model.Colors.BrightWhite;
Background = _theme.Colors.Background;
Foreground = _theme.Colors.Foreground;
Cursor = _theme.Colors.Cursor;
CursorAccent = _theme.Colors.CursorAccent;
Selection = _theme.Colors.Selection;
Background = Model.Colors.Background;
Foreground = Model.Colors.Foreground;
Cursor = Model.Colors.Cursor;
CursorAccent = Model.Colors.CursorAccent;
Selection = Model.Colors.Selection;
SetActiveCommand = new RelayCommand(SetActive);
DeleteCommand = new RelayCommand(async () => await Delete().ConfigureAwait(false), NotPreInstalled);
@@ -94,6 +93,8 @@ namespace FluentTerminal.App.ViewModels
public event EventHandler Deleted;
public TerminalTheme Model { get; }
public string Author
{
get => _author;
@@ -268,78 +269,125 @@ namespace FluentTerminal.App.ViewModels
public void SaveChanges()
{
_theme.Name = Name;
_theme.Author = Author;
Model.Name = Name;
Model.Author = Author;
_theme.Colors.Black = Black;
_theme.Colors.Red = Red;
_theme.Colors.Green = Green;
_theme.Colors.Yellow = Yellow;
_theme.Colors.Blue = Blue;
_theme.Colors.Magenta = Magenta;
_theme.Colors.Cyan = Cyan;
_theme.Colors.White = White;
Model.Colors.Black = Black;
Model.Colors.Red = Red;
Model.Colors.Green = Green;
Model.Colors.Yellow = Yellow;
Model.Colors.Blue = Blue;
Model.Colors.Magenta = Magenta;
Model.Colors.Cyan = Cyan;
Model.Colors.White = White;
_theme.Colors.BrightBlack = BrightBlack;
_theme.Colors.BrightRed = BrightRed;
_theme.Colors.BrightGreen = BrightGreen;
_theme.Colors.BrightYellow = BrightYellow;
_theme.Colors.BrightBlue = BrightBlue;
_theme.Colors.BrightMagenta = BrightMagenta;
_theme.Colors.BrightCyan = BrightCyan;
_theme.Colors.BrightWhite = BrightWhite;
Model.Colors.BrightBlack = BrightBlack;
Model.Colors.BrightRed = BrightRed;
Model.Colors.BrightGreen = BrightGreen;
Model.Colors.BrightYellow = BrightYellow;
Model.Colors.BrightBlue = BrightBlue;
Model.Colors.BrightMagenta = BrightMagenta;
Model.Colors.BrightCyan = BrightCyan;
Model.Colors.BrightWhite = BrightWhite;
_theme.Colors.Background = Background;
_theme.Colors.Foreground = Foreground;
_theme.Colors.Cursor = Cursor;
_theme.Colors.CursorAccent = CursorAccent;
_theme.Colors.Selection = Selection;
Model.Colors.Background = Background;
Model.Colors.Foreground = Foreground;
Model.Colors.Cursor = Cursor;
Model.Colors.CursorAccent = CursorAccent;
Model.Colors.Selection = Selection;
_settingsService.SaveTheme(_theme);
_settingsService.SaveTheme(Model);
InEditMode = false;
_isNew = false;
}
private async Task CancelEdit()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
if (_isNew)
{
Black = _fallBackColors.Black;
Red = _fallBackColors.Red;
Green = _fallBackColors.Green;
Yellow = _fallBackColors.Yellow;
Blue = _fallBackColors.Blue;
Magenta = _fallBackColors.Magenta;
Cyan = _fallBackColors.Cyan;
White = _fallBackColors.White;
await Delete();
}
else
{
TerminalTheme changedTheme = new TerminalTheme()
{
Name = Name,
Author = Author,
Colors = new TerminalColors()
{
Black = Black,
Red = Red,
Green = Green,
Yellow = Yellow,
Blue = Blue,
Magenta = Magenta,
Cyan = Cyan,
White = White,
BrightBlack = _fallBackColors.BrightBlack;
BrightRed = _fallBackColors.BrightRed;
BrightGreen = _fallBackColors.BrightGreen;
BrightYellow = _fallBackColors.BrightYellow;
BrightBlue = _fallBackColors.BrightBlue;
BrightMagenta = _fallBackColors.BrightMagenta;
BrightCyan = _fallBackColors.BrightCyan;
BrightWhite = _fallBackColors.BrightWhite;
BrightBlack = BrightBlack,
BrightRed = BrightRed,
BrightGreen = BrightGreen,
BrightYellow = BrightYellow,
BrightBlue = BrightBlue,
BrightMagenta = BrightMagenta,
BrightCyan = BrightCyan,
BrightWhite = BrightWhite,
Background = _fallBackColors.Background;
Foreground = _fallBackColors.Foreground;
Cursor = _fallBackColors.Cursor;
CursorAccent = _fallBackColors.CursorAccent;
Selection = _fallBackColors.Selection;
Background = Background,
Foreground = Foreground,
Cursor = Cursor,
CursorAccent = CursorAccent,
Selection = Selection
}
};
Name = _fallBackName;
Author = _fallBackAuthor;
if (!_fallbackTheme.Equals(changedTheme))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
InEditMode = false;
if (result == DialogButton.OK)
{
Black = _fallbackTheme.Colors.Black;
Red = _fallbackTheme.Colors.Red;
Green = _fallbackTheme.Colors.Green;
Yellow = _fallbackTheme.Colors.Yellow;
Blue = _fallbackTheme.Colors.Blue;
Magenta = _fallbackTheme.Colors.Magenta;
Cyan = _fallbackTheme.Colors.Cyan;
White = _fallbackTheme.Colors.White;
BrightBlack = _fallbackTheme.Colors.BrightBlack;
BrightRed = _fallbackTheme.Colors.BrightRed;
BrightGreen = _fallbackTheme.Colors.BrightGreen;
BrightYellow = _fallbackTheme.Colors.BrightYellow;
BrightBlue = _fallbackTheme.Colors.BrightBlue;
BrightMagenta = _fallbackTheme.Colors.BrightMagenta;
BrightCyan = _fallbackTheme.Colors.BrightCyan;
BrightWhite = _fallbackTheme.Colors.BrightWhite;
Background = _fallbackTheme.Colors.Background;
Foreground = _fallbackTheme.Colors.Foreground;
Cursor = _fallbackTheme.Colors.Cursor;
CursorAccent = _fallbackTheme.Colors.CursorAccent;
Selection = _fallbackTheme.Colors.Selection;
Name = _fallbackTheme.Name;
Author = _fallbackTheme.Author;
InEditMode = false;
}
}
else
{
InEditMode = false;
}
}
}
private bool NotPreInstalled()
{
return !_theme.PreInstalled;
return !Model.PreInstalled;
}
private async Task Delete()
@@ -354,9 +402,7 @@ namespace FluentTerminal.App.ViewModels
private void Edit()
{
_fallBackColors = new TerminalColors(_theme.Colors);
_fallBackName = Name;
_fallBackAuthor = Author;
_fallbackTheme = new TerminalTheme(Model);
InEditMode = true;
}
@@ -367,7 +413,7 @@ namespace FluentTerminal.App.ViewModels
private Task Export()
{
var content = JsonConvert.SerializeObject(_theme, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
var content = JsonConvert.SerializeObject(Model, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
return _fileSystemService.SaveTextFile(Name, "Fluent Terminal Theme", ".flutecolors", content);
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Timer = System.Timers.Timer;
namespace FluentTerminal.App.ViewModels.Utilities
{
public class InputBuffer
{
private readonly Timer _timer = new Timer();
private readonly Func<string, Task> _writeCallback;
private readonly List<string> _buffer = new List<string>();
private readonly object _lock = new object();
public InputBuffer(Func<string, Task> writeCallback)
{
_timer.AutoReset = true;
_timer.Elapsed += _timer_Elapsed;
_writeCallback = writeCallback;
_timer.Interval = 5;
_timer.Start();
}
private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
lock (_lock)
{
var data = string.Concat(_buffer);
_buffer.Clear();
_writeCallback.Invoke(data);
}
}
public void Write(string data)
{
lock (_lock)
{
_buffer.Add(data);
}
}
}
}
@@ -3,6 +3,7 @@ using FluentTerminal.App.Services.EventArgs;
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation.Metadata;
using Windows.UI.Core;
using Windows.UI.Core.Preview;
using Windows.UI.ViewManagement;
@@ -29,6 +30,11 @@ namespace FluentTerminal.App.Adapters
set => _applicationView.Title = value;
}
public bool IsApiContractPresent(string api, ushort version)
{
return ApiInformation.IsApiContractPresent(api, version);
}
public Task RunOnDispatcherThread(Action action)
{
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
@@ -57,7 +63,10 @@ namespace FluentTerminal.App.Adapters
var deferral = e.GetDeferral();
var args = new CancelableEventArgs();
await CloseRequested?.Invoke(this, args);
if (CloseRequested != null)
{
await CloseRequested.Invoke(this, args);
}
e.Handled = args.Cancelled;
+1
View File
@@ -37,6 +37,7 @@
<converters:EnumValueToVisibiltyConverter x:Key="EnumValueToVisibiltyConverter" />
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
<converters:EnumToDescriptionConverter x:Key="EnumToDescriptionConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
+36 -12
View File
@@ -4,6 +4,7 @@ using FluentTerminal.App.Dialogs;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Adapters;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.EventArgs;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.App.ViewModels;
using FluentTerminal.App.Views;
@@ -48,6 +49,8 @@ namespace FluentTerminal.App
InitializeComponent();
UnhandledException += OnUnhandledException;
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
@@ -63,6 +66,8 @@ namespace FluentTerminal.App
builder.RegisterType<TrayProcessCommunicationService>().As<ITrayProcessCommunicationService>().SingleInstance();
builder.RegisterType<DialogService>().As<IDialogService>().SingleInstance();
builder.RegisterType<KeyboardCommandService>().As<IKeyboardCommandService>().InstancePerDependency();
builder.RegisterType<NotificationService>().As<INotificationService>().InstancePerDependency();
builder.RegisterType<UpdateService>().As<IUpdateService>().InstancePerDependency();
builder.RegisterType<MainViewModel>().InstancePerDependency();
builder.RegisterType<SettingsViewModel>().InstancePerDependency();
builder.RegisterType<ThemeParserFactory>().As<IThemeParserFactory>().SingleInstance();
@@ -73,6 +78,7 @@ namespace FluentTerminal.App
builder.RegisterType<SystemFontService>().As<ISystemFontService>().SingleInstance();
builder.RegisterType<ShellProfileSelectionDialog>().As<IShellProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<CreateKeyBindingDialog>().As<ICreateKeyBindingDialog>().InstancePerDependency();
builder.RegisterType<InputDialog>().As<IInputDialog>().InstancePerDependency();
builder.RegisterType<MessageDialogAdapter>().As<IMessageDialog>().InstancePerDependency();
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
@@ -89,6 +95,11 @@ namespace FluentTerminal.App
_applicationSettings = _settingsService.GetApplicationSettings();
}
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
{
System.Diagnostics.Debug.WriteLine(e.Exception.ToString());
}
protected override async void OnActivated(IActivatedEventArgs args)
{
if (args is CommandLineActivatedEventArgs commandLineActivated)
@@ -112,11 +123,11 @@ namespace FluentTerminal.App
{
if (_applicationSettings.NewTerminalLocation == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
{
await _mainViewModels.Last().AddTerminal(parameter, false).ConfigureAwait(true);
await _mainViewModels.Last().AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
}
else
{
await CreateNewTerminalWindow(parameter).ConfigureAwait(true);
await CreateNewTerminalWindow(parameter, false).ConfigureAwait(true);
}
}
}
@@ -130,7 +141,7 @@ namespace FluentTerminal.App
else if (command == "new")
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminal(parameter, false).ConfigureAwait(true);
await viewModel.AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
}
}
@@ -142,13 +153,13 @@ namespace FluentTerminal.App
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminal(null, false).ConfigureAwait(true);
await viewModel.AddTerminal(null, false, Guid.Empty).ConfigureAwait(true);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
Window.Current.Activate();
}
else if (_mainViewModels.Count == 0)
{
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, string.Empty).ConfigureAwait(true);
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, false, string.Empty).ConfigureAwait(true);
}
}
@@ -199,6 +210,7 @@ namespace FluentTerminal.App
mainViewModel.Closed += OnMainViewModelClosed;
mainViewModel.NewWindowRequested += OnNewWindowRequested;
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(mainViewModel);
}
@@ -208,13 +220,13 @@ namespace FluentTerminal.App
Window.Current.Activate();
}
private async Task CreateNewTerminalWindow(string startupDirectory)
private async Task CreateNewTerminalWindow(string startupDirectory, bool showProfileSelection)
{
var id = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, startupDirectory).ConfigureAwait(true);
var id = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true, showProfileSelection, startupDirectory).ConfigureAwait(true);
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(id);
}
private async Task<int> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar, object parameter)
private async Task<int> CreateSecondaryView<TViewModel>(Type pageType, bool ExtendViewIntoTitleBar, bool showProfileSelection, object parameter)
{
int windowId = 0;
TViewModel viewModel = default;
@@ -236,8 +248,9 @@ namespace FluentTerminal.App
mainViewModel.Closed += OnMainViewModelClosed;
mainViewModel.NewWindowRequested += OnNewWindowRequested;
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(mainViewModel);
await mainViewModel.AddTerminal(directory, false).ConfigureAwait(true);
await mainViewModel.AddTerminal(directory, showProfileSelection, Guid.Empty).ConfigureAwait(true);
}
if (viewModel is SettingsViewModel settingsViewModel)
@@ -266,9 +279,9 @@ namespace FluentTerminal.App
}
}
private async void OnNewWindowRequested(object sender, EventArgs e)
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
await CreateNewTerminalWindow(string.Empty).ConfigureAwait(true);
await CreateNewTerminalWindow(string.Empty, e.ShowProfileSelection).ConfigureAwait(true);
}
private void OnSettingsClosed(object sender, EventArgs e)
@@ -278,16 +291,27 @@ namespace FluentTerminal.App
_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 ShowAbout()
{
await ShowSettings().ConfigureAwait(true);
_settingsViewModel.NavigateToAboutPage();
}
private async Task ShowSettings()
{
if (_settingsViewModel == null)
{
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, null).ConfigureAwait(true);
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, false, null).ConfigureAwait(true);
}
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,24 @@
using System;
using FluentTerminal.App.Services.Utilities;
using Windows.UI.Xaml.Data;
namespace FluentTerminal.App.Converters
{
public class EnumToDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
// value is the Enum, parameter is the string.
if (value is Enum enumValue)
{
return EnumHelper.GetEnumDescription(enumValue);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
}
@@ -0,0 +1,18 @@
using System;
using Windows.UI.Xaml.Data;
namespace FluentTerminal.App.Converters
{
public class ToolTipValueToPixelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return string.Format("{0} px", value);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,13 @@
<ContentDialog
x:Class="FluentTerminal.App.Dialogs.InputDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
CloseButtonText="Cancel"
PrimaryButtonText="OK"
KeyDown="Dialog_KeyUp"
mc:Ignorable="d">
<TextBox VerticalAlignment="Center" Text="{x:Bind Input, Mode=TwoWay}" />
</ContentDialog>
@@ -0,0 +1,45 @@
using FluentTerminal.App.Services.Dialogs;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
using System;
using Windows.UI.Xaml.Input;
using Windows.System;
namespace FluentTerminal.App.Dialogs
{
public sealed partial class InputDialog : ContentDialog, IInputDialog
{
private bool EnterPressed = false;
public string Input { get; private set; }
public InputDialog()
{
this.InitializeComponent();
}
public async Task<string> GetInput()
{
var result = await ShowAsync();
if (result == ContentDialogResult.Primary || EnterPressed)
{
return Input;
}
return null;
}
public void SetTitle(string title)
{
Title = title;
}
void Dialog_KeyUp(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
EnterPressed = true;
Hide();
}
}
}
}
+40 -2
View File
@@ -17,8 +17,8 @@
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WindowsXamlEnableOverview>true</WindowsXamlEnableOverview>
<PackageCertificateKeyFile>FluentTerminal.App_StoreKey.pfx</PackageCertificateKeyFile>
<PackageCertificateThumbprint>8FA8EF727E767FAE5409DDD1694C1CEE054A2962</PackageCertificateThumbprint>
<PackageCertificateKeyFile>FluentTerminal.App_TemporaryKey.pfx</PackageCertificateKeyFile>
<PackageCertificateThumbprint>F96F3E0E485F07E8E98DA6E5C06AA4B6F112B498</PackageCertificateThumbprint>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<AppxBundle>Always</AppxBundle>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
@@ -113,6 +113,7 @@
<Compile Include="Behaviors\MiddleClickBehavior.cs" />
<Compile Include="Converters\BackgroundToApplicationThemeConverter.cs" />
<Compile Include="Converters\ColorResourceKeyFallbackConverter.cs" />
<Compile Include="Converters\EnumToDescriptionConverter.cs" />
<Compile Include="Converters\TabColorFallbackConverter.cs" />
<Compile Include="Converters\EnumValueToVisibiltyConverter.cs" />
<Compile Include="Converters\FalseToVisibleConverter.cs" />
@@ -122,8 +123,12 @@
<Compile Include="Converters\StringToColorConverter.cs" />
<Compile Include="Converters\TabThemeSelectedConverter.cs" />
<Compile Include="Converters\TerminalViewModelToViewConverter.cs" />
<Compile Include="Converters\ToolTipValueToPixelConverter.cs" />
<Compile Include="Converters\TrueToVisibleConverter.cs" />
<Compile Include="Adapters\MessageDialogAdapter.cs" />
<Compile Include="Dialogs\InputDialog.xaml.cs">
<DependentUpon>InputDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Dialogs\ShellProfileSelectionDialog.xaml.cs">
<DependentUpon>ShellProfileSelectionDialog.xaml</DependentUpon>
</Compile>
@@ -146,6 +151,9 @@
<Compile Include="Views\SettingsPage.xaml.cs">
<DependentUpon>SettingsPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\SettingsPages\About.xaml.cs">
<DependentUpon>About.xaml</DependentUpon>
</Compile>
<Compile Include="Views\SettingsPages\MouseSettings.xaml.cs">
<DependentUpon>MouseSettings.xaml</DependentUpon>
</Compile>
@@ -168,18 +176,29 @@
<DependentUpon>TabBar.xaml</DependentUpon>
</Compile>
<Compile Include="Views\TabBarBackgroundBindingHelper.cs" />
<Compile Include="Views\TerminalKeybindTemplateSelector.cs" />
<Compile Include="Views\TabThemeTemplateSelector.cs" />
<Compile Include="Views\TerminalColorPicker.xaml.cs">
<DependentUpon>TerminalColorPicker.xaml</DependentUpon>
</Compile>
<Compile Include="Views\NoValueTemplateSelector.cs" />
<Compile Include="Views\TerminalThemeTemplateSelector.cs" />
<Compile Include="Views\ITerminalView.cs" />
<Compile Include="Views\TerminalView.xaml.cs">
<DependentUpon>TerminalView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\VtNetCoreTerminalView.xaml.cs">
<DependentUpon>VtNetCoreTerminalView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\XtermTerminalView.xaml.cs">
<DependentUpon>XtermTerminalView.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
<Content Include="Assets\AppIcons\Square106x106Logo.black.png" />
<Content Include="Assets\settingsicons.ttf" />
<Content Include="Win32\Autofac.dll" />
<Content Include="Win32\FluentTerminal.App.Services.dll" />
@@ -269,6 +288,10 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="Dialogs\InputDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Dialogs\ShellProfileSelectionDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@@ -294,6 +317,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\SettingsPages\About.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\SettingsPages\MouseSettings.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@@ -330,6 +357,14 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\VtNetCoreTerminalView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\XtermTerminalView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac">
@@ -350,6 +385,9 @@
<PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version>
</PackageReference>
<PackageReference Include="VtNetCore">
<Version>1.0.15</Version>
</PackageReference>
<PackageReference Include="Win2D.uwp">
<Version>1.21.0</Version>
</PackageReference>
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" IgnorableNamespaces="uap mp desktop rescap">
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.2.1.0" />
<Identity Name="53621FSApps.FluentTerminal" Publisher="CN=BDC72197-B634-438A-B863-16DCB7D90C85" Version="0.3.1.0" />
<mp:PhoneIdentity PhoneProductId="f7225014-f405-4342-ad2e-ebf6ac60ea2c" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
<Properties>
<DisplayName>Fluent Terminal</DisplayName>
+4
View File
@@ -1,5 +1,9 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Thickness x:Key="HeaderMargin">24,0,0,0</Thickness>
<Thickness x:Key="ContainerMargin">24,0,24,24</Thickness>
<Thickness x:Key="ItemMargin">0,24,0,0</Thickness>
<Style
x:Key="WindowTitleTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
+17
View File
@@ -0,0 +1,17 @@
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Views
{
public interface ITerminalView
{
Task ChangeTheme(TerminalTheme theme);
Task ChangeKeyBindings();
Task ChangeOptions(TerminalOptions options);
Task Initialize(TerminalViewModel viewModel);
Task FindNext(string searchText);
Task FindPrevious(string searchText);
Task FocusTerminal();
}
}
@@ -10,13 +10,15 @@
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Width="180"
Margin="0,6,24,0"
Text="{x:Bind ViewModel.CommandName, Mode=OneWay}" />
Text="{x:Bind ViewModel.CommandName, Mode=OneWay}"
Visibility="{x:Bind ShowCommandName, Mode=OneWay}" />
<ItemsControl Grid.Column="1" ItemsSource="{x:Bind ViewModel.KeyBindings, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="settings:KeyBindingViewModel">
@@ -82,11 +84,13 @@
<HyperlinkButton
Margin="12,0,0,0"
Command="{x:Bind EditCommand}"
Content="Edit" />
Content="Edit"
Visibility="{x:Bind Parent.Editable, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}" />
<HyperlinkButton
Margin="12,0,0,0"
Command="{x:Bind DeleteCommand}"
Content="Delete" />
Content="Delete"
Visibility="{x:Bind Parent.Editable, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
@@ -6,14 +6,23 @@ namespace FluentTerminal.App.Views
{
public sealed partial class KeyBindingsView : UserControl
{
public static readonly DependencyProperty ShowCommandNameProperty =
DependencyProperty.Register(nameof(ShowCommandName), typeof(bool), typeof(KeyBindingsView), new PropertyMetadata(true));
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(nameof(ViewModel), typeof(KeyBindingsViewModel), typeof(KeyBindingsView), new PropertyMetadata(null));
DependencyProperty.Register(nameof(ViewModel), typeof(KeyBindingsViewModel), typeof(KeyBindingsView), new PropertyMetadata(null));
public KeyBindingsView()
{
InitializeComponent();
}
public bool ShowCommandName
{
get { return (bool)GetValue(ShowCommandNameProperty); }
set { SetValue(ShowCommandNameProperty, value); }
}
public KeyBindingsViewModel ViewModel
{
get { return (KeyBindingsViewModel)GetValue(ViewModelProperty); }
+19 -4
View File
@@ -6,7 +6,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:FluentTerminal.App.Views"
RequestedTheme="{x:Bind ViewModel.Background, Mode=OneWay, Converter={StaticResource BackgroundToApplicationThemeConverter}}"
RequestedTheme="{x:Bind ViewModel.SelectedTerminal.TerminalTheme.Colors.Background, Mode=OneWay, Converter={StaticResource BackgroundToApplicationThemeConverter}}"
mc:Ignorable="d">
<Page.Resources>
<converters:TerminalViewModelToViewConverter x:Key="TerminalViewModelToViewConverter" />
@@ -15,9 +15,11 @@
<Grid.Background>
<AcrylicBrush
BackgroundSource="HostBackdrop"
FallbackColor="{x:Bind ViewModel.Background, Mode=OneWay}"
TintColor="{x:Bind ViewModel.Background, Mode=OneWay}"
TintOpacity="{x:Bind ViewModel.BackgroundOpacity, Mode=OneWay}" />
FallbackColor="{x:Bind ViewModel.SelectedTerminal.TerminalTheme.Colors.Background, Mode=OneWay}"
TintColor="{x:Bind ViewModel.SelectedTerminal.TerminalTheme.Colors.Background, Mode=OneWay}"
TintOpacity="{x:Bind ViewModel.BackgroundOpacity, Mode=OneWay}"
TintTransitionDuration="{x:Bind NoDuration}" />
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -60,6 +62,17 @@
<SymbolIcon Symbol="Setting" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Width="160"
Command="{x:Bind ViewModel.ShowAboutCommand}"
Text="About">
<MenuFlyoutItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Glyph="&#xE946;"
ToolTipService.ToolTip="About" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
</Button.Flyout>
</Button>
@@ -68,6 +81,8 @@
IsHitTestVisible="False"
Style="{StaticResource WindowTitleTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedTerminal.Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind ViewModel.ShowTabsOnTop, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
<views:TabBar
Grid.Column="1"
+5 -2
View File
@@ -1,5 +1,6 @@
using FluentTerminal.App.Utilities;
using FluentTerminal.App.ViewModels;
using System;
using System.ComponentModel;
using Windows.ApplicationModel.Core;
using Windows.UI.Core;
@@ -18,6 +19,8 @@ namespace FluentTerminal.App.Views
public double CoreTitleBarHeight => coreTitleBar.Height;
public TimeSpan NoDuration => TimeSpan.Zero;
public Thickness CoreTitleBarPadding
{
get
@@ -59,11 +62,11 @@ namespace FluentTerminal.App.Views
}
}
private async void OnWindowActivated(object sender, WindowActivatedEventArgs e)
private void OnWindowActivated(object sender, WindowActivatedEventArgs e)
{
if (e.WindowActivationState != CoreWindowActivationState.Deactivated && TerminalContainer.Content is TerminalView terminal)
{
await terminal.FocusWebView().ConfigureAwait(true);
terminal.ViewModel.FocusTerminal();
}
}
@@ -0,0 +1,26 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public class NoValueTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultTemplate { get; set; }
public DataTemplate CustomTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
protected override DataTemplate SelectTemplateCore(object item)
{
if (item is String arguments)
{
return String.Empty.Equals(arguments) ? DefaultTemplate : CustomTemplate;
}
return null;
}
}
}
+11 -1
View File
@@ -18,7 +18,7 @@
x:Name="NavigationView"
AlwaysShowHeader="False"
Canvas.ZIndex="0"
CompactModeThresholdWidth="560"
CompactModeThresholdWidth="0"
ExpandedModeThresholdWidth="560"
IsSettingsVisible="False"
Loaded="NavigationView_Loaded"
@@ -74,6 +74,16 @@
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<NavigationView.PaneFooter>
<NavigationViewItem Content="About" Tapped="AboutTapped">
<NavigationViewItem.Icon>
<FontIcon
FontFamily="Segoe MDL2 Assets"
Glyph="&#xE946;"
ToolTipService.ToolTip="About" />
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.PaneFooter>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Frame x:Name="ContentFrame" />
<TextBlock
@@ -1,6 +1,7 @@
using FluentTerminal.App.Utilities;
using FluentTerminal.App.ViewModels;
using FluentTerminal.App.Views.SettingsPages;
using System;
using System.Linq;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
@@ -9,6 +10,7 @@ using Windows.UI.Core.Preview;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Navigation;
namespace FluentTerminal.App.Views
@@ -18,6 +20,7 @@ namespace FluentTerminal.App.Views
private readonly UISettings _uiSettings;
private readonly ApplicationViewTitleBar _titleBar;
private readonly CoreDispatcher _dispatcher;
private readonly NavigationViewItem hiddenNavigationItem;
private bool _onThemesPage;
public SettingsPage()
@@ -39,6 +42,8 @@ namespace FluentTerminal.App.Views
_uiSettings = new UISettings();
_uiSettings.ColorValuesChanged += OnColorValuesChanged;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += SettingsPage_CloseRequested;
hiddenNavigationItem = new NavigationViewItem();
}
private void OnColorValuesChanged(UISettings sender, object args)
@@ -67,9 +72,24 @@ namespace FluentTerminal.App.Views
if (e.Parameter is SettingsViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.AboutPageRequested += OnAboutPageRequested;
}
}
private void OnAboutPageRequested(object sender, System.EventArgs e)
{
_dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
ContentFrame.Navigate(typeof(About), ViewModel.About);
// Deselect item in NavigationView (https://stackoverflow.com/a/49082640/4132379)
NavigationView.MenuItems.Add(hiddenNavigationItem);
NavigationView.SelectedItem = hiddenNavigationItem;
NavigationView.SelectedItem = null;
NavigationView.MenuItems.Remove(hiddenNavigationItem);
});
}
private void NavigationView_Loaded(object sender, RoutedEventArgs e)
{
NavigationView.SelectedItem = NavigationView.MenuItems.Cast<NavigationViewItemBase>().FirstOrDefault(m => m.Tag.ToString() == "general");
@@ -116,6 +136,11 @@ namespace FluentTerminal.App.Views
}
}
private void AboutTapped(object sender, TappedRoutedEventArgs e)
{
OnAboutPageRequested(sender, EventArgs.Empty);
}
private void SettingsPage_CloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
{
ViewModel.Close();
@@ -0,0 +1,90 @@
<Page
x:Class="FluentTerminal.App.Views.SettingsPages.About"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock
Margin="0,10,0,0"
VerticalAlignment="Center"
FontSize="28"
Style="{StaticResource TitleTextBlockStyle}"
Text="About" />
<CommandBar
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="{ThemeResource SystemControlBackgroundAltHighBrush}"
DefaultLabelPosition="Right">
<AppBarButton
Command="{x:Bind ViewModel.CheckForUpdatesCommand}"
Icon="Sync"
Label="Check for updates" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2">
<Grid Margin="{StaticResource ContainerMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Image
MaxWidth="106"
MaxHeight="106"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Source="/Assets/AppIcons/Square106x106Logo.black.png"
Stretch="None" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Current version" />
<TextBlock>
<Run Text="{x:Bind ViewModel.CurrentVersion}" />
-<Hyperlink NavigateUri="{x:Bind ViewModel.CurrentVersionReleaseNotesURL}">Release notes</Hyperlink>
</TextBlock>
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Latest version" />
<StackPanel Orientation="Horizontal" Visibility="{x:Bind ViewModel.LatestVersionLoading, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<ProgressRing Margin="0,0,8,0" IsActive="True" />
<TextBlock><Italic>Checking for updates...</Italic></TextBlock>
</StackPanel>
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionNotFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<Italic>Not found</Italic>
</TextBlock>
<TextBlock Visibility="{x:Bind ViewModel.LatestVersionFound, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}">
<Run Text="{x:Bind ViewModel.LatestVersion, Mode=OneWay}" />
-<Hyperlink NavigateUri="{x:Bind ViewModel.LatestVersionReleaseNotesURL, Mode=OneWay}">Release notes</Hyperlink>
</TextBlock>
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="License" />
<TextBlock><Hyperlink NavigateUri="https://github.com/felixse/FluentTerminal/blob/master/LICENSE">GNU General Public License v3.0</Hyperlink></TextBlock>
</StackPanel>
</StackPanel>
<StackPanel
Grid.Row="1"
Margin="0,72,0,0"
VerticalAlignment="Bottom">
<TextBlock x:Name="CreatedBy" />
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Page>
@@ -0,0 +1,31 @@
using FluentTerminal.App.ViewModels.Settings;
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents;
using Windows.UI.Xaml.Navigation;
namespace FluentTerminal.App.Views.SettingsPages
{
public sealed partial class About : Page
{
public AboutPageViewModel ViewModel { get; private set; }
public About()
{
InitializeComponent();
CreatedBy.Inlines.Add(new Run { Text = "Created by " });
CreatedBy.Inlines.Add(new Italic { Inlines = { new Run { Text = "felixse " } } });
CreatedBy.Inlines.Add(new Run { Text = "and " });
CreatedBy.Inlines.Add(new Hyperlink { Inlines = { new Run { Text = "contributors" } }, NavigateUri = new Uri("https://github.com/felixse/FluentTerminal/graphs/contributors") });
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is AboutPageViewModel viewModel)
{
ViewModel = viewModel;
}
ViewModel.OnNavigatedTo();
}
}
}
@@ -11,7 +11,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,00,0,24">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -34,67 +34,93 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
<StackPanel>
<ScrollViewer Grid.Row="2">
<StackPanel Margin="{StaticResource ContainerMargin}">
<StackPanel Margin="{StaticResource ItemMargin}">
<ToggleSwitch
Header="Run on startup"
IsEnabled="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay}"
IsOn="{x:Bind ViewModel.StartupTaskEnabled, Mode=TwoWay}" />
<TextBlock
Foreground="Red"
Text="{x:Bind ViewModel.StartupTaskErrorMessage, Mode=OneWay}"
Visibility="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<ToggleSwitch Header="Show in system tray" IsOn="{x:Bind ViewModel.EnableTrayIcon, Mode=TwoWay}" />
<TextBlock
Foreground="Red"
Text="Relaunch required to apply setting"
Visibility="{x:Bind ViewModel.ShouldRestartForTrayMessage, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Open new terminals as" />
<RadioButton
Content="Tabs"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.TabIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Windows"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.WindowIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Show tabs on" />
<RadioButton
Content="Top"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.TopIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Bottom"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.BottomIsSelected, Mode=TwoWay}" />
</StackPanel>
<ToggleSwitch
Header="Run on startup"
IsEnabled="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay}"
IsOn="{x:Bind ViewModel.StartupTaskEnabled, Mode=TwoWay}" />
<TextBlock
Foreground="Red"
Text="{x:Bind ViewModel.StartupTaskErrorMessage, Mode=OneWay}"
Visibility="{x:Bind ViewModel.CanEnableStartupTask, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
<TextBlock Margin="0,24,0,8" Text="Open new terminals as" />
<RadioButton
Content="Tabs"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.TabIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Windows"
GroupName="NewTerminalLocation"
IsChecked="{x:Bind ViewModel.WindowIsSelected, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Show tabs on" />
<RadioButton
Content="Top"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.TopIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Bottom"
GroupName="TabsPosition"
IsChecked="{x:Bind ViewModel.BottomIsSelected, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Always show tabs"
IsOn="{x:Bind ViewModel.AlwaysShowTabs, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Underline selected tab"
IsOn="{x:Bind ViewModel.UnderlineSelectedTab, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Color on inactive tabs" />
<RadioButton
Content="Background"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.BackgroundIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Underline"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.UnderlinedIsSelected, Mode=TwoWay}" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Color on inactive tabs" />
<RadioButton
Content="Background"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.BackgroundIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Underline"
GroupName="InactiveTabColorMode"
IsChecked="{x:Bind ViewModel.UnderlinedIsSelected, Mode=TwoWay}" />
</StackPanel>
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Ask before closing windows"
IsOn="{x:Bind ViewModel.ConfirmClosingWindows, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Ask before closing tabs"
IsOn="{x:Bind ViewModel.ConfirmClosingTabs, Mode=TwoWay}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Show new output indicator on background tabs"
IsOn="{x:Bind ViewModel.ShowNewOutputIndicator, Mode=TwoWay}" />
<ToggleSwitch
Margin="{StaticResource ItemMargin}"
Header="Windows 10 1809+: Always use WinPty"
IsOn="{x:Bind ViewModel.AlwaysUseWinPty, Mode=TwoWay}" />
</StackPanel>
</ScrollViewer>
</Grid>
</Page>
</Page>
@@ -13,7 +13,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,0,0,0">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -42,11 +42,11 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,24,0,0">
<ItemsControl ItemsSource="{x:Bind ViewModel.KeyBindings, Mode=OneWay}">
<ScrollViewer Grid.Row="2">
<ItemsControl Margin="{StaticResource ContainerMargin}" ItemsSource="{x:Bind ViewModel.KeyBindings, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
<views:KeyBindingsView Margin="0,0,0,24" ViewModel="{x:Bind}" />
<views:KeyBindingsView Margin="{StaticResource ItemMargin}" ViewModel="{x:Bind}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -1,6 +1,7 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models;
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
@@ -22,17 +23,18 @@ namespace FluentTerminal.App.Views.SettingsPages
{
ViewModel = viewModel;
// Add the commands corresponding to all of the application's static commands
foreach (var value in Enum.GetValues(typeof(Command)))
{
var command = (Command)value;
Command command = (Command)value;
AddCommandMenu.Items.Add(new MenuFlyoutItem
{
Text = EnumHelper.GetEnumDescription(command),
Command = ViewModel.AddCommand,
CommandParameter = command
CommandParameter = command.ToString()
});
}
}
}
}
}
}
@@ -6,14 +6,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,00,0,24">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -36,40 +35,44 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
<StackPanel>
<ScrollViewer Grid.Row="2">
<StackPanel Margin="{StaticResource ContainerMargin}">
<ToggleSwitch
Margin="0,0,0,24"
Header="Copy on select"
IsOn="{x:Bind ViewModel.CopyOnSelect, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Middle button action" />
<RadioButton
Content="Nothing"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Paste"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickPasteIsSelected, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Right button action" />
<RadioButton
Content="Nothing"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Margin="0,0,0,24"
Content="Paste"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickPasteIsSelected, Mode=TwoWay}" />
Margin="{StaticResource ItemMargin}"
Header="Copy on select"
IsOn="{x:Bind ViewModel.CopyOnSelect, Mode=TwoWay}" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Middle button action" />
<RadioButton
Content="Nothing"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Paste"
GroupName="MiddleButton"
IsChecked="{x:Bind ViewModel.MouseMiddleClickPasteIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Right button action" />
<RadioButton
Content="Nothing"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickNoneIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Context menu"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickContextMenuIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Paste"
GroupName="RightButton"
IsChecked="{x:Bind ViewModel.MouseRightClickPasteIsSelected, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
@@ -6,6 +6,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:FluentTerminal.Models"
xmlns:settings="using:FluentTerminal.App.ViewModels.Settings"
xmlns:viewmodels="using:FluentTerminal.App.ViewModels"
xmlns:views="using:FluentTerminal.App.Views"
mc:Ignorable="d">
@@ -35,6 +36,45 @@
</DataTemplate>
</views:TabThemeTemplateSelector.ColoredTemplate>
</views:TabThemeTemplateSelector>
<views:TerminalThemeTemplateSelector x:Key="TerminalThemeTemplateSelector">
<views:TerminalThemeTemplateSelector.DefaultTemplate>
<DataTemplate x:DataType="models:TerminalTheme">
<TextBlock FontStyle="Italic" Text="{x:Bind Name}" />
</DataTemplate>
</views:TerminalThemeTemplateSelector.DefaultTemplate>
<views:TerminalThemeTemplateSelector.ThemeTemplate>
<DataTemplate x:DataType="models:TerminalTheme">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</views:TerminalThemeTemplateSelector.ThemeTemplate>
</views:TerminalThemeTemplateSelector>
<views:TerminalKeybindTemplateSelector x:Key="TerminalKeybindTemplateSelector">
<views:TerminalKeybindTemplateSelector.DefaultTemplate>
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
<TextBlock FontStyle="Italic" Text="None" />
</DataTemplate>
</views:TerminalKeybindTemplateSelector.DefaultTemplate>
<views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
<DataTemplate x:DataType="settings:KeyBindingsViewModel">
<views:KeyBindingsView
Margin="0,6,0,0"
ShowCommandName="False"
ViewModel="{x:Bind}" />
</DataTemplate>
</views:TerminalKeybindTemplateSelector.KeyBindingsTemplate>
</views:TerminalKeybindTemplateSelector>
<views:NoValueTemplateSelector x:Key="NoValueTemplateSelector">
<views:NoValueTemplateSelector.DefaultTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock FontStyle="Italic" Text="None" />
</DataTemplate>
</views:NoValueTemplateSelector.DefaultTemplate>
<views:NoValueTemplateSelector.CustomTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{x:Bind}" />
</DataTemplate>
</views:NoValueTemplateSelector.CustomTemplate>
</views:NoValueTemplateSelector>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions>
@@ -50,16 +90,13 @@
Grid.RowSpan="3"
Grid.Column="0"
Background="{ThemeResource SystemControlAcrylicWindowBrush}" />
<Grid
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,0,0,24">
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="24,10,0,0"
Margin="0,10,0,0"
VerticalAlignment="Center"
FontSize="28"
Style="{StaticResource TitleTextBlockStyle}"
@@ -76,6 +113,11 @@
Icon="Accept"
IsEnabled="{x:Bind ViewModel.SelectedShellProfile.IsDefault, Mode=OneWay, Converter={StaticResource NegateConverter}}"
Label="Set Default" />
<AppBarButton
Command="{x:Bind ViewModel.CloneCommand, Mode=OneWay}"
CommandParameter="{x:Bind ViewModel.SelectedShellProfile, Mode=OneWay}"
Icon="Copy"
Label="Clone" />
<AppBarButton
Command="{x:Bind ViewModel.SelectedShellProfile.EditCommand, Mode=OneWay}"
Icon="Edit"
@@ -83,7 +125,13 @@
<AppBarButton
Command="{x:Bind ViewModel.SelectedShellProfile.DeleteCommand, Mode=OneWay}"
Icon="Delete"
Label="Delete" />
Label="Delete"
Visibility="{x:Bind ViewModel.SelectedShellProfile.PreInstalled, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}" />
<AppBarButton
Command="{x:Bind ViewModel.SelectedShellProfile.RestoreDefaultsCommand, Mode=OneWay}"
Icon="Undo"
Label="Restore Defaults"
Visibility="{x:Bind ViewModel.SelectedShellProfile.PreInstalled, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
</CommandBar>
<CommandBar
Grid.Column="1"
@@ -106,7 +154,7 @@
Grid.Row="2"
Grid.Column="0"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<ListView ItemsSource="{x:Bind ViewModel.ShellProfiles}" SelectedItem="{x:Bind ViewModel.SelectedShellProfile, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:ShellProfileViewModel">
@@ -157,39 +205,42 @@
<ScrollViewer
Grid.Row="2"
Grid.Column="1"
Margin="24,0,0,0"
HorizontalAlignment="Stretch"
VerticalScrollBarVisibility="Auto">
<ContentControl HorizontalContentAlignment="Stretch" Content="{x:Bind ViewModel.SelectedShellProfile, Mode=OneWay}">
<ContentControl
Margin="{StaticResource ContainerMargin}"
HorizontalContentAlignment="Stretch"
Content="{x:Bind ViewModel.SelectedShellProfile, Mode=OneWay}">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="viewmodels:ShellProfileViewModel">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<StackPanel Margin="0,0,24,0" Visibility="{x:Bind InEditMode, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
<StackPanel Visibility="{x:Bind InEditMode, Converter={StaticResource TrueToVisibleConverter}, Mode=OneWay}">
<TextBox
Width="300"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Name"
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
IsSpellCheckEnabled="False"
Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Margin="0,24,0,0" Orientation="Horizontal">
<StackPanel Margin="{StaticResource ItemMargin}" Orientation="Horizontal">
<TextBox
Width="300"
Header="Shell executable location"
IsSpellCheckEnabled="False"
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
IsSpellCheckEnabled="False"
Text="{x:Bind Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button
Margin="6,0,0,0"
VerticalAlignment="Bottom"
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
Command="{x:Bind BrowseForCustomShellCommand}"
Content="..."
IsEnabled="{x:Bind PreInstalled, Converter={StaticResource NegateConverter}, Mode=OneWay}"
Style="{StaticResource ButtonRevealStyle}" />
</StackPanel>
<StackPanel Margin="0,24,0,0" Orientation="Horizontal">
<StackPanel Margin="{StaticResource ItemMargin}" Orientation="Horizontal">
<TextBox
Width="300"
Header="Working directory"
@@ -206,43 +257,121 @@
<TextBox
Width="300"
Margin="0,24,0,0"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Arguments"
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
IsSpellCheckEnabled="False"
Text="{x:Bind Arguments, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox
Width="150"
Margin="0,24,0,0"
Margin="{StaticResource ItemMargin}"
Header="Tab color"
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
ItemTemplateSelector="{StaticResource TabThemeTemplateSelector}"
ItemsSource="{x:Bind TabThemes, Mode=OneWay}"
SelectedItem="{Binding SelectedTabTheme, Mode=TwoWay}" />
<ComboBox
Width="150"
Margin="{StaticResource ItemMargin}"
Header="Theme"
IsEnabled="{x:Bind InEditMode, Mode=OneWay}"
ItemTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
ItemsSource="{x:Bind TerminalThemes, Mode=OneWay}"
SelectedItem="{Binding SelectedTerminalTheme, Mode=TwoWay}" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Line ending translation mode on paste" />
<RadioButton
Content="Do not modify line endings"
GroupName="LineEndingStyle"
IsChecked="{x:Bind DoNotModifyIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Convert line endings to CR+LF"
GroupName="LineEndingStyle"
IsChecked="{x:Bind ToCRLFIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Convert line endings to LF"
GroupName="LineEndingStyle"
IsChecked="{x:Bind ToLFIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock
Margin="0,0,6,0"
VerticalAlignment="Center"
Text="Keyboard shortcut" />
<HyperlinkButton Command="{x:Bind AddKeyboardShortcutCommand}" Content="Add" />
<views:KeyBindingsView ShowCommandName="False" ViewModel="{x:Bind KeyBindings}" />
</StackPanel>
</StackPanel>
<StackPanel Margin="0,0,24,0" Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind Name, Mode=OneWay}" />
<StackPanel Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
<TextBlock
Margin="{StaticResource ItemMargin}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Name, Mode=OneWay}" />
<controls:HeaderedTextBlock
Margin="0,58,0,0"
Margin="{StaticResource ItemMargin}"
Header="Shell executable location"
Text="{x:Bind Location, Mode=OneWay}" />
<controls:HeaderedTextBlock
Margin="0,36,0,0"
Header="Working directory"
Text="{x:Bind WorkingDirectory, Mode=OneWay}" />
<controls:HeaderedTextBlock
Margin="0,36,0,0"
Header="Arguments"
Text="{x:Bind Arguments, Mode=OneWay}" />
<controls:HeaderedContentControl
Margin="0,36,0,0"
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Working directory">
<ContentControl
Content="{x:Bind WorkingDirectory, Mode=OneWay}"
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Arguments">
<ContentControl
Content="{x:Bind Arguments, Mode=OneWay}"
ContentTemplateSelector="{StaticResource NoValueTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Tab color">
<ContentControl
Margin="0,6,0,0"
Content="{x:Bind SelectedTabTheme, Mode=OneWay}"
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}" />
ContentTemplateSelector="{StaticResource TabThemeTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Line ending translation mode">
<ContentControl
Content="{x:Bind LineEndingStyle, Mode=OneWay, Converter={StaticResource EnumToDescriptionConverter}}"
ContentTemplateSelector="{StaticResource TerminalThemeTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
<controls:HeaderedContentControl
Margin="{StaticResource ItemMargin}"
HorizontalContentAlignment="Stretch"
Header="Keyboard shortcut">
<ContentControl
Content="{x:Bind KeyBindings, Mode=OneWay}"
ContentTemplateSelector="{StaticResource TerminalKeybindTemplateSelector}"
FontSize="18"
FontWeight="Light" />
</controls:HeaderedContentControl>
</StackPanel>
</Grid>
@@ -251,4 +380,4 @@
</ContentControl>
</ScrollViewer>
</Grid>
</Page>
</Page>
@@ -2,16 +2,20 @@
x:Class="FluentTerminal.App.Views.SettingsPages.TerminalSettings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:FluentTerminal.App.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<converters:ToolTipValueToPixelConverter x:Key="ToolTipValueToPixelConverter" />
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="24,00,0,24">
<Grid Grid.Row="1" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
@@ -34,68 +38,93 @@
Label="Restore Defaults" />
</CommandBar>
</Grid>
<ScrollViewer Grid.Row="2" Margin="24,0,0,0">
<StackPanel>
<ScrollViewer Grid.Row="2">
<StackPanel Margin="{StaticResource ContainerMargin}">
<ComboBox
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Font family"
ItemsSource="{x:Bind ViewModel.Fonts}"
SelectedValue="{x:Bind ViewModel.FontFamily, Mode=TwoWay}" />
<ComboBox
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Font size"
ItemsSource="{x:Bind ViewModel.Sizes}"
SelectedValue="{x:Bind ViewModel.FontSize, Mode=TwoWay}" />
<ToggleSwitch
Margin="{StaticResource ItemMargin}"
Header="Bold text"
IsOn="{x:Bind ViewModel.BoldText, Mode=TwoWay}" />
<Slider
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Background opacity"
Maximum="1"
Minimum="0"
StepFrequency="0.01"
Value="{x:Bind ViewModel.BackgroundOpacity, Mode=TwoWay}" />
<Slider
Width="200"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Padding"
Maximum="64"
Minimum="0"
StepFrequency="1"
ThumbToolTipValueConverter="{StaticResource ToolTipValueToPixelConverter}"
Value="{x:Bind ViewModel.Padding, Mode=TwoWay}" />
<TextBox
Width="200"
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Scrollback limit"
KeyDown="ScrollBackTextBoxOnKeyDown"
Text="{x:Bind ViewModel.ScrollBackLimit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ToggleSwitch
Margin="0,0,0,24"
Margin="{StaticResource ItemMargin}"
Header="Blinking cursor"
IsOn="{x:Bind ViewModel.CursorBlink, Mode=TwoWay}" />
<TextBlock Margin="0,0,0,8" Text="Cursor style" />
<RadioButton
Content="Block"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BlockIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Underline"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.UnderlineIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Bar"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BarIsSelected, Mode=TwoWay}" />
<TextBlock Margin="0,24,0,8" Text="Scrollbar style" />
<RadioButton
Content="Hidden"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.HiddenIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Auto hiding"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.AutoHidingIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Visible"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.VisibleIsSelected, Mode=TwoWay}" />
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Cursor style" />
<RadioButton
Content="Block"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BlockIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Underline"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.UnderlineIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Bar"
GroupName="Cursor"
IsChecked="{x:Bind ViewModel.BarIsSelected, Mode=TwoWay}" />
</StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<TextBlock Margin="0,0,0,8" Text="Scrollbar style" />
<RadioButton
Content="Hidden"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.HiddenIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Auto hiding"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.AutoHidingIsSelected, Mode=TwoWay}" />
<RadioButton
Content="Visible"
GroupName="ScrollBar"
IsChecked="{x:Bind ViewModel.VisibleIsSelected, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>
</Page>
@@ -28,16 +28,13 @@
Grid.RowSpan="3"
Grid.Column="0"
Background="{ThemeResource SystemControlAcrylicWindowBrush}" />
<Grid
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="0,0,0,24">
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="{StaticResource HeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="24,10,0,0"
Margin="0,10,0,0"
VerticalAlignment="Center"
FontSize="28"
Style="{StaticResource TitleTextBlockStyle}"
@@ -63,6 +60,12 @@
<FontIcon FontFamily="Segoe MDL2 Assets" Glyph="&#xEE71;" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton
Name="CloneButton"
Command="{x:Bind ViewModel.CloneCommand, Mode=OneWay}"
CommandParameter="{x:Bind ViewModel.SelectedTheme, Mode=OneWay}"
Icon="Copy"
Label="Clone" />
<AppBarButton
Name="EditButton"
Command="{x:Bind ViewModel.SelectedTheme.EditCommand, Mode=OneWay}"
@@ -97,7 +100,7 @@
Grid.Row="2"
Grid.Column="0"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Margin="{StaticResource ItemMargin}">
<ListView ItemsSource="{x:Bind ViewModel.Themes}" SelectedItem="{x:Bind ViewModel.SelectedTheme, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:ThemeViewModel">
@@ -169,15 +172,16 @@
<ContentControl
Grid.Row="2"
Grid.Column="1"
Margin="24,0,0,0"
HorizontalContentAlignment="Stretch"
Content="{x:Bind ViewModel.SelectedTheme, Mode=TwoWay}"
RequestedTheme="{x:Bind ViewModel.SelectedTheme.Background, Mode=OneWay, Converter={StaticResource BackgroundToApplicationThemeConverter}}">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="viewmodels:ThemeViewModel">
<ScrollViewer Padding="0,0,24,0" VerticalScrollBarVisibility="Auto">
<StackPanel>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="{StaticResource ContainerMargin}">
<!-- VIEW THEME -->
<TextBlock
Margin="{StaticResource ItemMargin}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Name, Mode=OneWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
@@ -185,22 +189,42 @@
<SolidColorBrush Color="{x:Bind Foreground, Mode=OneWay}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{x:Bind Author, Mode=OneWay}" />
<TextBlock
Margin="{StaticResource ItemMargin}"
FontSize="14"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Author, Mode=OneWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource FalseToVisibleConverter}}">
<TextBlock.Foreground>
<SolidColorBrush Color="{x:Bind Foreground, Mode=OneWay}" />
</TextBlock.Foreground>
</TextBlock>
<!-- EDIT THEME -->
<TextBox
Width="200"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Name"
Text="{x:Bind Name, Mode=TwoWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<TextBox
Margin="0,24,0,0"
Width="200"
Margin="{StaticResource ItemMargin}"
HorizontalAlignment="Left"
Header="Author"
Text="{x:Bind Author, Mode=TwoWay}"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<Grid Margin="0,48,0,0">
<Grid Margin="{StaticResource ItemMargin}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="Border">
@@ -209,7 +233,16 @@
<Setter Property="Height" Value="24" />
</Style>
</Grid.Resources>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Grid.Row="0" Text="Color palette" />
<TextBlock
Grid.Row="1"
Margin="0,0,0,8"
FontSize="14"
FontStyle="Italic"
Foreground="LightGray"
Text="Click on a color to edit"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<StackPanel Grid.Row="2" Orientation="Horizontal">
<views:TerminalColorPicker
ColorName="Black"
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
@@ -243,7 +276,7 @@
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
SelectedColor="{x:Bind White, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=false}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<StackPanel Grid.Row="3" Orientation="Horizontal">
<views:TerminalColorPicker
ColorName="Bright Black"
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
@@ -277,7 +310,7 @@
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
SelectedColor="{x:Bind BrightWhite, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=false}" />
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<StackPanel Grid.Row="4" Orientation="Horizontal">
<views:TerminalColorPicker
ColorName="Foreground"
EnableEditing="{x:Bind InEditMode, Mode=OneWay}"
@@ -300,11 +333,6 @@
IsAlphaEnabled="True"
SelectedColor="{x:Bind Selection, Mode=TwoWay, Converter={StaticResource StringToColorConverter}, ConverterParameter=true}" />
</StackPanel>
<TextBlock
Grid.Row="3"
Margin="0,6,0,0"
Text="Click on a color to edit"
Visibility="{x:Bind InEditMode, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
</Grid>
</StackPanel>
</ScrollViewer>
@@ -312,4 +340,4 @@
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Page>
</Page>
@@ -41,6 +41,7 @@ namespace FluentTerminal.App.Views.SettingsPages
SaveButton.RequestedTheme = theme;
CancelButton.RequestedTheme = theme;
ExportButton.RequestedTheme = theme;
CloneButton.RequestedTheme = theme;
ContrastHelper.SetTitleBarButtonsForTheme(theme);
}
+2 -1
View File
@@ -151,6 +151,7 @@
</ToggleMenuFlyoutItem.Icon>
</ToggleMenuFlyoutItem>
</MenuFlyoutSubItem>
<MenuFlyoutItem Command="{x:Bind EditTitleCommand}" Text="Edit Title" />
<MenuFlyoutSeparator />
<MenuFlyoutItem Command="{x:Bind CloseCommand}" Text="Close" />
</MenuFlyout>
@@ -176,7 +177,7 @@
<Grid
x:Name="CloseButton"
Grid.Column="2"
Width="32"
Width="32"
RelativePanel.AlignRightWithPanel="True">
<Button
HorizontalAlignment="Center"
@@ -9,10 +9,10 @@
mc:Ignorable="d">
<Button
x:Name="Root"
Background="{x:Bind ButtonBackground, Mode=OneWay}"
IsEnabled="{x:Bind EnableEditing, Mode=OneWay}"
Style="{StaticResource ColorPreviewButtonStyle}"
ToolTipService.ToolTip="{x:Bind ColorName}"
Background="{x:Bind ButtonBackground, Mode=OneWay}">
ToolTipService.ToolTip="{x:Bind ColorName}">
<!--<Button.Background>
<SolidColorBrush Color="{x:Bind VisibleColor, Mode=OneWay}" />
</Button.Background>-->
@@ -7,7 +7,7 @@ namespace FluentTerminal.App.Views
{
public sealed partial class TerminalColorPicker : UserControl
{
bool colorIsNull = false;
private bool _colorIsNull;
public static readonly DependencyProperty ColorNameProperty =
DependencyProperty.Register(nameof(ColorName), typeof(string), typeof(TerminalColorPicker), new PropertyMetadata(null));
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Views
{
if (s is TerminalColorPicker terminalColorPicker)
{
terminalColorPicker.colorIsNull = ((Color?)e.NewValue) == null;
terminalColorPicker._colorIsNull = ((Color?)e.NewValue) == null;
// When leading the color, or changing the selected color (happens on page load)
// only set the picker (triggering the event action later) if the color is non-null
@@ -0,0 +1,26 @@
using FluentTerminal.App.ViewModels.Settings;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public class TerminalKeybindTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultTemplate { get; set; }
public DataTemplate KeyBindingsTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
protected override DataTemplate SelectTemplateCore(object item)
{
if (item is KeyBindingsViewModel keyBindings)
{
return keyBindings.KeyBindings.Count == 0 ? DefaultTemplate : KeyBindingsTemplate;
}
return null;
}
}
}
@@ -0,0 +1,27 @@
using FluentTerminal.Models;
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public class TerminalThemeTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultTemplate { get; set; }
public DataTemplate ThemeTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
protected override DataTemplate SelectTemplateCore(object item)
{
if (item is TerminalTheme theme)
{
return theme.Id == Guid.Empty ? DefaultTemplate : ThemeTemplate;
}
return null;
}
}
}
+2 -2
View File
@@ -12,7 +12,7 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid x:Name="WebViewContainer" Grid.Row="0" />
<Grid x:Name="TerminalContainer" Grid.Row="0" />
<Grid
Grid.Row="0"
Margin="12"
@@ -38,7 +38,7 @@
Name="SearchTextBox"
Width="300"
Margin="6,0,6,0"
KeyUp="SearchTextBox_KeyUp"
KeyUp="OnSearchTextBoxKeyUp"
PlaceholderText="Enter search term"
RelativePanel.RightOf="SearchLabel"
Text="{x:Bind ViewModel.SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
+30 -251
View File
@@ -1,241 +1,68 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.RuntimeComponent.Enums;
using FluentTerminal.RuntimeComponent.Interfaces;
using FluentTerminal.RuntimeComponent.WebAllowedObjects;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using FluentTerminal.App.ViewModels;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
using Windows.System;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public sealed partial class TerminalView : UserControl, ITerminalView, ITerminalEventListener
public sealed partial class TerminalView : UserControl
{
private MenuFlyoutItem _copyMenuItem;
private BlockingCollection<Action> _dispatcherJobs;
private readonly SemaphoreSlim _loaded;
private MenuFlyoutItem _pasteMenuItem;
private WebView _webView;
private readonly ITerminalView _terminalView;
public TerminalView(TerminalViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.SearchStarted += OnSearchStarted;
ViewModel.Activated += OnActivated;
ViewModel.ThemeChanged += OnThemeChanged;
ViewModel.OptionsChanged += OnOptionsChanged;
ViewModel.KeyBindingsChanged += OnKeyBindingsChanged;
ViewModel.FindNextRequested += OnFindNextRequested;
ViewModel.FindPreviousRequested += OnFindPreviousRequested;
InitializeComponent();
Loaded += OnLoaded;
_loaded = new SemaphoreSlim(0, 1);
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(true));
return settings;
};
StartMediatorTask();
//_terminalView = new XtermTerminalView();
_terminalView = new VtNetCoreTerminalView();
TerminalContainer.Children.Add((UIElement)_terminalView);
_terminalView.Initialize(ViewModel);
}
public event EventHandler<Command> KeyboardCommandReceived;
public event EventHandler<TerminalSize> TerminalSizeChanged;
public event EventHandler<string> TerminalTitleChanged;
public TerminalViewModel ViewModel { get; }
public Task ChangeOptions(TerminalOptions options)
private async void OnActivated(object sender, EventArgs e)
{
var serialized = JsonConvert.SerializeObject(options);
return ExecuteScriptAsync($"changeOptions('{serialized}')");
}
public Task ChangeTheme(TerminalColors theme)
{
var serialized = JsonConvert.SerializeObject(theme);
return ExecuteScriptAsync($"changeTheme('{serialized}')");
}
public Task ChangeKeyBindings(IEnumerable<KeyBinding> keyBindings)
{
var serialized = JsonConvert.SerializeObject(keyBindings);
return ExecuteScriptAsync($"changeKeyBindings('{serialized}')");
}
public void Close()
{
_webView?.Navigate(new Uri("about:blank"));
_webView = null;
}
public Task ConnectToSocket(string url)
{
return ExecuteScriptAsync($"connectToWebSocket('{url}');");
}
public async Task<TerminalSize> CreateTerminal(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings)
{
var serializedOptions = JsonConvert.SerializeObject(options);
var serializedTheme = JsonConvert.SerializeObject(theme);
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}')").ConfigureAwait(true);
return JsonConvert.DeserializeObject<TerminalSize>(size);
}
public Task<string> ExecuteScriptAsync(string script)
{
try
if (_terminalView != null)
{
return _webView.InvokeScriptAsync("eval", new[] { script }).AsTask();
}
catch (Exception e)
{
Debug.WriteLine($"Exception while running:\n \"{script}\"\n\n {e}");
}
return Task.FromResult(string.Empty);
}
public async Task FocusTerminal()
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
await _terminalView.FocusTerminal().ConfigureAwait(true);
}
}
public async Task FocusWebView()
private async void OnFindNextRequested(object sender, string e)
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
}
await _terminalView.FindNext(e).ConfigureAwait(true);
}
public Task<string> GetSelection() => ExecuteScriptAsync("term.getSelection()");
public void OnKeyboardCommand(string command)
private async void OnFindPreviousRequested(object sender, string e)
{
_dispatcherJobs.Add(() => KeyboardCommandReceived?.Invoke(this, Enum.Parse<Command>(command, true)));
await _terminalView.FindPrevious(e).ConfigureAwait(true);
}
public void OnTerminalResized(int columns, int rows)
private async void OnKeyBindingsChanged(object sender, EventArgs e)
{
_dispatcherJobs.Add(() => TerminalSizeChanged?.Invoke(this, new TerminalSize { Columns = columns, Rows = rows }));
await _terminalView.ChangeKeyBindings().ConfigureAwait(true);
}
public void OnTitleChanged(string title)
private async void OnOptionsChanged(object sender, Models.TerminalOptions e)
{
_dispatcherJobs.Add(() => TerminalTitleChanged?.Invoke(this, title));
await _terminalView.ChangeOptions(e).ConfigureAwait(true);
}
public Task FindNext(string searchText)
private void OnSearchStarted(object sender, EventArgs e)
{
return ExecuteScriptAsync($"findNext('{searchText}')");
SearchTextBox.Focus(FocusState.Programmatic);
}
public Task FindPrevious(string searchText)
{
return ExecuteScriptAsync($"findPrevious('{searchText}')");
}
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
{
_loaded.Release();
}
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
var bridge = new TerminalBridge(this);
_webView.AddWebAllowedObject("terminalBridge", bridge);
}
private void Copy_Click(object sender, RoutedEventArgs e)
{
OnKeyboardCommand(nameof(Command.Copy));
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
if (WebViewContainer.Children.Any())
{
return;
}
_webView = new WebView(WebViewExecutionMode.SeparateThread)
{
DefaultBackgroundColor = Colors.Transparent
};
WebViewContainer.Children.Add(_webView);
_webView.NavigationCompleted += _webView_NavigationCompleted;
_webView.NavigationStarting += _webView_NavigationStarting;
_copyMenuItem = new MenuFlyoutItem { Text = "Copy" };
_copyMenuItem.Click += Copy_Click;
_pasteMenuItem = new MenuFlyoutItem { Text = "Paste" };
_pasteMenuItem.Click += Paste_Click;
_webView.ContextFlyout = new MenuFlyout
{
Items = { _copyMenuItem, _pasteMenuItem }
};
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
await _loaded.WaitAsync().ConfigureAwait(true);
_webView.Focus(FocusState.Programmatic);
await ViewModel.OnViewIsReady(this).ConfigureAwait(true);
}
private void Paste_Click(object sender, RoutedEventArgs e)
{
OnKeyboardCommand(nameof(Command.Paste));
}
private void StartMediatorTask()
{
_dispatcherJobs = new BlockingCollection<Action>();
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
Task.Run(async () =>
{
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
{
try
{
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => job.Invoke());
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
});
}
private void SearchTextBox_KeyUp(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
private void OnSearchTextBoxKeyUp(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Escape)
{
@@ -247,57 +74,9 @@ namespace FluentTerminal.App.Views
}
}
public void FocusSearchTextBox()
private async void OnThemeChanged(object sender, Models.TerminalTheme e)
{
SearchTextBox.Focus(FocusState.Programmatic);
}
public void OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection)
{
_dispatcherJobs.Add(() =>
{
if (mouseButton == MouseButton.Middle)
{
if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.Paste)
{
OnKeyboardCommand(nameof(Command.Paste));
}
}
else if (mouseButton == MouseButton.Right)
{
if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.Paste)
{
OnKeyboardCommand(nameof(Command.Paste));
}
}
});
}
public async void OnSelectionChanged(string selection)
{
if (ViewModel.ApplicationSettings.CopyOnSelect && !ViewModel.ShowSearchPanel)
{
_dispatcherJobs.Add(async () =>
{
ViewModel.CopyText(selection);
await ExecuteScriptAsync("term.clearSelection()");
});
}
}
private void ShowContextMenu(int x, int y, bool terminalHasSelection)
{
var flyout = (MenuFlyout)_webView.ContextFlyout;
_copyMenuItem.IsEnabled = terminalHasSelection;
flyout.ShowAt(_webView, new Point(x, y));
await _terminalView.ChangeTheme(e).ConfigureAwait(true);
}
}
}
@@ -0,0 +1,15 @@
<UserControl
x:Class="FluentTerminal.App.Views.VtNetCoreTerminalView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
IsTabStop="True"
mc:Ignorable="d">
<Grid>
<canvas:CanvasControl x:Name="Canvas" ClearColor="Transparent" />
</Grid>
</UserControl>
@@ -0,0 +1,660 @@
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using VtNetCore.VirtualTerminal;
using VtNetCore.VirtualTerminal.Layout;
using VtNetCore.XTermParser;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
namespace FluentTerminal.App.Views
{
public sealed partial class VtNetCoreTerminalView : UserControl, ITerminalView
{
private readonly int _blinkHideMs = 300;
private readonly int _blinkShowMs = 600;
private readonly DataConsumer _consumer;
private readonly VirtualTerminalController _terminal;
private readonly DispatcherTimer blinkDispatcher;
private double _characterHeight = -1;
private double _characterWidth = -1;
private int _columns = -1;
private TextPosition _mouseOver = new TextPosition();
private int _rows = -1;
private bool _scrolling;
private bool _selecting;
private TextRange _textSelection;
public VtNetCoreTerminalView()
{
this.InitializeComponent();
_terminal = new VirtualTerminalController();
_consumer = new DataConsumer(_terminal);
ViewTop = _terminal.ViewPort.TopRow;
_terminal.SendData += OnSendData;
blinkDispatcher = new DispatcherTimer();
blinkDispatcher.Tick += BlinkTimerHandler;
blinkDispatcher.Interval = TimeSpan.FromMilliseconds(Math.Min(150, GCD(_blinkShowMs, _blinkHideMs)));
blinkDispatcher.Start();
Canvas.Draw += OnDraw;
KeyDown += TerminalKeyDown;
Tapped += OnTapped;
GotFocus += TerminalGotFocus;
LostFocus += TerminalLostFocus;
PointerWheelChanged += OnWheelChanged;
PointerMoved += TerminalPointerMoved;
PointerExited += TerminalPointerExited;
PointerPressed += TerminalPointerPressed;
PointerReleased += TerminalPointerReleased;
}
public int MaxScrollValue => _terminal.ViewPort.TopRow;
public TextPosition MousePressedAt { get; set; }
public int ScrollValue
{
get { return ViewTop; }
set
{
_scrolling = true;
ViewTop = value;
Canvas.Invalidate();
}
}
public TerminalViewModel ViewModel { get; private set; }
public int ViewTop { get; set; }
public Task ChangeKeyBindings()
{
return Task.CompletedTask;
}
public Task ChangeOptions(TerminalOptions options)
{
return Task.CompletedTask;
}
public Task ChangeTheme(TerminalTheme theme)
{
return Task.CompletedTask;
}
public Task FindNext(string searchText)
{
return Task.CompletedTask;
}
public Task FindPrevious(string searchText)
{
return Task.CompletedTask;
}
public Task FocusTerminal()
{
return Task.CompletedTask;
}
public Color GetSolidColorBrush(string hex)
{
byte a = 255; // (byte)(Convert.ToUInt32(hex.Substring(0, 2), 16));
byte r = (byte)(Convert.ToUInt32(hex.Substring(1, 2), 16));
byte g = (byte)(Convert.ToUInt32(hex.Substring(3, 2), 16));
byte b = (byte)(Convert.ToUInt32(hex.Substring(5, 2), 16));
return Color.FromArgb(a, r, g, b);
}
public async Task Initialize(TerminalViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
ViewModel.Terminal.Closed += Terminal_Closed;
ViewModel.Terminal.RegisterSelectedTextCallback(() => Task.FromResult(_terminal.GetText(_textSelection.Start.Column, _textSelection.Start.Row, _textSelection.End.Column, _textSelection.End.Row)));
_terminal.WindowTitleChanged += OnWindowTitleChanged;
_terminal.SizeChanged += OnSizeChanged;
var settings = ViewModel.SettingsService.GetApplicationSettings();
var options = ViewModel.SettingsService.GetTerminalOptions();
Canvas.FontFamily = new FontFamily(options.FontFamily);
Canvas.FontSize = options.FontSize;
Canvas.Margin = new Thickness(options.Padding, options.Padding, options.Padding, options.Padding);
var sessionType = SessionType.Unknown;
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
{
sessionType = SessionType.WinPty;
}
else
{
sessionType = SessionType.ConPty;
}
await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, new TerminalSize { Columns = 80, Rows = 30 }, sessionType).ConfigureAwait(true);
}
private void BlinkTimerHandler(object sender, object e)
{
Canvas.Invalidate();
}
private bool BlinkVisible()
{
var blinkCycle = _blinkShowMs + _blinkHideMs;
return (DateTime.Now.Subtract(DateTime.MinValue).Milliseconds % blinkCycle) < _blinkHideMs;
}
private bool ControlPressed()
{
return (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Control) & CoreVirtualKeyStates.Down) != 0;
}
private void CoreWindow_CharacterReceived(CoreWindow sender, CharacterReceivedEventArgs args)
{
var ch = char.ConvertFromUtf32((int)args.KeyCode);
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
// I lookup whether KeyPressed should handle the key here or there.
var code = _terminal.GetKeySequence(ch, false, false);
if (code == null)
{
args.Handled = _terminal.KeyPressed(ch, false, false);
}
}
private long GCD(long a, long b)
{
a = Math.Abs(a);
b = Math.Abs(b);
// Pull out remainders.
for (; ; )
{
long remainder = a % b;
if (remainder == 0) return b;
a = b;
b = remainder;
};
}
private void OnDraw(CanvasControl sender, CanvasDrawEventArgs args)
{
var drawingSession = args.DrawingSession;
var textFormat =
new CanvasTextFormat
{
FontSize = Convert.ToSingle(Canvas.FontSize),
FontFamily = Canvas.FontFamily.Source,
FontWeight = Canvas.FontWeight,
WordWrapping = CanvasWordWrapping.NoWrap
};
ProcessTextFormat(drawingSession, textFormat);
var showBlink = BlinkVisible();
var spans = default(List<LayoutRow>);
var cursorPosition = default(TextPosition);
var showCursor = false;
var cursorColor = Colors.Green;
var TerminalTop = -1;
lock (_terminal)
{
TerminalTop = _terminal.ViewPort.TopRow;
if (_terminal.ViewPort.CursorPosition.Row > _rows)
{
throw new Exception("We should never be here");
}
spans = _terminal.ViewPort.GetPageSpans(ViewTop, _rows, _columns, _textSelection);
showCursor = _terminal.CursorState.ShowCursor;
cursorPosition = _terminal.ViewPort.CursorPosition.Clone();
cursorColor = GetSolidColorBrush(_terminal.CursorState.Attributes.WebColor);
}
if (!_scrolling && ViewTop != TerminalTop)
{
ViewTop = TerminalTop;
}
var defaultTransform = drawingSession.Transform;
PaintBackgroundLayer(drawingSession, spans);
PaintTextLayer(drawingSession, spans, textFormat, showBlink);
if (showCursor)
{
PaintCursor(drawingSession, spans, textFormat, cursorPosition.OffsetBy(0, TerminalTop - ViewTop), cursorColor);
}
drawingSession.Transform = defaultTransform;
}
private void OnSendData(object sender, SendDataEventArgs e)
{
ViewModel.Terminal.Write(Encoding.UTF8.GetString(e.Data));
}
private async void OnSizeChanged(object sender, SizeEventArgs args)
{
await ViewModel.Terminal.SetSize(new TerminalSize { Rows = args.Height, Columns = args.Width }).ConfigureAwait(true);
}
private void OnWindowTitleChanged(object sender, TextEventArgs args)
{
ViewModel.Terminal.SetTitle(args.Text);
}
private void PaintBackgroundLayer(CanvasDrawingSession drawingSession, List<LayoutRow> spans)
{
double lineY = 0;
foreach (var textRow in spans)
{
drawingSession.Transform =
Matrix3x2.CreateScale(
(float)(textRow.DoubleWidth ? 2.0 : 1.0), // Scale double width
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0) // Scale double high
);
var drawY =
(lineY - (textRow.DoubleHeightBottom ? _characterHeight : 0)) * // Offset position upwards for bottom of double high char
((textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0); // Scale position for double height
double drawX = 0;
foreach (var textSpan in textRow.Spans)
{
var bounds =
new Rect(
drawX,
drawY,
_characterWidth * (textSpan.Text.Length) + 0.9,
_characterHeight + 0.9
);
if (textSpan.BackgroundColor != "#000000") // todo set theme
{
drawingSession.FillRectangle(bounds, GetSolidColorBrush(textSpan.BackgroundColor));
}
drawX += _characterWidth * (textSpan.Text.Length);
}
lineY += _characterHeight;
}
}
private void PaintCursor(CanvasDrawingSession drawingSession, List<LayoutRow> spans, CanvasTextFormat textFormat, TextPosition cursorPosition, Color cursorColor)
{
var cursorY = cursorPosition.Row;
if (cursorY < 0 || cursorY >= _rows)
return;
var drawX = cursorPosition.Column * _characterWidth;
var drawY = (cursorY * _characterHeight);
if (cursorY < spans.Count)
{
var textRow = spans[cursorY];
drawingSession.Transform =
Matrix3x2.CreateTranslation(
1.0f,
(float)(textRow.DoubleHeightBottom ? -_characterHeight : 0)
) *
Matrix3x2.CreateScale(
(float)(textRow.DoubleWidth ? 2.0 : 1.0),
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0)
);
drawY *= (textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0;
}
var cursorRect = new Rect(
drawX,
drawY,
_characterWidth,
_characterHeight + 0.9
);
drawingSession.DrawRectangle(cursorRect, cursorColor);
}
private void PaintTextLayer(CanvasDrawingSession drawingSession, List<LayoutRow> spans, CanvasTextFormat textFormat, bool showBlink)
{
var dipToDpiRatio = Windows.Graphics.Display.DisplayInformation.GetForCurrentView().LogicalDpi / 96;
double lineY = 0;
foreach (var textRow in spans)
{
drawingSession.Transform =
Matrix3x2.CreateScale(
(float)(textRow.DoubleWidth ? 2.0 : 1.0), // Scale double width
(float)(textRow.DoubleHeightBottom | textRow.DoubleHeightTop ? 2.0 : 1.0) // Scale double high
);
var drawY =
(lineY - (textRow.DoubleHeightBottom ? _characterHeight : 0)) * // Offset position upwards for bottom of double high char
((textRow.DoubleHeightBottom | textRow.DoubleHeightTop) ? 0.5 : 1.0); // Scale position for double height
double drawX = 0;
foreach (var textSpan in textRow.Spans)
{
var runWidth = _characterWidth * (textSpan.Text.Length);
if (textSpan.Hidden || (textSpan.Blink && !showBlink))
{
drawX += runWidth;
continue;
}
var color = GetSolidColorBrush(textSpan.ForgroundColor);
textFormat.FontWeight = textSpan.Bold ? FontWeights.Bold : FontWeights.Light;
var textLayout = new CanvasTextLayout(drawingSession, textSpan.Text, textFormat, 0.0f, 0.0f);
drawingSession.DrawTextLayout(textLayout, (float)drawX, (float)drawY, color);
if (textSpan.Underline)
{
// TODO : Come up with a better means of identifying line weight and offset
var underlineOffset = textLayout.LineMetrics[0].Baseline * dipToDpiRatio * 1.07;
drawingSession.DrawLine(new Vector2((float)drawX, (float)(drawY + underlineOffset)), new Vector2((float)(drawX + runWidth), (float)(drawY + underlineOffset)), color);
}
drawX += _characterWidth * (textSpan.Text.Length);
}
lineY += _characterHeight;
}
}
private void ProcessTextFormat(CanvasDrawingSession drawingSession, CanvasTextFormat format)
{
CanvasTextLayout textLayout = new CanvasTextLayout(drawingSession, "\u2560", format, 0.0f, 0.0f);
if (_characterWidth != textLayout.DrawBounds.Width || _characterHeight != textLayout.DrawBounds.Height)
{
_characterWidth = textLayout.DrawBounds.Width;
_characterHeight = textLayout.DrawBounds.Height;
}
int columns = Convert.ToInt32(Math.Floor(Canvas.RenderSize.Width / _characterWidth));
int rows = Convert.ToInt32(Math.Floor(Canvas.RenderSize.Height / _characterHeight));
if (_columns != columns || _rows != rows)
{
_columns = columns;
_rows = rows;
ViewModel.Terminal.SetSize(new TerminalSize { Columns = columns, Rows = rows });
}
}
private bool ShiftPressed()
{
return (Window.Current.CoreWindow.GetKeyState(Windows.System.VirtualKey.Shift) & CoreVirtualKeyStates.Down) != 0;
}
private void Terminal_Closed(object sender, EventArgs e)
{
}
private void Terminal_OutputReceived(object sender, string e)
{
var oldTopRow = _terminal.ViewPort.TopRow;
_consumer.Push(Encoding.UTF8.GetBytes(e));
if (_terminal.Changed)
{
_terminal.ClearChanges();
if (oldTopRow != _terminal.ViewPort.TopRow && oldTopRow >= ViewTop)
{
ViewTop = _terminal.ViewPort.TopRow;
}
Canvas.Invalidate();
}
}
private void TerminalGotFocus(object sender, RoutedEventArgs e)
{
Window.Current.CoreWindow.CharacterReceived += CoreWindow_CharacterReceived;
_terminal?.FocusIn();
}
private void TerminalKeyDown(object sender, KeyRoutedEventArgs e)
{
_scrolling = false;
// Since I get the same key twice in TerminalKeyDown and in CoreWindow_CharacterReceived
// I lookup whether KeyPressed should handle the key here or there.
var code = _terminal.GetKeySequence(e.Key.ToString(), ControlPressed(), ShiftPressed());
if (code != null)
e.Handled = _terminal.KeyPressed(e.Key.ToString(), ControlPressed(), ShiftPressed());
if (ViewTop != _terminal.ViewPort.TopRow)
{
ViewTop = _terminal.ViewPort.TopRow;
Canvas.Invalidate();
}
}
private void TerminalLostFocus(object sender, RoutedEventArgs e)
{
Window.Current.CoreWindow.CharacterReceived -= CoreWindow_CharacterReceived;
_terminal?.FocusOut();
}
private void TerminalPointerExited(object sender, PointerRoutedEventArgs e)
{
_mouseOver = null;
Canvas.Invalidate();
}
private void TerminalPointerMoved(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
var position = PointToTextPosition(pointer.Position);
var textPosition = position.OffsetBy(0, ViewTop);
if ((_terminal.UseAllMouseTracking || _terminal.CellMotionMouseTracking) && position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
{
var button =
pointer.Properties.IsLeftButtonPressed ? 0 :
pointer.Properties.IsRightButtonPressed ? 1 :
pointer.Properties.IsMiddleButtonPressed ? 2 :
3; // No button
_terminal.MouseMove(position.Column, position.Row, button, ControlPressed(), ShiftPressed());
if (button == 3 && !_terminal.UseAllMouseTracking)
{
return;
}
}
if (_mouseOver != null && _mouseOver == position)
{
return;
}
_mouseOver = position;
if (pointer.Properties.IsLeftButtonPressed && MousePressedAt != null)
{
TextRange newSelection;
if (MousePressedAt != textPosition)
{
if (MousePressedAt <= textPosition)
{
newSelection = new TextRange
{
Start = MousePressedAt,
End = textPosition.OffsetBy(-1, 0)
};
}
else
{
newSelection = new TextRange
{
Start = textPosition,
End = MousePressedAt
};
}
_selecting = true;
if (_textSelection != newSelection)
{
_textSelection = newSelection;
Canvas.Invalidate();
}
}
}
}
private void TerminalPointerPressed(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
var position = PointToTextPosition(pointer.Position);
var textPosition = position.OffsetBy(0, ViewTop);
if (!_terminal.MouseTrackingEnabled)
{
if (pointer.Properties.IsLeftButtonPressed)
MousePressedAt = textPosition;
//else if (pointer.Properties.IsRightButtonPressed)
//PasteClipboard();
}
if (position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
{
var button =
pointer.Properties.IsLeftButtonPressed ? 0 :
pointer.Properties.IsRightButtonPressed ? 1 :
pointer.Properties.IsMiddleButtonPressed ? 2 :
3; // No button
_terminal.MousePress(position.Column, position.Row, button, ControlPressed(), ShiftPressed());
}
}
private void TerminalPointerReleased(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
var position = PointToTextPosition(pointer.Position);
var textPosition = position.OffsetBy(0, ViewTop);
if (!pointer.Properties.IsLeftButtonPressed)
{
if (_selecting)
{
MousePressedAt = null;
_selecting = false;
var captured = _terminal.GetText(_textSelection.Start.Column, _textSelection.Start.Row, _textSelection.End.Column, _textSelection.End.Row);
var dataPackage = new DataPackage();
dataPackage.SetText(captured);
dataPackage.Properties.EnterpriseId = "FluentTerminal";
try
{
Clipboard.SetContent(dataPackage);
}
catch (Exception exception)
{
Debug.WriteLine("Failed to post to clipboard: " + exception.Message);
}
}
else
{
_textSelection = null;
Canvas.Invalidate();
}
}
if (position.Column >= 0 && position.Row >= 0 && position.Column < _columns && position.Row < _rows)
{
_terminal.MouseRelease(position.Column, position.Row, ControlPressed(), ShiftPressed());
}
}
private void OnTapped(object sender, TappedRoutedEventArgs e)
{
Focus(FocusState.Pointer);
}
private void OnWheelChanged(object sender, PointerRoutedEventArgs e)
{
var pointer = e.GetCurrentPoint(Canvas);
int oldViewTop = ViewTop;
ViewTop -= pointer.Properties.MouseWheelDelta / 40;
_scrolling = true;
if (ViewTop < 0)
{
ViewTop = 0;
}
else if (ViewTop > _terminal.ViewPort.TopRow)
{
ViewTop = _terminal.ViewPort.TopRow;
}
if (oldViewTop != ViewTop)
{
Canvas.Invalidate();
}
}
private TextPosition PointToTextPosition(Point point)
{
var overColumn = (int)Math.Floor(point.X / _characterWidth);
if (overColumn >= _columns)
{
overColumn = _columns - 1;
}
var overRow = (int)Math.Floor(point.Y / _characterHeight);
if (overRow >= _rows)
{
overRow = _rows - 1;
}
return new TextPosition { Column = overColumn, Row = overRow };
}
}
}
@@ -0,0 +1,12 @@
<UserControl
x:Class="FluentTerminal.App.Views.XtermTerminalView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:FluentTerminal.App.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
<Grid x:Name="Root" />
</UserControl>
@@ -0,0 +1,350 @@
using Fleck;
using FluentTerminal.App.ViewModels;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.RuntimeComponent.Enums;
using FluentTerminal.RuntimeComponent.Interfaces;
using FluentTerminal.RuntimeComponent.WebAllowedObjects;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Views
{
public sealed partial class XtermTerminalView : UserControl, IxtermEventListener, ITerminalView
{
private readonly ManualResetEventSlim _connectedEvent;
private readonly SemaphoreSlim _loaded;
private bool _alreadyLoaded;
private InputBuffer _buffer;
private MenuFlyoutItem _copyMenuItem;
private BlockingCollection<Action> _dispatcherJobs;
private MenuFlyoutItem _pasteMenuItem;
private IWebSocketConnection _webSocket;
private WebView _webView;
public XtermTerminalView()
{
_loaded = new SemaphoreSlim(0, 1);
_connectedEvent = new ManualResetEventSlim(false);
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(true));
return settings;
};
InitializeComponent();
StartMediatorTask();
Loaded += XtermTerminalView_Loaded;
GotFocus += XtermTerminalView_GotFocus;
}
public TerminalViewModel ViewModel { get; private set; }
public Task ChangeKeyBindings()
{
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
var profiles = ViewModel.SettingsService.GetShellProfiles();
var serialized = JsonConvert.SerializeObject(FlattenKeyBindings(keyBindings, profiles));
return ExecuteScriptAsync($"changeKeyBindings('{serialized}')");
}
public Task ChangeOptions(TerminalOptions options)
{
var serialized = JsonConvert.SerializeObject(options);
return ExecuteScriptAsync($"changeOptions('{serialized}')");
}
public Task ChangeTheme(TerminalTheme theme)
{
var serialized = JsonConvert.SerializeObject(theme.Colors);
return ExecuteScriptAsync($"changeTheme('{serialized}')");
}
public Task FindNext(string searchText)
{
return ExecuteScriptAsync($"findNext('{searchText}')");
}
public Task FindPrevious(string searchText)
{
return ExecuteScriptAsync($"findPrevious('{searchText}')");
}
public async Task FocusTerminal()
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
}
}
public async Task Initialize(TerminalViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.Terminal.OutputReceived += Terminal_OutputReceived;
ViewModel.Terminal.RegisterSelectedTextCallback(() => ExecuteScriptAsync("term.getSelection()"));
ViewModel.Terminal.Closed += Terminal_Closed;
var options = ViewModel.SettingsService.GetTerminalOptions();
var keyBindings = ViewModel.SettingsService.GetCommandKeyBindings();
var profiles = ViewModel.SettingsService.GetShellProfiles();
var settings = ViewModel.SettingsService.GetApplicationSettings();
var theme = ViewModel.TerminalTheme;
var sessionType = SessionType.Unknown;
if (settings.AlwaysUseWinPty || !ViewModel.ApplicationView.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7))
{
sessionType = SessionType.WinPty;
}
else
{
sessionType = SessionType.ConPty;
}
await _loaded.WaitAsync().ConfigureAwait(true);
var size = await CreateXtermView(options, theme.Colors, FlattenKeyBindings(keyBindings, profiles), sessionType).ConfigureAwait(true);
var port = await ViewModel.TrayProcessCommunicationService.GetAvailablePort().ConfigureAwait(true);
await CreateWebSocketServer(port.Port).ConfigureAwait(true);
_connectedEvent.Wait();
await ViewModel.Terminal.StartShellProcess(ViewModel.ShellProfile, size, sessionType).ConfigureAwait(true);
_webView.Focus(FocusState.Programmatic);
}
void IxtermEventListener.OnKeyboardCommand(string command)
{
if (Enum.TryParse(command, true, out Command commandValue))
{
_dispatcherJobs.Add(() => ViewModel.Terminal.ProcessKeyboardCommand(commandValue.ToString()));
}
else if (Guid.TryParse(command, out Guid shellProfileId))
{
_dispatcherJobs.Add(() => ViewModel.Terminal.ProcessKeyboardCommand(shellProfileId.ToString()));
}
}
void IxtermEventListener.OnMouseClick(MouseButton mouseButton, int x, int y, bool hasSelection)
{
_dispatcherJobs.Add(() =>
{
if (mouseButton == MouseButton.Middle)
{
if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseMiddleClickAction == MouseAction.Paste)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
}
}
else if (mouseButton == MouseButton.Right)
{
if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.ContextMenu)
{
ShowContextMenu(x, y, hasSelection);
}
else if (ViewModel.ApplicationSettings.MouseRightClickAction == MouseAction.Paste)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
}
}
});
}
void IxtermEventListener.OnSelectionChanged(string selection)
{
if (ViewModel.ApplicationSettings.CopyOnSelect && !ViewModel.ShowSearchPanel)
{
_dispatcherJobs.Add(async () =>
{
ViewModel.CopyText(selection);
await ExecuteScriptAsync("term.clearSelection()");
});
}
}
void IxtermEventListener.OnTerminalResized(int columns, int rows)
{
if (_connectedEvent.IsSet) // only propagate after xterm.js is finished with fitting
{
_dispatcherJobs.Add(async () => await ViewModel.Terminal.SetSize(new TerminalSize { Columns = columns, Rows = rows }).ConfigureAwait(true));
}
}
void IxtermEventListener.OnTitleChanged(string title)
{
_dispatcherJobs.Add(() => ViewModel.Terminal.SetTitle(title));
}
private void _webView_NavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
{
_loaded.Release();
}
private void _webView_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
var bridge = new TerminalBridge(this);
_webView.AddWebAllowedObject("terminalBridge", bridge);
}
private void Copy_Click(object sender, RoutedEventArgs e)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Copy));
}
private async Task CreateWebSocketServer(int port)
{
var webSocketUrl = "ws://127.0.0.1:" + port;
var webSocketServer = new WebSocketServer(webSocketUrl);
webSocketServer.Start(socket =>
{
_webSocket = socket;
_buffer = new InputBuffer(_webSocket.Send);
socket.OnOpen = () => _connectedEvent.Set();
socket.OnMessage = message => ViewModel.Terminal.Write(message);
});
await ExecuteScriptAsync($"connectToWebSocket('{webSocketUrl}');").ConfigureAwait(true);
}
private async Task<TerminalSize> CreateXtermView(TerminalOptions options, TerminalColors theme, IEnumerable<KeyBinding> keyBindings, SessionType sessionType)
{
var serializedOptions = JsonConvert.SerializeObject(options);
var serializedTheme = JsonConvert.SerializeObject(theme);
var serializedKeyBindings = JsonConvert.SerializeObject(keyBindings);
var size = await ExecuteScriptAsync($"createTerminal('{serializedOptions}', '{serializedTheme}', '{serializedKeyBindings}', '{sessionType}')").ConfigureAwait(true);
return JsonConvert.DeserializeObject<TerminalSize>(size);
}
private Task<string> ExecuteScriptAsync(string script)
{
try
{
return _webView.InvokeScriptAsync("eval", new[] { script }).AsTask();
}
catch (Exception e)
{
Debug.WriteLine($"Exception while running:\n \"{script}\"\n\n {e}");
}
return Task.FromResult(string.Empty);
}
private IEnumerable<KeyBinding> FlattenKeyBindings(IDictionary<string, ICollection<KeyBinding>> commandKeyBindings, IEnumerable<ShellProfile> profiles)
{
return commandKeyBindings.Values.SelectMany(k => k).Concat(profiles.SelectMany(x => x.KeyBindings));
}
private void Paste_Click(object sender, RoutedEventArgs e)
{
((IxtermEventListener)this).OnKeyboardCommand(nameof(Command.Paste));
}
private void ShowContextMenu(int x, int y, bool terminalHasSelection)
{
var flyout = (MenuFlyout)_webView.ContextFlyout;
_copyMenuItem.IsEnabled = terminalHasSelection;
flyout.ShowAt(_webView, new Point(x, y));
}
private void StartMediatorTask()
{
_dispatcherJobs = new BlockingCollection<Action>();
var dispatcher = CoreApplication.GetCurrentView().CoreWindow.Dispatcher;
Task.Run(async () =>
{
foreach (var job in _dispatcherJobs.GetConsumingEnumerable())
{
try
{
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => job.Invoke());
}
catch (Exception e)
{
Debug.WriteLine(e);
}
}
});
}
private async void Terminal_Closed(object sender, EventArgs e)
{
ViewModel.Terminal.OutputReceived -= Terminal_OutputReceived;
ViewModel.Terminal.Closed -= Terminal_Closed;
_webView?.Navigate(new Uri("about:blank"));
_webView = null;
await ViewModel.Terminal.Close().ConfigureAwait(true);
}
private void Terminal_OutputReceived(object sender, string e)
{
_webSocket.Send(e);
}
private async void XtermTerminalView_GotFocus(object sender, RoutedEventArgs e)
{
if (_webView != null)
{
_webView.Focus(FocusState.Programmatic);
await ExecuteScriptAsync("document.focus();").ConfigureAwait(true);
}
}
private void XtermTerminalView_Loaded(object sender, RoutedEventArgs e)
{
if (_alreadyLoaded)
{
return;
}
if (Root.Children.Any())
{
return;
}
_webView = new WebView(WebViewExecutionMode.SeparateThread)
{
DefaultBackgroundColor = Colors.Transparent
};
Root.Children.Add(_webView);
_webView.NavigationCompleted += _webView_NavigationCompleted;
_webView.NavigationStarting += _webView_NavigationStarting;
_copyMenuItem = new MenuFlyoutItem { Text = "Copy" };
_copyMenuItem.Click += Copy_Click;
_pasteMenuItem = new MenuFlyoutItem { Text = "Paste" };
_pasteMenuItem.Click += Paste_Click;
_webView.ContextFlyout = new MenuFlyout
{
Items = { _copyMenuItem, _pasteMenuItem }
};
_webView.Navigate(new Uri("ms-appx-web:///Client/index.html"));
_alreadyLoaded = true;
}
}
}
+3 -3
View File
@@ -4305,9 +4305,9 @@
"dev": true
},
"xterm": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-3.8.0.tgz",
"integrity": "sha512-rS3HLryuMWbLsv98+jVVSUXCxmoyXPwqwJNC0ad0VSMdXgl65LefPztQVwfurkaF7kM7ZSgM8eJjnJ9kkdoR1w=="
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-3.11.0.tgz",
"integrity": "sha512-VB9+s2Fhig31pNBkbxPnz+/wdCvxdQ6JQ0HZmWDwpULV8iAggzxoyUonB4FR+WW3lj0LNVb/ZaD64rxbw+HB4A=="
},
"y18n": {
"version": "4.0.0",
+1 -1
View File
@@ -14,6 +14,6 @@
"webpack-cli": "^3.1.0"
},
"dependencies": {
"xterm": "^3.8.0"
"xterm": "^3.11.0"
}
}
+1
View File
@@ -5,6 +5,7 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="xterm.css" />
<link rel="stylesheet" href="style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
</head>
<body>
+26 -13
View File
@@ -13,7 +13,7 @@ Terminal.applyAddon(search);
var term, socket;
var terminalContainer = document.getElementById('terminal-container');
function createTerminal(options, theme, keyBindings) {
function createTerminal(options, theme, keyBindings, sessionType) {
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
@@ -29,6 +29,8 @@ function createTerminal(options, theme, keyBindings) {
var terminalOptions = {
fontFamily: options.fontFamily,
fontSize: options.fontSize,
fontWeight: options.boldText ? 'bold' : 'normal',
fontWeightBold: options.boldText ? 'bolder' : 'bold',
cursorStyle: options.cursorStyle,
cursorBlink: options.cursorBlink,
bellStyle: options.bellStyle,
@@ -55,9 +57,15 @@ function createTerminal(options, theme, keyBindings) {
});
term.open(terminalContainer);
term.winptyCompatInit();
term.fit();
if (sessionType === 'WinPty') {
term.winptyCompatInit();
}
term.focus();
setPadding(options.padding);
var resizeTimeout;
window.onresize = function () {
@@ -82,15 +90,15 @@ function createTerminal(options, theme, keyBindings) {
&& keyBinding.shift == e.shiftKey
&& keyBinding.key == e.keyCode) {
if (document.visibilityState == 'visible') {
if (keyBinding.command == 'copy' && term.getSelection() == '') {
if (keyBinding.command == 'Copy' && term.getSelection() == '') {
return true;
}
if (keyBinding.command == 'clear') {
if (keyBinding.command == 'Clear') {
term.clearSelection();
term.clear();
return false;
}
if (keyBinding.command == 'selectAll') {
if (keyBinding.command == 'SelectAll') {
term.selectAll();
return false;
}
@@ -137,19 +145,24 @@ function changeOptions(options) {
term.setOption('cursorStyle', options.cursorStyle);
term.setOption('fontFamily', options.fontFamily);
term.setOption('fontSize', options.fontSize);
term.setOption('fontWeight', options.boldText ? 'bold' : 'normal');
term.setOption('fontWeightBold', options.boldText ? 'bolder' : 'bold');
term.setOption('scrollback', options.scrollBackLimit);
setScrollBarStyle(options.scrollBarStyle);
setPadding(options.padding);
}
function setScrollBarStyle(scrollBarStyle) {
if (scrollBarStyle == 'hidden') {
document.getElementById('terminal-container').style['-ms-overflow-style'] = 'none';
} else if (scrollBarStyle == 'autoHiding') {
document.getElementById('terminal-container').style['-ms-overflow-style'] = '-ms-autohiding-scrollbar';
} else if (scrollBarStyle == 'visible') {
document.getElementById('terminal-container').style['-ms-overflow-style'] = 'scrollbar';
switch (scrollBarStyle) {
case 'hidden': return terminalContainer.style['-ms-overflow-style'] = 'none';
case 'autoHiding': return terminalContainer.style['-ms-overflow-style'] = '-ms-autohiding-scrollbar';
case 'visible': return terminalContainer.style['-ms-overflow-style'] = 'scrollbar';
}
}
function setPadding(padding) {
document.querySelector('.terminal').style.padding = padding + 'px';
term.fit();
}
function changeKeyBindings(keyBindings) {
@@ -181,4 +194,4 @@ window.changeTheme = changeTheme;
window.changeOptions = changeOptions;
window.changeKeyBindings = changeKeyBindings;
window.findNext = findNext;
window.findPrevious = findPrevious;
window.findPrevious = findPrevious;
+9 -2
View File
@@ -5,11 +5,19 @@
html, body {
background: transparent;
height: 100%;
}
width: 100%;
overflow: hidden;
-ms-content-zooming: none;
}
#terminal-container {
width: 100%;
height: 100%;
/* re-enable scrolling only in the xterm container */
overflow-y: auto;
overflow-x: hidden;
touch-action: pan-y;
-ms-touch-action: pan-y;
}
.xterm .xterm-viewport {
@@ -18,5 +26,4 @@ html, body {
.terminal {
background: transparent !important;
padding: 12px;
}
+1
View File
@@ -1,6 +1,7 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
@@ -15,5 +15,7 @@ namespace FluentTerminal.Models
public MouseAction MouseRightClickAction { get; set; }
public bool AlwaysShowTabs { get; set; }
public bool ShowNewOutputIndicator { get; set; }
public bool EnableTrayIcon { get; set; }
public bool AlwaysUseWinPty { get; set; }
}
}
+9
View File
@@ -19,12 +19,18 @@ namespace FluentTerminal.Models.Enums
[Description("Configurable new tab")]
ConfigurableNewTab,
[Description("Change tab title")]
ChangeTabTitle,
[Description("Close tab")]
CloseTab,
[Description("New window")]
NewWindow,
[Description("Configurable new window")]
ConfigurableNewWindow,
[Description("Show settings")]
ShowSettings,
@@ -34,6 +40,9 @@ namespace FluentTerminal.Models.Enums
[Description("Paste")]
Paste,
[Description("Paste without newlines")]
PasteWithoutNewlines,
[Description("Search")]
Search,
@@ -0,0 +1,16 @@
using System.ComponentModel;
namespace FluentTerminal.Models.Enums
{
public enum LineEndingStyle
{
[Description("Do not modify")]
DoNotModify,
[Description("Convert to CRLF")]
ToCRLF,
[Description("Convert to LF")]
ToLF
}
}
@@ -0,0 +1,9 @@
namespace FluentTerminal.Models.Enums
{
public enum SessionType
{
Unknown,
WinPty,
ConPty
}
}
+31 -4
View File
@@ -1,14 +1,41 @@
using FluentTerminal.Models.Enums;
namespace FluentTerminal.Models
namespace FluentTerminal.Models
{
public class KeyBinding
{
public Command Command { get; set; }
public KeyBinding()
{
}
public KeyBinding(KeyBinding other)
{
Command = other.Command;
Key = other.Key;
Ctrl = other.Ctrl;
Alt = other.Alt;
Shift = other.Shift;
Meta = other.Meta;
}
public string Command { get; set; }
public int Key { get; set; }
public bool Ctrl { get; set; }
public bool Alt { get; set; }
public bool Shift { get; set; }
public bool Meta { get; set; }
public override bool Equals(object obj)
{
if (obj is KeyBinding other)
{
return other.Command.Equals(Command)
&& other.Key == Key
&& other.Ctrl == Ctrl
&& other.Alt == Alt
&& other.Shift == Shift
&& other.Meta == Meta;
}
return false;
}
}
}
+2
View File
@@ -9,8 +9,10 @@ namespace FluentTerminal.Models
public ICollection<KeyBinding> PreviousTab { get; set; }
public ICollection<KeyBinding> NewTab { get; set; }
public ICollection<KeyBinding> ConfigurableNewTab { get; set; }
public ICollection<KeyBinding> ChangeTabTitle { get; set; }
public ICollection<KeyBinding> CloseTab { get; set; }
public ICollection<KeyBinding> NewWindow { get; set; }
public ICollection<KeyBinding> ConfigurableNewWindow { get; set; }
public ICollection<KeyBinding> ShowSettings { get; set; }
public ICollection<KeyBinding> Copy { get; set; }
public ICollection<KeyBinding> Paste { get; set; }
@@ -1,8 +1,12 @@
namespace FluentTerminal.Models.Requests
using FluentTerminal.Models.Enums;
namespace FluentTerminal.Models.Requests
{
public class CreateTerminalRequest
{
public int Id { get; set; }
public TerminalSize Size { get; set; }
public ShellProfile Profile { get; set; }
public SessionType SessionType { get; set; }
}
}
@@ -0,0 +1,6 @@
namespace FluentTerminal.Models.Requests
{
public class GetAvailablePortRequest
{
}
}
@@ -2,7 +2,6 @@
{
public class CreateTerminalResponse
{
public int Id { get; set; }
public bool Success { get; set; }
public string Error { get; set; }
public string ShellExecutableName { get; set; }
@@ -0,0 +1,7 @@
namespace FluentTerminal.Models.Responses
{
public class GetAvailablePortResponse
{
public int Port { get; set; }
}
}
+64
View File
@@ -1,9 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FluentTerminal.Models.Enums;
using System.Linq;
namespace FluentTerminal.Models
{
public class ShellProfile
{
/// <summary>
/// Replace all instances of anything resembling a newline, treating pairs of \r\n in either order as a single linebreak.
/// </summary>
public static Regex NewlinePattern = new Regex(@"\n\r|\r\n|\r|\n", RegexOptions.Compiled);
public ShellProfile()
{
}
public ShellProfile(ShellProfile other)
{
Id = other.Id;
PreInstalled = other.PreInstalled;
Name = other.Name;
Arguments = other.Arguments;
Location = other.Location;
WorkingDirectory = other.WorkingDirectory;
TabThemeId = other.TabThemeId;
TerminalThemeId = other.TerminalThemeId;
LineEndingTranslation = other.LineEndingTranslation;
KeyBindings = other.KeyBindings.Select(x => new KeyBinding(x)).ToList();
}
public Guid Id { get; set; }
public bool PreInstalled { get; set; }
public string Name { get; set; }
@@ -11,5 +39,41 @@ namespace FluentTerminal.Models
public string Location { get; set; }
public string WorkingDirectory { get; set; }
public int TabThemeId { get; set; }
public LineEndingStyle LineEndingTranslation { get; set; }
public string TranslateLineEndings(string content)
{
switch (LineEndingTranslation)
{
case LineEndingStyle.ToCRLF:
return NewlinePattern.Replace(content, "\r\n");
case LineEndingStyle.ToLF:
return NewlinePattern.Replace(content, "\n");
case LineEndingStyle.DoNotModify:
default:
return content;
}
}
public Guid TerminalThemeId { get; set; }
public ICollection<KeyBinding> KeyBindings { get; set; } = new List<KeyBinding>();
public override bool Equals(object obj)
{
if (obj is ShellProfile other)
{
return other.Id.Equals(Id)
&& other.PreInstalled.Equals(PreInstalled)
&& other.Name.Equals(Name)
&& other.Arguments.Equals(Arguments)
&& other.Location.Equals(Location)
&& other.WorkingDirectory.Equals(WorkingDirectory)
&& other.TabThemeId.Equals(TabThemeId)
&& other.TerminalThemeId.Equals(TerminalThemeId)
&& other.LineEndingTranslation == LineEndingTranslation
&& other.KeyBindings.SequenceEqual(KeyBindings);
}
return false;
}
}
}
+29
View File
@@ -56,5 +56,34 @@
BrightCyan = other.BrightCyan;
BrightWhite = other.BrightWhite;
}
public override bool Equals(object obj)
{
if (obj is TerminalColors other)
{
return other.Foreground.Equals(Foreground)
&& other.Background.Equals(Background)
&& other.Cursor.Equals(Cursor)
&& other.CursorAccent.Equals(CursorAccent)
&& other.Selection.Equals(Selection)
&& other.Black.Equals(Black)
&& other.Red.Equals(Red)
&& other.Green.Equals(Green)
&& other.Yellow.Equals(Yellow)
&& other.Blue.Equals(Blue)
&& other.Magenta.Equals(Magenta)
&& other.Cyan.Equals(Cyan)
&& other.White.Equals(White)
&& other.BrightBlack.Equals(BrightBlack)
&& other.BrightRed.Equals(BrightRed)
&& other.BrightGreen.Equals(BrightGreen)
&& other.BrightYellow.Equals(BrightYellow)
&& other.BrightBlue.Equals(BrightBlue)
&& other.BrightMagenta.Equals(BrightMagenta)
&& other.BrightCyan.Equals(BrightCyan)
&& other.BrightWhite.Equals(BrightWhite);
}
return false;
}
}
}
+4
View File
@@ -8,6 +8,8 @@ namespace FluentTerminal.Models
public int FontSize { get; set; }
public bool BoldText { get; set; }
public CursorStyle CursorStyle { get; set; }
public bool CursorBlink { get; set; }
@@ -18,6 +20,8 @@ namespace FluentTerminal.Models
public double BackgroundOpacity { get; set; }
public int Padding { get; set; }
public uint ScrollBackLimit { get; set; }
}
}
+24
View File
@@ -4,10 +4,34 @@ namespace FluentTerminal.Models
{
public class TerminalTheme
{
public TerminalTheme()
{
}
public TerminalTheme(TerminalTheme other)
{
Id = other.Id;
Name = other.Name;
Author = other.Author;
PreInstalled = other.PreInstalled;
Colors = new TerminalColors(other.Colors);
}
public Guid Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public bool PreInstalled { get; set; }
public TerminalColors Colors { get; set; }
public override bool Equals(object obj)
{
if (obj is TerminalTheme other)
{
return Equals(other.Name, Name)
&& Equals(other.Author, Author)
&& Equals(other.Colors, Colors);
}
return false;
}
}
}
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff. Show more