Compare commits

..
114 Commits
Author SHA1 Message Date
FelixSe 04a50e8152 Increase version 2019-07-03 21:56:04 +02:00
FelixSe e89853b6b5 Update dependencies 2019-07-03 21:36:33 +02:00
Max Risuhin c6bb9c9e28 Fix restore default value for RTrimCopiedLines option. (#429) 2019-07-03 20:04:16 +02:00
FelixSe d027ab9a02 Increase version 2019-07-02 22:57:05 +02:00
FelixSe 13321cbc1a Update translations 2019-07-02 22:31:40 +02:00
Aleksandar Pesic 9a5af6437f Fixing ssh / WSL issue. (#424) 2019-07-02 10:29:16 +02:00
Menno Finlay-Smits a1967edb1d Mention SSH suport in the README (#426) 2019-07-02 10:27:21 +02:00
Max Risuhin 5900c51e70 Settings option to right trim lines on coping to clipboard. (#422) 2019-06-30 09:43:50 +02:00
Max Risuhin 6209727037 [Xterm.JS] Reset wrapped line flag. (#421) 2019-06-29 21:55:06 +02:00
FelixSe df15cb2231 Update translations 2019-06-29 17:30:56 +02:00
Aleksandar Pesic eb2a44d2de Simplifies entering identity files in "SSH Info" dialog (#419)
* Preparing infrastructure for file checking.

* File checking infrastructure prepared.

* Implementing identity file validation.

* Improving identity file validation.
2019-06-25 12:29:45 +02:00
Max Risuhin e4fff8f638 Use ConPTY by default for SSH connections (#416) 2019-06-25 12:23:06 +02:00
Max Risuhin 80ec667f8b Fixes broken settings export/import. Notifies user on completion of operation. (#414) 2019-06-25 12:18:53 +02:00
Aleksandar Pesic 112c8c7c6c Making tab names for ssh and mosh nicer. (#417)
* Making tab names for ssh and mosh nicer.

* Slightly changing the titles as required.
2019-06-25 12:16:43 +02:00
Aleksandar Pesic 45338d00b8 Implementing auto-fallback to Windows username. (#415)
* Implementing auto-fallback to Windows username.

* mosh.exe recompiled.

* Minor change, as required by Menno.
2019-06-25 12:15:21 +02:00
Menno Finlay-Smits 04ed5d85e5 Remove the concept of a default SSH profile (#418)
Unlike local profiles, it doesn't really make sense to have a default
SSH profile. The UI elements and backing infrastructure for this has
been removed.
2019-06-25 12:14:07 +02:00
Max Risuhin 96ac5cd49f Settings option to turn on Mosh by default for ssh connections (#412) 2019-06-22 11:19:25 +02:00
Max Risuhin c70061995f Use patched xterm 3.14.1 with wordSeparator option (#413) 2019-06-22 11:18:36 +02:00
Aleksandar Pesic 501c5e20d8 Adding a scrollbar to SSH Info dialog. (#409)
* Adding a scrollbar to SSH Info dialog.

* Remote SSH selection dialog improved.
2019-06-18 11:08:55 +02:00
Menno Finlay-Smits 5cc7d813a8 Don't store ssh.exe & mosh.exe paths in profiles (#411)
The full path to ssh.exe and mosh.exe was being stored in SSH profiles.
The path to mosh.exe includes FluentTerminal's version number causing
SSH profiles to break after an upgrade.

Instead, a token indicating SSH or Mosh is stored in the profile
instead. This is resolved just before the shell is started.

This change also meant that the infrastructure for the
"GetMoshSshExecutable" system tray request is no longer required, so
this has been removed.
2019-06-18 11:07:10 +02:00
Max Risuhin a0462ef502 Allow git to handle line endings automatically with text=auto in .gitattributes (#408) 2019-06-16 16:30:10 +02:00
FelixSe 1b1e29cbfe Localize ssh profiles gui 2019-06-16 12:39:06 +02:00
Aleksandar Pesic 94855b7487 Fixing a bug with mosh links. (#407) 2019-06-16 11:17:26 +02:00
Aleksandar Pesic 8ec0d58065 Removing duplcated code from profile view models. (#403)
* Minor improvements to remote SSH profiles. Username prefilled, and line ending defaults to LF.

* Implementing "Set Default" on SSH Profiles.

* Simplifying profile view models.

* Fixing null themes issue.

* Changing line ending of a sinfle file to CRLF.

* Changing file to LF, to avoid false differences.

* Converting a file to LF in order to avoid false differences.

* Fixining a bug with shortcuts.
2019-06-13 19:11:54 +02:00
Max Risuhin 4476fe3061 [mosh] Avoid flickering on scrolling inside tmux panes (#402) 2019-06-12 14:31:50 +02:00
Menno Finlay-Smits 4dbc75f540 Clean up SSH keybindings (#399)
* Added missing resource strings for new SSH bindings

Without these the settings page has no labels for these bindings.

* Rationalise default SSH keybindings

The logic is:
- Adhoc SSH tabs and windows are based around the Y key
- SSH profile tabs and windows are based around the U key (next to Y and
  easier to reach than Z)

* Reorder command enum

Group commands together in a more logical order. In particular, this
makes command keybindings appear in a more logical order on the relevant
Settings page.

Note: this does not break existing keybinding configuration.
2019-06-11 21:51:38 +02:00
Menno Finlay-Smits a82c2a59f5 Honour "Open new terminals as" setting for profile keybindings (#400)
The keyboard shortcuts for local and SSH profiles will now result in the
new terminal being started in a window if "Open new terminals as" is set
to "Windows".

The diff is longer than strictly necessary because Visual Studio fixed
inconsistent line endings.
2019-06-11 21:46:50 +02:00
Menno Finlay-Smits 00b250c1b2 Fix crash when any keybinding is changed (#398)
This is a new regression introduced in 0065827bf (SSH profile support).
It's not clear to me why these JSON serializer settings were introduced
so there might be a better way to fix this.
2019-06-11 21:43:02 +02:00
Aleksandar Pesic 4ec9f0a5a6 Implementing remote SSH profiles, 2nd attempt (#395)
* Implementing remote SSH profiles.

* Fixing async terminal adds.

* Improving behavior when new window is requested.

* Minor changes.

* Move ssh profiles to roaming, rename sshshellprofile to sshprofile
2019-06-07 00:22:45 +02:00
Max Risuhin 5f82724039 Avoid empty string copying to clipboard on mouse selection. (#397) 2019-06-06 08:50:53 +02:00
Menno Finlay-Smits 6c65258439 Debounce terminal resize events (#394)
When FluentTerminal's window is resized many resize events are sent to
the terminal in quick succession. These rapid terminal size changes
while other output is being sent to the terminal can result in
display corruption.

In this change:

- Resize events are "debounced", greatly reducing the number of
  resize events being sent to the underlying terminal.
- Output to the terminal is blocked while resize events are processed.
  The blocked output is temporarily buffered until the resize event
  should have been processed by the underlying terminal.

The net result is that window resizes are *much* less likely to result
in corrupted buffer output. Corruption can still happen however,
especially in cases of very high rates of output being sent to the
terminal.

The mostly solves #373.
2019-06-04 20:11:18 +02:00
Felix f23493e3c8 Improve IPC (#391)
* Minimize overhead for buffer IPC

* use byte for terminalId
2019-06-02 21:44:56 +02:00
FelixSe de07e277f1 write utf8 to xterm.js 2019-06-02 13:23:45 +02:00
FelixSe 5c52810149 Migrate index.js to typescript 2019-06-01 14:12:15 +02:00
FelixSe 05a5ad56ea Update xterm.js 2019-06-01 12:28:07 +02:00
Max Risuhin c51ffaa6e9 Resolve not recognized arrows keys codes under vim (#384) 2019-05-29 20:06:40 +02:00
FelixSe 288b661611 Consolidate RadioButton bindings 2019-05-26 21:34:35 +02:00
FelixSe 32ab18f5f9 Add spanish localization 2019-05-26 20:42:03 +02:00
FelixSe fddbd12820 Translate MessageDialog buttons 2019-05-25 23:04:13 +02:00
Felix 563a7071ea Add missing text for 'delete profile' (#380) 2019-05-25 23:03:11 +02:00
FelixSe d2c89ccb08 Fix tab theme selection crash 2019-05-25 22:48:07 +02:00
FelixSe b5e8ce7639 Add missing button translation 2019-05-25 22:16:51 +02:00
Felix 6829c4c7f3 Fix layout of TabBar, add missing translations (#379) 2019-05-25 22:10:14 +02:00
Max Risuhin 09bd970673 Ignore multiple launch requests at the same time (#378)
* Ignore multiple launch requests at the same time

* Add missing brackets
2019-05-25 22:09:51 +02:00
Aleksandar Pesic af61c83949 Fixed a bug with clicking links without having the app opened. (#376)
* Fixed a bug with clicking links without having the app opened.

* Added back braces and a few other cleanups
2019-05-21 22:10:23 +02:00
Felix Seidl 5ed24f85cf Fix resize event handler argument deconstruction 2019-05-20 12:03:30 +02:00
FelixSe 763a071488 New logo 2019-05-17 23:10:18 +02:00
Max Risuhin bdb4d57d62 Standardize default mosh ports to the range of 60001:60999 (#371) 2019-05-16 21:28:31 +02:00
Felix 1be5fecf41 Update translation files (#367) 2019-05-14 12:30:24 +02:00
Max Risuhin 14dfb8b26a Force log file creation with Initialization log message of the highes severity. (#365) 2019-05-14 12:00:04 +02:00
FelixSe 7284ce7f52 Update xterm.js 2019-05-12 14:29:11 +02:00
FelixSe ac9f3c2a0d Merge branch 'master' of https://github.com/felixse/FluentTerminal 2019-05-12 14:15:30 +02:00
FelixSe 9dfab2223f Move conpty option to ShellProfile 2019-05-12 14:15:21 +02:00
FelixSe c15da646fc Add german translation 2019-05-11 17:40:21 +02:00
Andrew Montgomery 2a47555987 feat(cli): Add json import/export of app config (#296)
* feat(cli): Add json import/export of app config

Add the capability to export and import the application config to/from
a json file. The JSON file is LocalState\config.json in the app package
folder. The options are available via the new flags to the settings
command, e.g. `fluent.exe settings --export` and `fluent.exe settings
--import`

Import attempts to merge imported config with existing config, and
refuses to import thigns marked as pre-installed. It attempts to
merge configuration for pre-installed profiles where those fields
are editable, but otherwise discards invalid configuration.

Change-Id: Iadb468de5f724396db9a2de6bf3c3ba0d4d7ac2f

* Moved logic to SettingsService, fixed some issues
2019-05-11 17:29:37 +02:00
Felix ccf75bd45f Make language selectable, extend to new ssh feature (#362) 2019-05-11 16:50:20 +02:00
Aleksandar Pesic d9b0b86cd5 SSH and Mosh implemented (#360)
* Implementing SSH (changes from ap/ssh branch).

* Implemented opening links in the last active window.

* Using settings to determine where to open link (tab or window).

* Adding FluentTerminal.App_TemporaryKey.pfx back to repo, to solve bug with keys.

* App activation on mosh schema launching; mosh protocol parsing. (#35)

* SSH handling changed to improve links.

* Creating shortcuts from SSH dialog.

* Trivial Mosh checkbox label fix

It's "Mosh" not "MOSH".

* Obtain mosh key from ssh connection (#40)

* Prepopulate SSH username with Windows username (#50)

* Undoing Max's mosh authentication changes.

* Adding the new mosh wrapper.

* Request user name from tray process (#51)

* Mosh implemented, but still doesn't work (cannot find mosh.exe).

*  Add New Remote Shell menu; Rename New Tab into New Local Shell (#52)

* Mosh works like charm.

* Mosh wrapper improved to print an error.

* Changing mosh wrapper to always exit with 0 (to ensure that the tab gets closed).

* Changing mosh wrapper again to return error codes.

* Slightly optimizing getting the username.

* Saving links improved.

* Fixed an issue in mosh wrapper, related to mosh to hostname.

* mosh-client.exe updated with 32 bit version for x86 platform (#59)

* Set line endings for remote shell connection;
Left focus on SSH info dialog if input validation error occured;
Update DataContext object on each key entered into target host text box control.

* Simplifying SSH dialog.

* Fixing some leftovers after merging.

* Fixing some after-merge issues.

* Avoiding some unintentional upstream changes.

* Fix review findings
2019-05-10 22:54:09 +02:00
Alex Moore d157b68384 Updated default line ending for WSL to address issue #310 (#348) 2019-05-07 16:11:19 +02:00
Menno Finlay-Smits 0733381cd0 Leave tabs open on error (#357)
* Do not close tab with shell process error exit code

* Show tab indicator when shell has exited with error
2019-05-07 16:10:39 +02:00
王文斌 1870209878 New feature: JumpList (#349)
* FluentTerminal.SystemTray
TargetFrameworkVersion: v4.7.1 -> v4.7.2

* New feature: JumpList

* Revert "FluentTerminal.SystemTray"

This reverts commit 5c3122ac394bea0a9e484e314983ad267683d714.

* New feature: JumpList (optimization)
2019-05-02 22:20:32 +02:00
王文斌 565b3c3f69 Multi-language support: Add zh-CN (#347)
* FluentTerminal.SystemTray
TargetFrameworkVersion: v4.7.1 -> v4.7.2

* Revert "FluentTerminal.SystemTray"

This reverts commit 5c3122ac394bea0a9e484e314983ad267683d714.

* Multi-language support: Add zh-CN

* Updated translations, added script
2019-05-02 12:14:15 +02:00
Menno Finlay-Smits 7cdda79d67 Bubble up shell process exit codes (#343)
Make the shell process's exit code available to the main app so that
actions can be taken depending on whether the shell command was
successful or not.

Later work will build on this so that failed commands will result in
tabs being left open when the shell command fails. This will help when
troubleshooting incorrectly configured profiles, SSH connection issues
etc.

Exit codes are passed up for both WinPty and ConPty based terminals.
2019-05-01 20:36:56 +02:00
Meir Blachman a0f57ab4a3 Update KeyboardCommandServiceTests.cs (#330) 2019-04-28 11:14:12 +02:00
TomerZeitune edee7628d0 F11 to toggle fullscreen (#311)
* Add F11 support

* Added F11 to toggle fullscreen

Signed-off-by: TomerZeitune <tomerzeitune@gmail.com>

* removed F11

* Remove 'false' for unused modifiers
2019-04-15 20:39:03 +02:00
nikitar020 d06ad45423 Fix finding SSH when run as 32-bit process on a 64-bit OS (#316)
* Fix finding SSH when executing as 32-bit process on a 64-bit operational system

* Display a suitable error message when no OpenSSH client installed
and the user tries to open an SSH connection
2019-04-15 20:04:50 +02:00
Menno Finlay-Smits 9cf6a61e0f Less conflicting default keybindings (#309)
Many of FluentTerminal's default keybindings collide with commonly used
default keys in various terminal apps. Use defaults that are less likely
to cause problems to provide a better out-of-the-box experience. Many of
the bindings are inspired by gnome-terminal's defaults.

Also, the `false` modifiers in the default keybinding specifications
have been removed. This makes it easier to see which modifiers are
actually in use.
2019-04-09 22:03:07 +02:00
Andrew Kralovec 0ea4722dc8 Dev/overlay control (#278)
* Creating a control for the overlay

* using Interactivity

* trying to bind by reference

* fix bindings

* remove unused packages

* Move getter/setter to show method

* Convert to AutoProperty

* Make text copied notification configurable

* cache settings, removed unused file
2019-04-09 22:00:51 +02:00
Max Risuhin 39960dd755 Read whole terminal frame at once to avoid console data append glitches (#299)
* Read whole terminal frame at once to avoid console data append glitches

* Simplify calculation logic with Math.Max
2019-04-07 15:24:21 +02:00
Kim Chan 8e5d3dea0a fix: new line bug in verbatim string (#294) 2019-04-05 11:48:15 +02:00
Aleksandar Pesic 973b1a4963 Ap/ssh Implemented SSH by using the exsting infrastructure (#293)
* Appveyor builds (#6)

* SSH implemented.

* Implementing SSH by using the existing app infrastructure.

* Refactored to new command instead of a profile

* Update .gitignore
2019-04-05 10:53:17 +02:00
Mike MacCana dbd8632076 Add dark and light mode SVG icons for #203 (#290) 2019-04-03 19:41:45 +02:00
MinchinWeb fffa7ebcb9 Un-swap Ubuntu yellow and cyan (#291)
The build-in Ubuntu profile has the yellows and cyans swapped. This fixes that.
2019-04-03 19:40:20 +02:00
Andrew Kralovec ded043db1d move the $null to the left side of the operator (#259) 2019-03-30 11:00:10 +01:00
Paul Galbraith 61ad64519b Revert "Fix missing Developer mode requirement for Chocolatey install (#235)" (#265)
This reverts commit 1a0c51c6d4.
2019-03-30 10:59:52 +01:00
FelixSe ef7f909d26 Increase version 2019-03-27 21:57:16 +01:00
FelixSe f2618fa940 Fix launch error when TERM was already defined 2019-03-27 21:13:23 +01:00
ivanjh 363f885aea Fix shell profile radio button line-ending binding (#257)
* Fix shell profile radio button line-ending binding
Only change when updated from a checked radio button

* Fix cancelling profile edit showing default line translation

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

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

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

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

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

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

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

* parse arguments with commandlineparser nuget

* Run commands with no underlying shell process

* Create RunVerb, cleanup terminal creation

* Fix window close bug

* Use working directory of caller if none specified

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

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

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

* Apply color to single tab

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

* fix TerminalTheme Equals()

* Fix new output indicator

* Ut8 and some fixes

* fix test

* fixed infinite loop on shell exit
2019-03-06 20:06:19 +01:00
312 changed files with 13336 additions and 5703 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Git will handle the files in whatever way it thinks is best.
# Overwrites global core.autocrlf git setting
* text=auto
+2
View File
@@ -292,3 +292,5 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
!FluentTerminal.App/MoshExecutables/*
@@ -1,3 +1,3 @@
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%1" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%V" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new %%V" /f
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%1\"" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Services.Test
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
{
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
dialogService.ShowCreateKeyBindingDialog();
@@ -36,7 +36,7 @@ namespace FluentTerminal.App.Services.Test
var title = string.Empty;
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
@@ -49,7 +49,7 @@ namespace FluentTerminal.App.Services.Test
var title = _fixture.Create<string>();
var content = string.Empty;
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
@@ -61,7 +61,7 @@ namespace FluentTerminal.App.Services.Test
{
var title = _fixture.Create<string>();
var content = _fixture.Create<string>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
@@ -75,7 +75,7 @@ namespace FluentTerminal.App.Services.Test
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var messageDialog = new Mock<IMessageDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
@@ -86,11 +86,24 @@ namespace FluentTerminal.App.Services.Test
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
{
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>);
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
dialogService.ShowProfileSelectionDialogAsync();
shellProfileSelectionDialog.Verify(x => x.SelectProfile(), Times.Once);
}
[Fact]
public void ShowInputDialogAsync_UsesIInputDialogSetTitle()
{
var title = "title";
var inputDialog = new Mock<IInputDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
dialogService.ShowInputDialogAsync(title);
inputDialog.Verify(x => x.SetTitle(title), Times.Once);
inputDialog.Verify(x => x.GetInput(), Times.Once);
}
}
}
}
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
@@ -11,13 +11,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.4.0" />
<PackageReference Include="FluentAssertions" Version="5.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Moq" Version="4.8.3" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<PackageReference Include="AutoFixture" Version="4.10.0" />
<PackageReference Include="FluentAssertions" Version="5.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Moq" Version="4.12.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
@@ -31,4 +34,11 @@
<ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.FoundationContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
@@ -41,6 +41,41 @@ namespace FluentTerminal.App.Services.Test
invoke.Should().Throw<InvalidOperationException>();
}
[Fact]
public void DeregisterCommandHandler_CommandIsNull_ThrowsArgumentNullException()
{
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(null);
invoke.Should().Throw<ArgumentNullException>();
}
[Fact]
public void DeregisterCommandHandler_CommandRegistered_HandlerGetsRemoved()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var handler = _fixture.Create<Action>();
var keyboardCommandService = new KeyboardCommandService();
keyboardCommandService.RegisterCommandHandler(command, handler);
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact]
public void DeregisterCommandHandler_CommandNotRegistered_ShouldNotThrow()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact]
public void SendCommand_CommandIsRegistered_HandlerGetsInvoked()
@@ -471,10 +471,6 @@ namespace FluentTerminal.App.Services.Test
var commands = _fixture.CreateMany<Command>(5);
var defaultValueProvider = new Mock<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
foreach (var command in commands)
{
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(EnumHelper.GetEnumDescription(command), null)).Returns(new Collection<KeyBinding> { });
}
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
@@ -10,6 +10,7 @@ using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
using Xunit;
namespace FluentTerminal.App.Services.Test
@@ -34,19 +35,20 @@ namespace FluentTerminal.App.Services.Test
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var response = _fixture.Create<CreateTerminalResponse>();
var responseMessage = new Dictionary<string, string>
var responseMessage = new ValueSet
{
[MessageKeys.Type] = _fixture.Create<string>(),
[MessageKeys.Content] = JsonConvert.SerializeObject(response)
};
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<IDictionary<string, string>>())).Returns(Task.FromResult((IDictionary<string, string>)responseMessage));
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<ValueSet>())).Returns(Task.FromResult(responseMessage));
var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>();
var id = _fixture.Create<byte>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
var result = await trayProcessCommunicationService.CreateTerminal(terminalSize, shellProfile, sessionType);
var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
result.Should().BeEquivalentTo(response);
}
@@ -54,7 +56,7 @@ namespace FluentTerminal.App.Services.Test
[Fact]
public async Task ResizeTerminal_Default_SendsResizeTerminalRequest()
{
var terminalId = _fixture.Create<int>();
var terminalId = _fixture.Create<byte>();
var terminalSize = _fixture.Create<TerminalSize>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -68,14 +70,14 @@ namespace FluentTerminal.App.Services.Test
await trayProcessCommunicationService.ResizeTerminal(terminalId, terminalSize);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(ResizeTerminalRequest))), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == ResizeTerminalRequest.Identifier)), Times.Once);
}
[Fact]
public async Task WriteText_Default_SendsWriteTextRequest()
public async Task Write_Default_SendsWriteDataRequest()
{
var terminalId = _fixture.Create<int>();
var text = _fixture.Create<string>();
var terminalId = _fixture.Create<byte>();
var data = _fixture.Create<byte[]>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
@@ -86,15 +88,15 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.WriteText(terminalId, text);
await trayProcessCommunicationService.Write(terminalId, data);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(WriteTextRequest))), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
}
[Fact]
public async Task CloseTerminal_Default_SendsTerminalExitedRequest()
{
var terminalId = _fixture.Create<int>();
var terminalId = _fixture.Create<byte>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
@@ -107,22 +109,19 @@ namespace FluentTerminal.App.Services.Test
await trayProcessCommunicationService.CloseTerminal(terminalId);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<IDictionary<string, string>>(d => d[MessageKeys.Type] == nameof(TerminalExitedRequest))), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == TerminalExitedRequest.Identifier)), Times.Once);
}
[Fact]
public void OnMessageReceived_TerminalExitedRequest_InvokesTerminalExitedEvent()
{
var terminalId = _fixture.Create<int>();
var receivedTerminalId = 0;
var terminalExitedEventCalled = false;
var request = new TerminalExitedRequest
var request = new TerminalExitedRequest(_fixture.Create<byte>(), _fixture.Create<int>());
var receivedExitStatus = (TerminalExitStatus) null;
var message = new ValueSet
{
TerminalId = terminalId
};
var message = new Dictionary<string, string>
{
[MessageKeys.Type] = nameof(TerminalExitedRequest),
[MessageKeys.Type] = TerminalExitedRequest.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
};
var settingsService = new Mock<ISettingsService>();
@@ -134,33 +133,29 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
var appServiceConnection = new Mock<IAppServiceConnection>();
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
trayProcessCommunicationService.TerminalExited += (s, e) =>
trayProcessCommunicationService.TerminalExited += (s, status) =>
{
terminalExitedEventCalled = true;
receivedTerminalId = e;
receivedExitStatus = status;
};
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
terminalExitedEventCalled.Should().BeTrue();
receivedTerminalId.Should().Be(terminalId);
receivedExitStatus.Should().NotBeNull();
receivedExitStatus.TerminalId.Should().Be(request.TerminalId);
receivedExitStatus.ExitCode.Should().Be(request.ExitCode);
}
[Fact]
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
{
var terminalId = _fixture.Create<int>();
var output = _fixture.Create<string>();
var receivedOutput = string.Empty;
var request = new DisplayTerminalOutputRequest
var terminalId = _fixture.Create<byte>();
var output = _fixture.Create<byte[]>();
var receivedOutput = default(byte[]);
var message = new ValueSet
{
TerminalId = terminalId,
Output = output
};
var message = new Dictionary<string, string>
{
[MessageKeys.Type] = nameof(DisplayTerminalOutputRequest),
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
[MessageKeys.TerminalId] = terminalId,
[MessageKeys.Content] = output
};
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
@@ -178,7 +173,7 @@ namespace FluentTerminal.App.Services.Test
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
receivedOutput.Should().Be(output);
receivedOutput.Should().BeEquivalentTo(output);
}
}
}
@@ -7,5 +7,6 @@
public IApplicationDataContainer Themes { get; set; }
public IApplicationDataContainer KeyBindings { get; set; }
public IApplicationDataContainer ShellProfiles { get; set; }
public IApplicationDataContainer SshProfiles { get; set; }
}
}
+2
View File
@@ -5,5 +5,7 @@
public const string ThemesContainerName = "Themes";
public const string KeyBindingsContainerName = "KeyBindings";
public const string ShellProfilesContainerName = "ShellProfiles";
public const string SshProfilesContainerName = "SshProfiles";
public const byte TerminalBufferRequestIdentifier = 0;
}
}
@@ -0,0 +1,10 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshConnectionInfoDialog
{
Task<ISshConnectionInfo> GetSshConnectionInfoAsync(ISshConnectionInfo input);
}
}
@@ -0,0 +1,12 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshProfileSelectionDialog
{
SshProfile SelectedProfile { get; }
Task<SshProfile> SelectProfile();
}
}
@@ -1,7 +1,10 @@
namespace FluentTerminal.App.Services.EventArgs
using System;
namespace FluentTerminal.App.Services.EventArgs
{
public class NewWindowRequestedEventArgs : System.EventArgs
{
public bool ShowProfileSelection { get; set; }
public Models.Enums.NewWindowAction Action { get; set; }
public Guid ProfileId { get; set; }
}
}
@@ -7,9 +7,11 @@
<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="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="PListNet" Version="3.0.0" />
<PackageReference Include="RestSharp" Version="106.6.9" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
</ItemGroup>
@@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
namespace FluentTerminal.App.Services
{
public interface IAppServiceConnection
{
event EventHandler<IDictionary<string, string>> MessageReceived;
event EventHandler<ValueSet> MessageReceived;
Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message);
Task<ValueSet> SendMessageAsync(ValueSet message);
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface IApplicationLanguageService
{
IEnumerable<string> Languages { get; }
void SetLanguage(string language);
string GetCurrentLanguage();
}
}
@@ -9,7 +9,9 @@ namespace FluentTerminal.App.Services
public interface IApplicationView
{
event CloseRequestedHandler CloseRequested;
event EventHandler Closed;
int Id { get; }
string Title { get; set; }
Task RunOnDispatcherThread(Action action);
@@ -17,5 +17,9 @@ namespace FluentTerminal.App.Services
Task<KeyBinding> ShowCreateKeyBindingDialog();
Task<string> ShowInputDialogAsync(string title);
Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync(ISshConnectionInfo input);
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
}
}
@@ -11,19 +11,25 @@ namespace FluentTerminal.App.Services
event EventHandler KeyBindingsChanged;
event EventHandler<ShellProfile> ShellProfileAdded;
event EventHandler<Guid> ShellProfileDeleted;
event EventHandler<SshProfile> SshProfileAdded;
event EventHandler<Guid> SshProfileDeleted;
event EventHandler<TerminalOptions> TerminalOptionsChanged;
event EventHandler<Guid> ThemeDeleted;
event EventHandler<TerminalTheme> ThemeAdded;
void DeleteShellProfile(Guid id);
void DeleteSshProfile(Guid id);
void DeleteTheme(Guid id);
ApplicationSettings GetApplicationSettings();
TerminalTheme GetCurrentTheme();
Guid GetCurrentThemeId();
ShellProfile GetDefaultShellProfile();
SshProfile GetDefaultSshProfile();
Guid GetDefaultShellProfileId();
Guid GetDefaultSshProfileId();
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
IEnumerable<ShellProfile> GetShellProfiles();
IEnumerable<SshProfile> GetSshProfiles();
IEnumerable<TabTheme> GetTabThemes();
TerminalOptions GetTerminalOptions();
TerminalTheme GetTheme(Guid id);
@@ -32,10 +38,15 @@ namespace FluentTerminal.App.Services
void SaveApplicationSettings(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id);
void SaveDefaultSshProfileId(Guid id);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile shellProfile, bool newShell = false);
void SaveTerminalOptions(TerminalOptions terminalOptions);
void SaveTheme(TerminalTheme theme, bool newTheme = false);
ShellProfile GetShellProfile(Guid id);
SshProfile GetSshProfile(Guid id);
string ExportSettings();
void ImportSettings(string serializedSettings);
}
}
@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface ISshHelperService
{
bool IsSsh(Uri uri);
ISshConnectionInfo ParseSsh(Uri uri);
Task<SshProfile> GetSshProfileAsync(SshProfile profile);
Task<SshProfile> GetSavedSshProfileAsync();
Task<string> ConvertToUriAsync(ISshConnectionInfo sshConnectionInfo);
}
}
@@ -1,9 +1,10 @@
using System.Collections.Generic;
using FluentTerminal.Models;
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface ISystemFontService
{
IEnumerable<string> GetSystemFontFamilies();
IEnumerable<FontInfo> GetSystemFontFamilies();
}
}
@@ -8,22 +8,32 @@ namespace FluentTerminal.App.Services
{
public interface ITrayProcessCommunicationService
{
event EventHandler<int> TerminalExited;
event EventHandler<TerminalExitStatus> TerminalExited;
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task ResizeTerminal(int id, TerminalSize size);
Task ResizeTerminal(byte id, TerminalSize size);
Task UpdateToggleWindowKeyBindings();
Task WriteText(int terminalId, string text);
Task Write(byte terminalId, byte[] data);
void SubscribeForTerminalOutput(int terminalId, Action<string> callback);
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
Task CloseTerminal(int terminalId);
Task CloseTerminal(byte terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
int GetNextTerminalId();
byte GetNextTerminalId();
Task<string> GetUserName();
Task SaveTextFileAsync(string path, string content);
Task<string> GetSshConfigDirAsync();
Task<string[]> GetFilesFromSshConfigDirAsync();
Task<bool> CheckFileExistsAsync(string path);
}
}
@@ -1,7 +1,9 @@
using FluentTerminal.Models;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using Windows.Foundation.Metadata;
namespace FluentTerminal.App.Services.Implementation
{
@@ -21,9 +23,12 @@ namespace FluentTerminal.App.Services.Implementation
MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu,
AlwaysShowTabs = false,
AlwaysUseWinPty = true,
ShowNewOutputIndicator = false,
EnableTrayIcon = true
EnableTrayIcon = true,
ShowCustomTitleInTitlebar = true,
UseMoshByDefault = true,
AutoFallbackToWindowsUsernameInLinks = true,
RTrimCopiedLines = true
};
}
@@ -37,9 +42,6 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding
{
Command = nameof(Command.ToggleWindow),
Ctrl = false,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Scroll
}
};
@@ -51,8 +53,6 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NextTab),
Ctrl = true,
Alt = false,
Shift = false,
Key = (int)ExtendedVirtualKey.Tab
}
};
@@ -64,7 +64,6 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.PreviousTab),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.Tab
}
@@ -84,9 +83,7 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding
{
Command = command.ToString(),
Ctrl = true,
Alt = false,
Shift = false,
Alt = true,
Key = (int)ExtendedVirtualKey.Number1 + (command - Command.SwitchToTerm1)
}
};
@@ -98,8 +95,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NewTab),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.T
}
};
@@ -111,12 +107,60 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ConfigurableNewTab),
Ctrl = true,
Alt = false,
Shift = true,
Alt = true,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewSshTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewSshTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Y
}
};
case Command.NewSshWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewSshWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.Y
}
};
case Command.SavedSshNewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.SavedSshNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
@@ -124,7 +168,6 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ChangeTabTitle),
Ctrl = true,
Alt = false,
Shift = true,
Key = (int)ExtendedVirtualKey.R
}
@@ -137,8 +180,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.CloseTab),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.W
}
};
@@ -150,8 +192,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NewWindow),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.N
}
};
@@ -163,8 +204,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ConfigurableNewWindow),
Ctrl = true,
Alt = false,
Shift = true,
Alt = true,
Key = (int)ExtendedVirtualKey.N
}
};
@@ -176,8 +216,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.ShowSettings),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.Comma
}
};
@@ -189,8 +228,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Copy),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.C
}
};
@@ -202,8 +240,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Paste),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.V
}
};
@@ -216,8 +253,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.PasteWithoutNewlines),
Ctrl = true,
Alt = false,
Shift = true,
Alt = true,
Key = (int)ExtendedVirtualKey.V
}
};
@@ -229,8 +265,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Search),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.F
}
};
@@ -241,10 +276,13 @@ namespace FluentTerminal.App.Services.Implementation
new KeyBinding
{
Command = nameof(Command.ToggleFullScreen),
Ctrl = false,
Alt = true,
Shift = false,
Key = (int)ExtendedVirtualKey.Enter
},
new KeyBinding
{
Command = nameof(Command.ToggleFullScreen),
Key = (int)ExtendedVirtualKey.F11
}
};
@@ -255,8 +293,7 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.SelectAll),
Ctrl = true,
Alt = false,
Shift = false,
Shift = true,
Key = (int)ExtendedVirtualKey.A
}
};
@@ -268,11 +305,11 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.Clear),
Ctrl = true,
Alt = false,
Shift = false,
Alt = true,
Key = (int)ExtendedVirtualKey.L
}
};
}
return null;
@@ -290,7 +327,7 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme
{
Id = 0,
Name = "None",
Name = I18N.Translate("TabTheme.None"),
BackgroundOpacity = double.NaN,
BackgroundPointerOverOpacity = double.NaN,
BackgroundPressedOpacity = double.NaN,
@@ -301,41 +338,42 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme
{
Id = 1,
Name = "Red",
Name = I18N.Translate("TabTheme.Red"),
Color = "#E81123"
},
new TabTheme
{
Id = 2,
Name = "Green",
Name = I18N.Translate("TabTheme.Green"),
Color = "#10893E"
},
new TabTheme
{
Id = 3,
Name = "Blue",
Name = I18N.Translate("TabTheme.Blue"),
Color = "#0078D7"
},
new TabTheme
{
Id = 4,
Name = "Purple",
Name = I18N.Translate("TabTheme.Purple"),
Color = "#881798"
},
new TabTheme
{
Id = 5,
Name = "Orange",
Name = I18N.Translate("TabTheme.Orange"),
Color = "#FF8C00"
},
new TabTheme
{
Id = 6,
Name = "Teal",
Name = I18N.Translate("TabTheme.Teal"),
Color = "#00B7C3"
}
};
}
public TerminalOptions GetDefaultTerminalOptions()
{
return new TerminalOptions
@@ -371,6 +409,7 @@ namespace FluentTerminal.App.Services.Implementation
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+
KeyBindings = new []
{
new KeyBinding
@@ -393,6 +432,7 @@ namespace FluentTerminal.App.Services.Implementation
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
KeyBindings = new []
{
new KeyBinding
@@ -414,7 +454,8 @@ namespace FluentTerminal.App.Services.Implementation
Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
LineEndingTranslation = LineEndingStyle.ToLF,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
KeyBindings = new []
{
new KeyBinding
@@ -577,18 +618,18 @@ namespace FluentTerminal.App.Services.Implementation
Black = "#300A24",
Red = "#CC0000",
Green = "#4E9A06",
Yellow = "#06989A",
Yellow = "#C4A000",
Blue = "#3465A4",
Magenta = "#75507B",
Cyan = "#C4A000",
Cyan = "#06989A",
White = "#D3D7CF",
BrightBlack = "#554E53",
BrightRed = "#EF2929",
BrightGreen = "#8AE234",
BrightYellow = "#34E2E2",
BrightYellow = "#FCE94F",
BrightBlue = "#729FCF",
BrightMagenta = "#AD7FA8",
BrightCyan = "#FCE94F",
BrightCyan = "#34E2E2",
BrightWhite = "#EEEEEE"
}
};
@@ -11,13 +11,20 @@ namespace FluentTerminal.App.Services.Implementation
private readonly Func<IMessageDialog> _messageDialogFactory;
private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory;
private readonly Func<IInputDialog> _inputDialogFactory;
private readonly Func<ISshConnectionInfoDialog> _sshConnectionInfoDialogFactory;
private readonly Func<ISshProfileSelectionDialog> _sshProfileSelectionDialogFactory;
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory, Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory, Func<IInputDialog> inputDialogFactory)
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
Func<ISshProfileSelectionDialog> sshProfileSelectionDialogFactory)
{
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
_messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory;
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
_sshProfileSelectionDialogFactory = sshProfileSelectionDialogFactory;
}
public Task<KeyBinding> ShowCreateKeyBindingDialog()
@@ -70,5 +77,15 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.SelectProfile();
}
public Task<ISshConnectionInfo> ShowSshConnectionInfoDialogAsync(ISshConnectionInfo input) =>
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
public Task<SshProfile> ShowSshProfileSelectionDialogAsync()
{
var dialog = _sshProfileSelectionDialogFactory();
return dialog.SelectProfile();
}
}
}
@@ -12,12 +12,14 @@ namespace FluentTerminal.App.Services.Implementation
{
public const string CurrentThemeKey = "CurrentTheme";
public const string DefaultShellProfileKey = "DefaultShellProfile";
public const string DefaultSshProfileKey = "DefaultSshProfile";
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IApplicationDataContainer _keyBindings;
private readonly IApplicationDataContainer _localSettings;
private readonly IApplicationDataContainer _roamingSettings;
private readonly IApplicationDataContainer _shellProfiles;
private readonly IApplicationDataContainer _sshProfiles;
private readonly IApplicationDataContainer _themes;
public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers)
@@ -29,6 +31,8 @@ namespace FluentTerminal.App.Services.Implementation
_themes = containers.Themes;
_keyBindings = containers.KeyBindings;
_shellProfiles = containers.ShellProfiles;
_sshProfiles = containers.SshProfiles;
foreach (var theme in _defaultValueProvider.GetPreInstalledThemes())
{
@@ -47,6 +51,101 @@ namespace FluentTerminal.App.Services.Implementation
}
}
public string ExportSettings()
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = GetCommandKeyBindings(),
TerminalOptions = GetTerminalOptions(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>()
};
foreach (var theme in GetThemes().Where(x => !x.PreInstalled))
{
config.Themes.Add(theme);
}
foreach (var profile in GetShellProfiles().Where(x => !x.PreInstalled))
{
config.Profiles.Add(profile);
}
foreach (var profile in GetSshProfiles())
{
config.SshProfiles.Add(profile);
}
return JsonConvert.SerializeObject(config);
}
public void ImportSettings(string serializedSettings)
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = new Dictionary<string, ICollection<KeyBinding>>(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>(),
TerminalOptions = GetTerminalOptions()
};
JsonConvert.PopulateObject(serializedSettings, config);
SaveApplicationSettings(config.App);
// Since we set each command sepaartely, we don't need all existing settings
foreach (var pair in config.KeyBindings)
{
SaveKeyBindings(pair.Key, pair.Value);
}
// Can't create/modify pre-installed themes
foreach (var theme in config.Themes.Where(x => !x.PreInstalled))
{
var existingTheme = GetTheme(theme.Id);
if (existingTheme?.PreInstalled == true)
{
continue;
}
SaveTheme(theme, existingTheme != null);
}
// Can't create pre-installed profiles
foreach (var profile in config.Profiles.Where(x => !x.PreInstalled))
{
var existingProfile = GetShellProfile(profile.Id);
var isNew = existingProfile.EqualTo(default);
// You can only edit certain parts of preinstalled profiles
if (!isNew && existingProfile.PreInstalled)
{
existingProfile.WorkingDirectory = profile.WorkingDirectory;
existingProfile.Arguments = profile.Arguments;
existingProfile.TabThemeId = profile.TabThemeId;
existingProfile.TerminalThemeId = profile.TerminalThemeId;
existingProfile.LineEndingTranslation = profile.LineEndingTranslation;
existingProfile.KeyBindings = profile.KeyBindings;
SaveShellProfile(profile, isNew);
continue;
}
SaveShellProfile(profile, isNew);
}
foreach (var profile in config.SshProfiles)
{
var existingProfile = GetSshProfile(profile.Id);
var isNew = existingProfile.EqualTo(default(SshProfile));
SaveSshProfile(profile, isNew);
}
SaveTerminalOptions(config.TerminalOptions);
}
public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
public event EventHandler<TerminalTheme> ThemeAdded;
@@ -58,6 +157,9 @@ namespace FluentTerminal.App.Services.Implementation
public event EventHandler<ShellProfile> ShellProfileAdded;
public event EventHandler<Guid> ShellProfileDeleted;
public event EventHandler<SshProfile> SshProfileAdded;
public event EventHandler<Guid> SshProfileDeleted;
public event EventHandler<TerminalOptions> TerminalOptionsChanged;
public void DeleteShellProfile(Guid id)
@@ -66,6 +168,14 @@ namespace FluentTerminal.App.Services.Implementation
ShellProfileDeleted?.Invoke(this, id);
}
public void DeleteSshProfile(Guid id)
{
_sshProfiles.Delete(id.ToString());
SshProfileDeleted?.Invoke(this, id);
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
}
public void DeleteTheme(Guid id)
{
_themes.Delete(id.ToString());
@@ -121,11 +231,22 @@ namespace FluentTerminal.App.Services.Implementation
}
return profile;
}
public SshProfile GetDefaultSshProfile()
{
var id = GetDefaultSshProfileId();
var profile = _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
return profile;
}
public ShellProfile GetShellProfile(Guid id)
{
return _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
}
public SshProfile GetSshProfile(Guid id)
{
return _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
}
public Guid GetDefaultShellProfileId()
{
@@ -135,6 +256,14 @@ namespace FluentTerminal.App.Services.Implementation
}
return _defaultValueProvider.GetDefaultShellProfileId();
}
public Guid GetDefaultSshProfileId()
{
if (_localSettings.TryGetValue(DefaultSshProfileKey, out object value))
{
return (Guid)value;
}
return System.Guid.Empty;
}
public IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings()
{
@@ -151,6 +280,14 @@ namespace FluentTerminal.App.Services.Implementation
{
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string)x)).ToList();
}
public IEnumerable<SshProfile> GetSshProfiles()
{
if (_sshProfiles == null)
return new List<SshProfile>();
else
return _sshProfiles.GetAll()
.Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x)).ToList();
}
public IEnumerable<TabTheme> GetTabThemes()
{
@@ -199,15 +336,19 @@ namespace FluentTerminal.App.Services.Implementation
{
_localSettings.SetValue(DefaultShellProfileKey, id);
}
public void SaveDefaultSshProfileId(Guid id)
{
_localSettings.SetValue(DefaultSshProfileKey, id);
}
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
{
if (!Enum.IsDefined(typeof(Command), command))
if (!Enum.TryParse<Command>(command, true, out var enumValue))
{
throw new InvalidOperationException();
}
_keyBindings.WriteValueAsJson(command.ToString(), keyBindings);
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
}
@@ -223,6 +364,18 @@ namespace FluentTerminal.App.Services.Implementation
ShellProfileAdded?.Invoke(this, shellProfile);
}
}
public void SaveSshProfile(SshProfile sshProfile, bool newShell = false)
{
_sshProfiles.WriteValueAsJson(sshProfile.Id.ToString(), sshProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
if (newShell)
{
SshProfileAdded?.Invoke(this, sshProfile);
}
}
public void SaveTerminalOptions(TerminalOptions terminalOptions)
{
@@ -5,8 +5,8 @@ using FluentTerminal.Models.Responses;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
namespace FluentTerminal.App.Services.Implementation
{
@@ -14,18 +14,18 @@ namespace FluentTerminal.App.Services.Implementation
{
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private readonly Dictionary<int, Action<string>> _terminalOutputHandlers;
private int _nextTerminalId = 0;
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers;
private byte _nextTerminalId = 0;
public event EventHandler<int> TerminalExited;
public event EventHandler<TerminalExitStatus> TerminalExited;
public TrayProcessCommunicationService(ISettingsService settingsService)
{
_settingsService = settingsService;
_terminalOutputHandlers = new Dictionary<int, Action<string>>();
_terminalOutputHandlers = new Dictionary<byte, Action<byte[]>>();
}
public int GetNextTerminalId()
public byte GetNextTerminalId()
{
return _nextTerminalId++;
}
@@ -35,11 +35,110 @@ namespace FluentTerminal.App.Services.Implementation
var request = new GetAvailablePortRequest();
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>((string)responseMessage[MessageKeys.Content]);
return JsonConvert.DeserializeObject<GetAvailablePortResponse>(responseMessage[MessageKeys.Content]);
Logger.Instance.Debug("Received GetAvailablePortResponse: {@response}", response);
return response;
}
public async Task<CreateTerminalResponse> CreateTerminal(int id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
private string _userName;
public async Task<string> GetUserName()
{
if (!string.IsNullOrEmpty(_userName))
{
// Returning the username from cache
return _userName;
}
GetUserNameResponse response;
// No need to crash for username, so try/catch
try
{
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new GetUserNameRequest()));
response = JsonConvert.DeserializeObject<GetUserNameResponse>((string)responseMessage[MessageKeys.Content]);
}
catch (Exception e)
{
Logger.Instance.Error(e, "Error while trying to get username.");
return null;
}
Logger.Instance.Debug("Received GetUserNameResponse: {@response}", response);
_userName = response.UserName;
return _userName;
}
public async Task SaveTextFileAsync(string path, string content)
{
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new SaveTextFileRequest {Path = path, Content = content}));
var response = JsonConvert.DeserializeObject<CommonResponse>((string)responseMessage[MessageKeys.Content]);
if (!response.Success)
{
throw new Exception(string.IsNullOrEmpty(response.Error) ? "Failed to save the file." : response.Error);
}
}
private static string _sshConfigDir;
public async Task<string> GetSshConfigDirAsync()
{
if (string.IsNullOrEmpty(_sshConfigDir))
{
var response = await GetSshConfigFolderAsync(false);
if (response?.Success ?? false)
{
_sshConfigDir = response.Path;
}
}
return _sshConfigDir;
}
public async Task<string[]> GetFilesFromSshConfigDirAsync()
{
var response = await GetSshConfigFolderAsync(false);
if (response == null || !response.Success)
{
return null;
}
if (string.IsNullOrEmpty(_sshConfigDir))
{
_sshConfigDir = response.Path;
}
return response.Files;
}
private async Task<GetSshConfigFolderResponse> GetSshConfigFolderAsync(bool includeContent)
{
var responseMessage =
await _appServiceConnection.SendMessageAsync(CreateMessage(new GetSshConfigFolderRequest
{IncludeContent = includeContent}));
return JsonConvert.DeserializeObject<GetSshConfigFolderResponse>(
(string) responseMessage[MessageKeys.Content]);
}
public async Task<bool> CheckFileExistsAsync(string path)
{
var responseMessage =
await _appServiceConnection.SendMessageAsync(CreateMessage(new CheckFileExistsRequest {Path = path}));
return JsonConvert.DeserializeObject<CommonResponse>((string) responseMessage[MessageKeys.Content]).Success;
}
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
{
var request = new CreateTerminalRequest
{
@@ -49,9 +148,14 @@ namespace FluentTerminal.App.Services.Implementation
SessionType = sessionType
};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
return JsonConvert.DeserializeObject<CreateTerminalResponse>(responseMessage[MessageKeys.Content]);
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>((string)responseMessage[MessageKeys.Content]);
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
return response;
}
public void Initialize(IAppServiceConnection appServiceConnection)
@@ -60,33 +164,38 @@ namespace FluentTerminal.App.Services.Implementation
_appServiceConnection.MessageReceived += OnMessageReceived;
}
private void OnMessageReceived(object sender, IDictionary<string, string> e)
private void OnMessageReceived(object sender, IDictionary<string, object> e)
{
var messageType = e[MessageKeys.Type];
var messageType = (byte)e[MessageKeys.Type];
var messageContent = e[MessageKeys.Content];
if (messageType == nameof(DisplayTerminalOutputRequest))
switch (messageType)
{
var request = JsonConvert.DeserializeObject<DisplayTerminalOutputRequest>(messageContent);
case Constants.TerminalBufferRequestIdentifier:
var terminalId = (byte)e[MessageKeys.TerminalId];
if (_terminalOutputHandlers.ContainsKey(request.TerminalId))
{
_terminalOutputHandlers[request.TerminalId].Invoke(request.Output);
}
else
{
Debug.WriteLine("output was not handled: " + request.Output);
}
}
else if (messageType == nameof(TerminalExitedRequest))
{
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>(messageContent);
if (_terminalOutputHandlers.ContainsKey(terminalId))
{
_terminalOutputHandlers[terminalId].Invoke((byte[])messageContent);
}
else
{
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId);
}
break;
case TerminalExitedRequest.Identifier:
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent);
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
TerminalExited?.Invoke(this, request.TerminalId);
TerminalExited?.Invoke(this, request.ToStatus());
break;
default:
Logger.Instance.Error("Received unknown message type: {messageType}", messageType);
break;
}
}
public Task ResizeTerminal(int id, TerminalSize size)
public Task ResizeTerminal(byte id, TerminalSize size)
{
var request = new ResizeTerminalRequest
{
@@ -97,7 +206,7 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public void SubscribeForTerminalOutput(int terminalId, Action<string> callback)
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
{
_terminalOutputHandlers[terminalId] = callback;
}
@@ -114,32 +223,32 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public Task WriteText(int id, string text)
public Task Write(byte id, byte[] data)
{
var request = new WriteTextRequest
var message = new ValueSet
{
TerminalId = id,
Text = text
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
[MessageKeys.TerminalId] = id,
[MessageKeys.Content] = data
};
return _appServiceConnection.SendMessageAsync(message);
}
public Task CloseTerminal(byte terminalId)
{
var request = new TerminalExitedRequest(terminalId, -1);
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public Task CloseTerminal(int terminalId)
private ValueSet CreateMessage(IMessage content)
{
var request = new TerminalExitedRequest
return new ValueSet
{
TerminalId = terminalId
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
private IDictionary<string, string> CreateMessage(object content)
{
return new Dictionary<string, string>
{
[MessageKeys.Type] = content.GetType().Name,
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
};
}
+68
View File
@@ -0,0 +1,68 @@
using Serilog;
using Serilog.Events;
using System;
namespace FluentTerminal.App.Services
{
public sealed class Logger
{
public enum LogLevel
{
Verbose = 0,
Debug = 1,
Information = 2,
Warning = 3,
Error = 4,
Fatal = 5
}
public class Configuration
{
public LogLevel LogLevel { get; set; } = LogLevel.Error;
}
public static Logger Instance { get; } = new Logger();
private Logger()
{
}
public void Initialize(string filePath, Configuration configuration)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(filePath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7))
.MinimumLevel.Is((LogEventLevel)configuration.LogLevel)
.CreateLogger();
// Log file is not created during unhandled exception processing.
// Force log file creation with the highest severity initialization record.
Log.Fatal("Initialized");
}
public void Debug(string text, params object[] propertyValues)
{
Log.Debug(text, propertyValues, propertyValues);
}
public void Information(string text, params object[] propertyValues)
{
Log.Information(text, propertyValues);
}
public void Warning(string text, params object[] propertyValues)
{
Log.Warning(text, propertyValues);
}
public void Error(string text, params object[] propertyValues)
{
Log.Error(text, propertyValues);
}
public void Error(Exception exception, string text, params object[] propertyValues)
{
Log.Error(exception, text, propertyValues);
}
}
}
+161 -125
View File
@@ -1,126 +1,162 @@
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);
}
}
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public class Terminal
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
private bool _closingFromUI = false;
private bool _exited = false;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
Id = _trayProcessCommunicationService.GetNextTerminalId();
}
private void OnTerminalExited(object sender, TerminalExitStatus status)
{
if (status.TerminalId != Id)
{
return;
}
_exited = true;
Exited?.Invoke(this, status.ExitCode);
if (_closingFromUI == true || status.ExitCode <= 0)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
public event EventHandler<int> Exited;
/// <summary>
/// To be observed by both view and viewmodel
/// </summary>
public event EventHandler Closed;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> KeyboardCommandReceived;
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<byte[]> OutputReceived;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<TerminalSize> SizeChanged;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public byte Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
{
if (_exited)
{
Closed?.Invoke(this, System.EventArgs.Empty);
return;
}
_closingFromUI = true;
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
}
/// <summary>
/// to be called by viewmodel
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback();
}
/// <summary>
/// to be called by view
/// </summary>
public void ProcessKeyboardCommand(string command)
{
KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be set by view
/// </summary>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
{
_selectedTextCallback = selectedTextCallback;
}
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
SizeChanged?.Invoke(this, size);
}
/// <summary>
/// to be called by view
/// </summary>
public void SetTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// </summary>
/// <param name="shellProfile"></param>
/// <param name="size"></param>
/// <param name="sessionType"></param>
public async Task<CreateTerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType)
{
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
}
return response;
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.Write(Id, data);
}
/// <summary>
/// Tells the ViewModel that this view failed to launch and can be closed.
/// </summary>
public void ReportLauchFailed()
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
}
@@ -0,0 +1,17 @@
using Windows.ApplicationModel.Resources;
namespace FluentTerminal.App.Services.Utilities
{
/// <summary>
/// Get strings in different languages.
/// Author: Zuozishi
/// </summary>
public static class I18N
{
public static string Translate(string resource)
{
var resourceLoader = ResourceLoader.GetForCurrentView();
return resourceLoader.GetString(resource.Replace('.', '/'));
}
}
}
@@ -5,9 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fleck" Version="1.0.3" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1" />
<PackageReference Include="ReactiveUI" Version="9.4.1" />
<PackageReference Include="Fleck" Version="1.1.0" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
</ItemGroup>
<ItemGroup>
+502 -341
View File
@@ -1,342 +1,503 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.EventArgs;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public class MainViewModel : ViewModelBase
{
private readonly IApplicationView _applicationView;
private readonly IClipboardService _clipboardService;
private readonly IDialogService _dialogService;
private readonly IDispatcherTimer _dispatcherTimer;
private readonly IKeyboardCommandService _keyboardCommandService;
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private ApplicationSettings _applicationSettings;
private string _background;
private double _backgroundOpacity;
private TerminalViewModel _selectedTerminal;
private TabsPosition _tabsPosition;
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
{
_settingsService = settingsService;
_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(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++)
{
var switchCmd = Command.SwitchToTerm1 + i;
int tabNumber = i;
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));
}
var currentTheme = _settingsService.GetCurrentTheme();
var options = _settingsService.GetTerminalOptions();
Background = currentTheme.Colors.Background;
BackgroundOpacity = options.BackgroundOpacity;
_applicationSettings = _settingsService.GetApplicationSettings();
TabsPosition = _applicationSettings.TabsPosition;
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));
RaisePropertyChanged(nameof(ShowTabsOnBottom));
}
public event EventHandler Closed;
public event EventHandler<NewWindowRequestedEventArgs> NewWindowRequested;
public event EventHandler ShowSettingsRequested;
public event EventHandler ShowAboutRequested;
public RelayCommand AddTerminalCommand { get; }
public bool ShowTabsOnTop
{
get => TabsPosition == TabsPosition.Top && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
}
public bool ShowTabsOnBottom
{
get => TabsPosition == TabsPosition.Bottom && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
}
public string Background
{
get => _background;
set => Set(ref _background, value);
}
public double BackgroundOpacity
{
get => _backgroundOpacity;
set => Set(ref _backgroundOpacity, value);
}
public TerminalViewModel SelectedTerminal
{
get => _selectedTerminal;
set
{
if (SelectedTerminal != null)
{
_selectedTerminal.IsSelected = false;
}
if (Set(ref _selectedTerminal, value))
{
SelectedTerminal?.FocusTerminal();
if (SelectedTerminal != null)
{
SelectedTerminal.IsSelected = true;
}
}
}
}
public RelayCommand ShowAboutCommand { get; }
public RelayCommand ShowSettingsCommand { get; }
public TabsPosition TabsPosition
{
get => _tabsPosition;
set => Set(ref _tabsPosition, value);
}
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
/// <summary>
/// Add a new terminal window, either with a specified terminal profile, with the default, or by showing the profile selection dialog.
/// </summary>
/// <returns></returns>
public Task AddTerminal(string startupDirectory, bool showProfileSelection, Guid profileId)
{
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 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);
terminal.Closed += OnTerminalCloseRequested;
Terminals.Add(terminal);
SelectedTerminal = terminal;
});
}
public void CloseAllTerminals()
{
foreach (var terminal in Terminals)
{
terminal.Close();
}
}
private void CloseCurrentTab()
{
SelectedTerminal?.CloseCommand.Execute(null);
}
private void NewWindow(bool showProfileSelection)
{
var args = new NewWindowRequestedEventArgs
{
ShowProfileSelection = showProfileSelection
};
NewWindowRequested?.Invoke(this, args);
}
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
await _applicationView.RunOnDispatcherThread(() =>
{
_applicationSettings = e;
TabsPosition = e.TabsPosition;
RaisePropertyChanged(nameof(ShowTabsOnTop));
RaisePropertyChanged(nameof(ShowTabsOnBottom));
});
}
private async Task OnCloseRequest(object sender, CancelableEventArgs e)
{
if (_applicationSettings.ConfirmClosingWindows)
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this window?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(false);
if (result == DialogButton.OK)
{
CloseAllTerminals();
Closed?.Invoke(this, EventArgs.Empty);
}
else
{
e.Cancelled = true;
}
}
else
{
CloseAllTerminals();
Closed?.Invoke(this, EventArgs.Empty);
}
}
private async void OnCurrentThemeChanged(object sender, Guid e)
{
await _applicationView.RunOnDispatcherThread(() =>
{
var currentTheme = _settingsService.GetTheme(e);
Background = currentTheme.Colors.Background;
});
}
private void OnTerminalCloseRequested(object sender, EventArgs e)
{
if (sender is TerminalViewModel terminal)
{
terminal.Close();
if (SelectedTerminal == terminal)
{
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
}
Terminals.Remove(terminal);
if (Terminals.Count == 0)
{
Closed?.Invoke(this, EventArgs.Empty);
_applicationView.TryClose();
}
}
}
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
await _applicationView.RunOnDispatcherThread(() =>
{
BackgroundOpacity = e.BackgroundOpacity;
});
}
private void SelectTabNumber(int tabNumber)
{
if (tabNumber < Terminals.Count)
{
SelectedTerminal = Terminals[tabNumber];
}
}
private void SelectNextTab()
{
var currentIndex = Terminals.IndexOf(SelectedTerminal);
var nextIndex = (currentIndex + 1) % Terminals.Count;
SelectedTerminal = Terminals[nextIndex];
}
private void SelectPreviousTab()
{
var currentIndex = Terminals.IndexOf(SelectedTerminal);
var previousIndex = (currentIndex - 1 + Terminals.Count) % Terminals.Count;
SelectedTerminal = Terminals[previousIndex];
}
private void ShowAbout()
{
ShowAboutRequested?.Invoke(this, EventArgs.Empty);
}
private void ShowSettings()
{
ShowSettingsRequested?.Invoke(this, EventArgs.Empty);
}
private void ToggleFullScreen()
{
_applicationView.ToggleFullScreen();
}
}
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.EventArgs;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public class MainViewModel : ViewModelBase
{
private readonly IClipboardService _clipboardService;
private readonly IDialogService _dialogService;
private readonly IDispatcherTimer _dispatcherTimer;
private readonly IKeyboardCommandService _keyboardCommandService;
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly ISshHelperService _sshHelperService;
private ApplicationSettings _applicationSettings;
private string _background;
private double _backgroundOpacity;
private TerminalViewModel _selectedTerminal;
private TabsPosition _tabsPosition;
private string _windowTitle;
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, ISshHelperService sshHelperService)
{
_settingsService = settingsService;
_settingsService.CurrentThemeChanged += OnCurrentThemeChanged;
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
_settingsService.ShellProfileAdded += OnShellProfileAdded;
_settingsService.ShellProfileDeleted += OnShellProfileDeleted;
_settingsService.SshProfileAdded += OnSshProfileAdded;
_settingsService.SshProfileDeleted += OnSshProfileDeleted;
_trayProcessCommunicationService = trayProcessCommunicationService;
_dialogService = dialogService;
ApplicationView = applicationView;
_dispatcherTimer = dispatcherTimer;
_clipboardService = clipboardService;
_sshHelperService = sshHelperService;
_keyboardCommandService = keyboardCommandService;
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), async () => await AddLocalTabAsync());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshTab), async () => await AddSshTabAsync());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewTab), async () => await AddConfigurableTerminal());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), async () => await SelectedTerminal.EditTitle());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.SavedSshNewTab), async () => await AddSavedSshTerminalAsync());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.SavedSshNewWindow), () => NewWindow(NewWindowAction.ShowSshProfileSelection));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshWindow), () => NewWindow(NewWindowAction.ShowSshInfoDialog));
// 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++)
{
var switchCmd = Command.SwitchToTerm1 + i;
int tabNumber = i;
// ReSharper disable once InconsistentNaming
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(NewWindowAction.StartDefaultLocalTerminal));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ConfigurableNewWindow), () => NewWindow(NewWindowAction.ShowProfileSelection));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ShowSettings), ShowSettings);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ToggleFullScreen), ToggleFullScreen);
foreach (ShellProfile profile in _settingsService.GetShellProfiles())
{
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), async () => await AddLocalTabOrWindowAsync(profile.Id));
}
foreach (SshProfile profile in _settingsService.GetSshProfiles())
{
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), async () => await AddSshTabOrWindowAsync(profile.Id));
}
var currentTheme = _settingsService.GetCurrentTheme();
var options = _settingsService.GetTerminalOptions();
Background = currentTheme.Colors.Background;
BackgroundOpacity = options.BackgroundOpacity;
_applicationSettings = _settingsService.GetApplicationSettings();
TabsPosition = _applicationSettings.TabsPosition;
AddLocalShellCommand = new RelayCommand(async () => await AddLocalTabAsync());
AddRemoteShellCommand = new RelayCommand(async () => await AddSshTabAsync());
ShowAboutCommand = new RelayCommand(ShowAbout);
ShowSettingsCommand = new RelayCommand(ShowSettings);
ApplicationView.CloseRequested += OnCloseRequest;
ApplicationView.Closed += OnClosed;
Terminals.CollectionChanged += OnTerminalsCollectionChanged;
}
private void OnClosed(object sender, EventArgs e)
{
Closed?.Invoke(this, e);
}
private void OnShellProfileDeleted(object sender, Guid e)
{
_keyboardCommandService.DeregisterCommandHandler(e.ToString());
}
private void OnShellProfileAdded(object sender, ShellProfile e)
{
_keyboardCommandService.RegisterCommandHandler(e.Id.ToString(), () => AddLocalTabOrWindowAsync(e.Id));
}
private void OnSshProfileAdded(object sender, ShellProfile e)
{
_keyboardCommandService.RegisterCommandHandler(e.Id.ToString(), () => AddSshTabOrWindowAsync(e.Id));
}
private void OnSshProfileDeleted(object sender, Guid e)
{
_keyboardCommandService.DeregisterCommandHandler(e.ToString());
}
private void OnTerminalsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged(nameof(ShowTabsOnTop));
RaisePropertyChanged(nameof(ShowTabsOnBottom));
}
public event EventHandler Closed;
public event EventHandler<NewWindowRequestedEventArgs> NewWindowRequested;
public event EventHandler ShowSettingsRequested;
public event EventHandler ShowAboutRequested;
public event EventHandler ActivatedMv;
public void FocusWindow()
{
ActivatedMv?.Invoke(this, EventArgs.Empty);
}
public RelayCommand AddLocalShellCommand { get; }
public RelayCommand AddRemoteShellCommand { get; }
public string WindowTitle
{
get => _windowTitle;
set
{
if (Set(ref _windowTitle, value))
{
ApplicationView.Title = value;
}
}
}
public bool ShowTabsOnTop
{
get => TabsPosition == TabsPosition.Top && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
}
public bool ShowTabsOnBottom
{
get => TabsPosition == TabsPosition.Bottom && (Terminals.Count > 1 || _applicationSettings.AlwaysShowTabs);
}
public string Background
{
get => _background;
set => Set(ref _background, value);
}
public double BackgroundOpacity
{
get => _backgroundOpacity;
set => Set(ref _backgroundOpacity, value);
}
public TerminalViewModel SelectedTerminal
{
get => _selectedTerminal;
set
{
if (SelectedTerminal != null)
{
_selectedTerminal.IsSelected = false;
}
if (Set(ref _selectedTerminal, value))
{
SelectedTerminal?.FocusTerminal();
if (SelectedTerminal != null)
{
SelectedTerminal.IsSelected = true;
if (_applicationSettings.ShowCustomTitleInTitlebar)
{
WindowTitle = SelectedTerminal.TabTitle;
}
else
{
WindowTitle = SelectedTerminal.ShellTitle;
}
}
}
}
}
public RelayCommand ShowAboutCommand { get; }
public RelayCommand ShowSettingsCommand { get; }
public TabsPosition TabsPosition
{
get => _tabsPosition;
set => Set(ref _tabsPosition, value);
}
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
public IApplicationView ApplicationView { get; }
public async Task AddConfigurableTerminal()
{
var profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (profile == null)
{
if (Terminals.Count == 0)
{
await ApplicationView.TryClose();
}
return;
}
await AddTerminalAsync(profile);
}
public async Task AddSshTabAsync()
{
SshProfile profile = await _sshHelperService.GetSshProfileAsync(new SshProfile());
if (profile == null)
{
// User selected "Cancel"
if (Terminals.Count == 0)
{
await ApplicationView.TryClose();
}
}
else
{
await AddTerminalAsync(profile);
}
}
public Task AddSshTabOrWindowAsync(Guid shellProfileId)
{
switch (_applicationSettings.NewTerminalLocation)
{
case NewTerminalLocation.Tab:
var profile = _settingsService.GetSshProfile(shellProfileId);
return AddTerminalAsync(profile);
case NewTerminalLocation.Window:
NewWindow(NewWindowAction.StartSshTerminal, shellProfileId);
return Task.FromResult(0);
default:
throw new ArgumentException("unknown NewTerminalLocation");
}
}
public async Task AddSavedSshTerminalAsync()
{
ShellProfile profile = await _sshHelperService.GetSavedSshProfileAsync();
if (profile == null)
{
// User selected "Cancel"
if (Terminals.Count == 0)
{
await ApplicationView.TryClose();
}
}
else
{
await AddTerminalAsync(profile);
}
}
public Task AddLocalTabAsync()
{
var profile = _settingsService.GetDefaultShellProfile();
return AddTerminalAsync(profile);
}
public Task AddLocalTabOrWindowAsync(Guid shellProfileId)
{
switch (_applicationSettings.NewTerminalLocation)
{
case NewTerminalLocation.Tab:
var profile = _settingsService.GetShellProfile(shellProfileId);
return AddTerminalAsync(profile);
case NewTerminalLocation.Window:
NewWindow(NewWindowAction.StartLocalTerminal, shellProfileId);
return Task.FromResult(0);
default:
throw new ArgumentException("unknown NewTerminalLocation");
}
}
public Task AddTerminalAsync(ShellProfile profile)
{
return ApplicationView.RunOnDispatcherThread(() =>
{
var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService,
_applicationSettings, profile, ApplicationView, _dispatcherTimer, _clipboardService);
terminal.Closed += OnTerminalClosed;
terminal.ShellTitleChanged += Terminal_ShellTitleChanged;
terminal.CustomTitleChanged += Terminal_CustomTitleChanged;
Terminals.Add(terminal);
SelectedTerminal = terminal;
});
}
private void Terminal_CustomTitleChanged(object sender, string e)
{
if (sender is TerminalViewModel terminal)
{
if (terminal.IsSelected && _applicationSettings.ShowCustomTitleInTitlebar)
{
if (string.IsNullOrWhiteSpace(e))
{
WindowTitle = terminal.ShellTitle;
}
else
{
WindowTitle = e;
}
}
}
}
private void Terminal_ShellTitleChanged(object sender, string e)
{
if (sender is TerminalViewModel terminal && terminal.IsSelected && !_applicationSettings.ShowCustomTitleInTitlebar)
{
WindowTitle = e;
}
}
public async Task CloseAllTerminals()
{
foreach (var terminal in Terminals)
{
terminal.Closed -= OnTerminalClosed;
terminal.ShellTitleChanged -= Terminal_ShellTitleChanged;
terminal.CustomTitleChanged -= Terminal_CustomTitleChanged;
await terminal.Close();
}
}
private void CloseCurrentTab()
{
SelectedTerminal?.CloseCommand.Execute(null);
}
private void NewWindow(NewWindowAction showSelection)
{
NewWindow(showSelection, Guid.Empty);
}
private void NewWindow(NewWindowAction showSelection, Guid profileId)
{
var args = new NewWindowRequestedEventArgs
{
Action = showSelection,
ProfileId = profileId
};
NewWindowRequested?.Invoke(this, args);
}
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
await ApplicationView.RunOnDispatcherThread(() =>
{
_applicationSettings = e;
TabsPosition = e.TabsPosition;
RaisePropertyChanged(nameof(ShowTabsOnTop));
RaisePropertyChanged(nameof(ShowTabsOnBottom));
if (e.ShowCustomTitleInTitlebar)
{
WindowTitle = SelectedTerminal?.TabTitle;
}
else
{
WindowTitle = SelectedTerminal?.ShellTitle;
}
});
}
private async Task OnCloseRequest(object sender, CancelableEventArgs e)
{
if (_applicationSettings.ConfirmClosingWindows)
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmCloseWindow"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(false);
if (result == DialogButton.OK)
{
await CloseAllTerminals();
}
else
{
e.Cancelled = true;
}
}
else
{
await CloseAllTerminals();
}
}
private async void OnCurrentThemeChanged(object sender, Guid e)
{
await ApplicationView.RunOnDispatcherThread(() =>
{
var currentTheme = _settingsService.GetTheme(e);
Background = currentTheme.Colors.Background;
});
}
private async void OnTerminalClosed(object sender, EventArgs e)
{
if (sender is TerminalViewModel terminal)
{
if (SelectedTerminal == terminal)
{
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
}
Logger.Instance.Debug("Terminal with Id: {@id} closed.", terminal.Terminal.Id);
Terminals.Remove(terminal);
if (Terminals.Count == 0)
{
await ApplicationView.TryClose();
}
}
}
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
await ApplicationView.RunOnDispatcherThread(() =>
{
BackgroundOpacity = e.BackgroundOpacity;
});
}
private void SelectTabNumber(int tabNumber)
{
if (tabNumber < Terminals.Count)
{
SelectedTerminal = Terminals[tabNumber];
}
}
private void SelectNextTab()
{
var currentIndex = Terminals.IndexOf(SelectedTerminal);
var nextIndex = (currentIndex + 1) % Terminals.Count;
SelectedTerminal = Terminals[nextIndex];
}
private void SelectPreviousTab()
{
var currentIndex = Terminals.IndexOf(SelectedTerminal);
var previousIndex = (currentIndex - 1 + Terminals.Count) % Terminals.Count;
SelectedTerminal = Terminals[previousIndex];
}
private void ShowAbout()
{
ShowAboutRequested?.Invoke(this, EventArgs.Empty);
}
private void ShowSettings()
{
ShowSettingsRequested?.Invoke(this, EventArgs.Empty);
}
private void ToggleFullScreen()
{
ApplicationView.ToggleFullScreen();
}
}
}
@@ -0,0 +1,51 @@
using FluentTerminal.App.Services;
using GalaSoft.MvvmLight;
using System;
namespace FluentTerminal.App.ViewModels
{
public class OverlayViewModel : ViewModelBase
{
private readonly IDispatcherTimer _overlayTimer;
private bool _showOverlay;
private string _overlayContent;
public OverlayViewModel(IDispatcherTimer dispatcherTimer)
{
_overlayTimer = dispatcherTimer;
_overlayTimer.Interval = new TimeSpan(0, 0, 2);
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
}
public bool ShowOverlay
{
get => _showOverlay;
set => Set(ref _showOverlay, value);
}
public string OverlayContent
{
get => _overlayContent;
set => Set(ref _overlayContent, value);
}
public void Show(string message)
{
OverlayContent = message;
ShowOverlay = true;
if (_overlayTimer.IsEnabled)
{
_overlayTimer.Stop();
}
_overlayTimer.Start();
}
private void OnResizeOverlayTimerFinished(object sender, object e)
{
_overlayTimer.Stop();
ShowOverlay = false;
}
}
}
@@ -1,8 +1,10 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
@@ -15,25 +17,44 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly ISettingsService _settingsService;
private readonly IStartupTaskService _startupTaskService;
private bool _canEnableStartupTask;
private bool _editingNewTerminalLocation;
private bool _editingTabsPosition;
private bool _editingInactiveTabColorMode;
private bool _startupTaskEnabled;
private bool _shouldRestartForTrayMessage;
private string _startupTaskErrorMessage;
private bool _needsToRestart;
private readonly IApplicationLanguageService _applicationLanguageService;
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, IStartupTaskService startupTaskService)
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService)
{
_settingsService = settingsService;
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_startupTaskService = startupTaskService;
_applicationLanguageService = applicationLanguageService;
_applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
}
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
public bool NeedsToRestart
{
get => _needsToRestart;
set => Set(ref _needsToRestart, value);
}
public string SelectedLanguage
{
get => _applicationLanguageService.GetCurrentLanguage();
set
{
_applicationLanguageService.SetLanguage(value);
NeedsToRestart = true;
}
}
public async Task OnNavigatedTo()
{
var startupTaskStatus = await _startupTaskService.GetStatus();
@@ -54,14 +75,14 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool AlwaysUseWinPty
public bool ShowCustomTitleInTitlebar
{
get => _applicationSettings.AlwaysUseWinPty;
get => _applicationSettings.ShowCustomTitleInTitlebar;
set
{
if (_applicationSettings.AlwaysUseWinPty != value)
if (_applicationSettings.ShowCustomTitleInTitlebar != value)
{
_applicationSettings.AlwaysUseWinPty = value;
_applicationSettings.ShowCustomTitleInTitlebar = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
@@ -105,10 +126,52 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool UseMoshByDefault
{
get => _applicationSettings.UseMoshByDefault;
set
{
if (_applicationSettings.UseMoshByDefault != value)
{
_applicationSettings.UseMoshByDefault = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool AutoFallbackToWindowsUsernameInLinks
{
get => _applicationSettings.AutoFallbackToWindowsUsernameInLinks;
set
{
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks != value)
{
_applicationSettings.AutoFallbackToWindowsUsernameInLinks = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool RTrimCopiedLines
{
get => _applicationSettings.RTrimCopiedLines;
set
{
if (_applicationSettings.RTrimCopiedLines != value)
{
_applicationSettings.RTrimCopiedLines = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool BottomIsSelected
{
get => TabsPosition == TabsPosition.Bottom;
set => TabsPosition = TabsPosition.Bottom;
set { if (value) TabsPosition = TabsPosition.Bottom; }
}
public bool CanEnableStartupTask
@@ -150,15 +213,11 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.NewTerminalLocation;
set
{
if (_applicationSettings.NewTerminalLocation != value && !_editingNewTerminalLocation)
if (_applicationSettings.NewTerminalLocation != value)
{
_editingNewTerminalLocation = true;
_applicationSettings.NewTerminalLocation = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(WindowIsSelected));
RaisePropertyChanged(nameof(TabIsSelected));
_editingNewTerminalLocation = false;
}
}
}
@@ -188,7 +247,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool TabIsSelected
{
get => NewTerminalLocation == NewTerminalLocation.Tab;
set => NewTerminalLocation = NewTerminalLocation.Tab;
set { if (value) NewTerminalLocation = NewTerminalLocation.Tab; }
}
public TabsPosition TabsPosition
@@ -196,15 +255,11 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.TabsPosition;
set
{
if (_applicationSettings.TabsPosition != value && !_editingTabsPosition)
if (_applicationSettings.TabsPosition != value)
{
_editingTabsPosition = true;
_applicationSettings.TabsPosition = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(TopIsSelected));
RaisePropertyChanged(nameof(BottomIsSelected));
_editingTabsPosition = false;
}
}
}
@@ -212,7 +267,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool TopIsSelected
{
get => TabsPosition == TabsPosition.Top;
set => TabsPosition = TabsPosition.Top;
set { if (value) TabsPosition = TabsPosition.Top; }
}
public bool UnderlineSelectedTab
@@ -232,13 +287,13 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool BackgroundIsSelected
{
get => InactiveTabColorMode == InactiveTabColorMode.Background;
set => InactiveTabColorMode = InactiveTabColorMode.Background;
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Background; }
}
public bool UnderlinedIsSelected
{
get => InactiveTabColorMode == InactiveTabColorMode.Underlined;
set => InactiveTabColorMode = InactiveTabColorMode.Underlined;
set { if (value) InactiveTabColorMode = InactiveTabColorMode.Underlined; }
}
public InactiveTabColorMode InactiveTabColorMode
@@ -246,15 +301,11 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.InactiveTabColorMode;
set
{
if (_applicationSettings.InactiveTabColorMode != value && !_editingInactiveTabColorMode)
if (_applicationSettings.InactiveTabColorMode != value)
{
_editingInactiveTabColorMode = true;
_applicationSettings.InactiveTabColorMode = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(BackgroundIsSelected));
RaisePropertyChanged(nameof(UnderlinedIsSelected));
_editingInactiveTabColorMode = false;
}
}
}
@@ -262,12 +313,12 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool WindowIsSelected
{
get => NewTerminalLocation == NewTerminalLocation.Window;
set => NewTerminalLocation = NewTerminalLocation.Window;
set { if (value) NewTerminalLocation = NewTerminalLocation.Window; }
}
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the general settings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -278,9 +329,12 @@ namespace FluentTerminal.App.ViewModels.Settings
InactiveTabColorMode = defaults.InactiveTabColorMode;
NewTerminalLocation = defaults.NewTerminalLocation;
AlwaysShowTabs = defaults.AlwaysShowTabs;
AlwaysUseWinPty = defaults.AlwaysUseWinPty;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon;
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
UseMoshByDefault = defaults.UseMoshByDefault;
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
RTrimCopiedLines = defaults.RTrimCopiedLines;
}
}
@@ -302,13 +356,13 @@ namespace FluentTerminal.App.ViewModels.Settings
case StartupTaskStatus.DisabledByUser:
StartupTaskEnabled = false;
StartupTaskErrorMessage = "Disabled by user. Please reactivate it in the Startup tab of the Task Manager.";
StartupTaskErrorMessage = I18N.Translate("DisabledByUser");
CanEnableStartupTask = false;
break;
case StartupTaskStatus.DisabledByPolicy:
StartupTaskEnabled = false;
StartupTaskErrorMessage = "Disabled by policy.";
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy");
CanEnableStartupTask = false;
break;
}
@@ -323,7 +377,7 @@ namespace FluentTerminal.App.ViewModels.Settings
}
else
{
_startupTaskService.DisableStartupTask();
await _startupTaskService.DisableStartupTask();
status = await _startupTaskService.GetStatus();
}
SetStartupTaskPropertiesForStatus(status);
@@ -1,4 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
@@ -118,7 +119,7 @@ namespace FluentTerminal.App.ViewModels.Settings
private async Task Delete()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this keybinding?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -61,7 +61,7 @@ namespace FluentTerminal.App.ViewModels.Settings
foreach (var value in Enum.GetValues(typeof(Command)))
{
var command = (Command)value;
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, EnumHelper.GetEnumDescription(command), true);
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, I18N.Translate($"{nameof(Command)}.{command}"), true);
foreach (var keyBinding in _keyBindings[command.ToString()])
{
viewModel.Add(keyBinding);
@@ -79,7 +79,7 @@ namespace FluentTerminal.App.ViewModels.Settings
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the default keybindings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -1,11 +1,9 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
@@ -16,8 +14,6 @@ namespace FluentTerminal.App.ViewModels.Settings
private readonly IDialogService _dialogService;
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly ApplicationSettings _applicationSettings;
private bool _editingMouseRightClickAction;
private bool _editingMouseMiddleClickAction;
public MousePageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider)
{
@@ -48,16 +44,11 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.MouseRightClickAction;
set
{
if (_applicationSettings.MouseRightClickAction != value && !_editingMouseRightClickAction)
if (_applicationSettings.MouseRightClickAction != value)
{
_editingMouseRightClickAction = true;
_applicationSettings.MouseRightClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(MouseRightClickNoneIsSelected));
RaisePropertyChanged(nameof(MouseRightClickContextMenuIsSelected));
RaisePropertyChanged(nameof(MouseRightClickPasteIsSelected));
_editingMouseRightClickAction = false;
}
}
}
@@ -65,19 +56,19 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool MouseRightClickNoneIsSelected
{
get => MouseRightClickAction == MouseAction.None;
set => MouseRightClickAction = MouseAction.None;
set { if (value) MouseRightClickAction = MouseAction.None; }
}
public bool MouseRightClickContextMenuIsSelected
{
get => MouseRightClickAction == MouseAction.ContextMenu;
set => MouseRightClickAction = MouseAction.ContextMenu;
set { if (value) MouseRightClickAction = MouseAction.ContextMenu; }
}
public bool MouseRightClickPasteIsSelected
{
get => MouseRightClickAction == MouseAction.Paste;
set => MouseRightClickAction = MouseAction.Paste;
set { if (value) MouseRightClickAction = MouseAction.Paste; }
}
public MouseAction MouseMiddleClickAction
@@ -85,16 +76,11 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _applicationSettings.MouseMiddleClickAction;
set
{
if (_applicationSettings.MouseMiddleClickAction != value && !_editingMouseMiddleClickAction)
if (_applicationSettings.MouseMiddleClickAction != value)
{
_editingMouseMiddleClickAction = true;
_applicationSettings.MouseMiddleClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(MouseMiddleClickNoneIsSelected));
RaisePropertyChanged(nameof(MouseMiddleClickContextMenuIsSelected));
RaisePropertyChanged(nameof(MouseMiddleClickPasteIsSelected));
_editingMouseMiddleClickAction = false;
}
}
}
@@ -102,26 +88,26 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool MouseMiddleClickNoneIsSelected
{
get => MouseMiddleClickAction == MouseAction.None;
set => MouseMiddleClickAction = MouseAction.None;
set { if (value) MouseMiddleClickAction = MouseAction.None; }
}
public bool MouseMiddleClickContextMenuIsSelected
{
get => MouseMiddleClickAction == MouseAction.ContextMenu;
set => MouseMiddleClickAction = MouseAction.ContextMenu;
set { if (value) MouseMiddleClickAction = MouseAction.ContextMenu; }
}
public bool MouseMiddleClickPasteIsSelected
{
get => MouseMiddleClickAction == MouseAction.Paste;
set => MouseMiddleClickAction = MouseAction.Paste;
set { if (value) MouseMiddleClickAction = MouseAction.Paste; }
}
public RelayCommand RestoreDefaultsCommand { get; }
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the mouse settings?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -98,12 +98,11 @@ namespace FluentTerminal.App.ViewModels.Settings
private void Clone(ShellProfileViewModel shellProfile)
{
var cloned = new ShellProfile(shellProfile.Model)
{
Id = Guid.NewGuid(),
PreInstalled = false,
Name = $"Copy of {shellProfile.Name}"
};
var cloned = shellProfile.Model.Clone();
cloned.Id = Guid.NewGuid();
cloned.PreInstalled = false;
cloned.Name = $"Copy of {shellProfile.Name}";
AddShellProfile(cloned);
}
@@ -0,0 +1,118 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
namespace FluentTerminal.App.ViewModels.Settings
{
public class SshProfilesPageViewModel: ViewModelBase
{
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private SshProfileViewModel _selectedShellProfile;
private readonly IApplicationView _applicationView;
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
public SshProfilesPageViewModel(ISettingsService settingsService, IDialogService dialogService,
IFileSystemService fileSystemService, IApplicationView applicationView,
IDefaultValueProvider defaultValueProvider,
ITrayProcessCommunicationService trayProcessCommunicationService)
{
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_defaultValueProvider = defaultValueProvider;
_trayProcessCommunicationService = trayProcessCommunicationService;
CreateSshProfileCommand = new RelayCommand(CreateSshProfile);
CloneCommand = new RelayCommand<SshProfileViewModel>(Clone);
foreach (var sshProfile in _settingsService.GetSshProfiles())
{
var viewModel = new SshProfileViewModel(sshProfile, settingsService, dialogService,
fileSystemService, applicationView, defaultValueProvider, _trayProcessCommunicationService, false);
viewModel.Deleted += OnSshProfileDeleted;
SshProfiles.Add(viewModel);
}
if (SshProfiles.Count == 0)
{
CreateSshProfile();
}
SelectedSshProfile = SshProfiles.First();
}
private void OnSshProfileDeleted(object sender, EventArgs e)
{
if (sender is SshProfileViewModel shellProfile)
{
if (SelectedSshProfile == shellProfile)
{
SelectedSshProfile = SshProfiles.First();
}
SshProfiles.Remove(shellProfile);
_settingsService.DeleteSshProfile(shellProfile.Id);
}
}
public RelayCommand CreateSshProfileCommand { get; }
public RelayCommand<SshProfileViewModel> CloneCommand { get; }
public void CreateSshProfile()
{
var shellProfile = new SshProfile
{
Id = Guid.NewGuid(),
PreInstalled = false,
Name = "New SSH profile",
KeyBindings = new List<KeyBinding>(),
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault
};
AddSshProfile(shellProfile);
}
private void Clone(SshProfileViewModel shellProfile)
{
var cloned = (SshProfile) shellProfile.Model.Clone();
cloned.Id = Guid.NewGuid();
cloned.PreInstalled = false;
cloned.Name = $"Copy of {shellProfile.Name}";
foreach (KeyBinding keyBinding in cloned.KeyBindings)
{
keyBinding.Command = cloned.Id.ToString();
}
AddSshProfile(cloned);
}
private void AddSshProfile(SshProfile sshProfile)
{
var viewModel = new SshProfileViewModel(sshProfile, _settingsService, _dialogService, _fileSystemService,
_applicationView, _defaultValueProvider, _trayProcessCommunicationService, true);
viewModel.EditCommand.Execute(null);
viewModel.Deleted += OnSshProfileDeleted;
SshProfiles.Add(viewModel);
SelectedSshProfile = viewModel;
}
public SshProfileViewModel SelectedSshProfile
{
get => _selectedShellProfile;
set => Set(ref _selectedShellProfile, value);
}
public ObservableCollection<SshProfileViewModel> SshProfiles { get; } = new ObservableCollection<SshProfileViewModel>();
}
}
@@ -1,59 +1,56 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
{
public class TerminalPageViewModel : ReactiveObject
public class TerminalPageViewModel : ViewModelBase
{
private readonly TerminalOptions _terminalOptions;
private readonly ISettingsService _settingsService;
private readonly IDialogService _dialogService;
private readonly IDefaultValueProvider _defaultValueProvider;
private bool _isEditingCursorStyle;
private bool _isEditingScrollBarStyle;
public bool BarIsSelected
{
get => CursorStyle == CursorStyle.Bar;
set => CursorStyle = CursorStyle.Bar;
set { if (value) CursorStyle = CursorStyle.Bar; }
}
public bool BlockIsSelected
{
get => CursorStyle == CursorStyle.Block;
set => CursorStyle = CursorStyle.Block;
set { if (value) CursorStyle = CursorStyle.Block; }
}
public bool UnderlineIsSelected
{
get => CursorStyle == CursorStyle.Underline;
set => CursorStyle = CursorStyle.Underline;
set { if (value) CursorStyle = CursorStyle.Underline; }
}
public bool HiddenIsSelected
{
get => ScrollBarStyle == ScrollBarStyle.Hidden;
set => ScrollBarStyle = ScrollBarStyle.Hidden;
set { if (value) ScrollBarStyle = ScrollBarStyle.Hidden; }
}
public bool AutoHidingIsSelected
{
get => ScrollBarStyle == ScrollBarStyle.AutoHiding;
set => ScrollBarStyle = ScrollBarStyle.AutoHiding;
set { if (value) ScrollBarStyle = ScrollBarStyle.AutoHiding; }
}
public bool VisibleIsSelected
{
get => ScrollBarStyle == ScrollBarStyle.Visible;
set => ScrollBarStyle = ScrollBarStyle.Visible;
set { if (value) ScrollBarStyle = ScrollBarStyle.Visible; }
}
public bool CursorBlink
@@ -65,7 +62,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.CursorBlink = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -79,7 +76,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontFamily = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -92,7 +89,8 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_terminalOptions.BackgroundOpacity != value)
{
_terminalOptions.BackgroundOpacity = value;
this.RaisePropertyChanged();
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
@@ -105,7 +103,8 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_terminalOptions.Padding != value)
{
_terminalOptions.Padding = value;
this.RaisePropertyChanged();
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
@@ -126,12 +125,12 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.ScrollBackLimit = intValue;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
else
{
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -145,12 +144,26 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
public IEnumerable<string> Fonts { get; }
public bool ShowTextCopied
{
get => _terminalOptions.ShowTextCopied;
set
{
if (_terminalOptions.ShowTextCopied != value)
{
_terminalOptions.ShowTextCopied = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public IEnumerable<FontInfo> Fonts { get; }
public int FontSize
{
@@ -161,7 +174,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontSize = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
RaisePropertyChanged();
}
}
}
@@ -175,16 +188,11 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.CursorStyle;
set
{
if (_terminalOptions.CursorStyle != value && !_isEditingCursorStyle)
if (_terminalOptions.CursorStyle != value)
{
_isEditingCursorStyle = true;
_terminalOptions.CursorStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(BlockIsSelected));
this.RaisePropertyChanged(nameof(BarIsSelected));
this.RaisePropertyChanged(nameof(UnderlineIsSelected));
_isEditingCursorStyle = false;
RaisePropertyChanged();
}
}
}
@@ -194,23 +202,18 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.ScrollBarStyle;
set
{
if (_terminalOptions.ScrollBarStyle != value && !_isEditingScrollBarStyle)
if (_terminalOptions.ScrollBarStyle != value)
{
_isEditingScrollBarStyle = true;
_terminalOptions.ScrollBarStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(HiddenIsSelected));
this.RaisePropertyChanged(nameof(AutoHidingIsSelected));
this.RaisePropertyChanged(nameof(VisibleIsSelected));
_isEditingScrollBarStyle = false;
RaisePropertyChanged();
}
}
}
private async Task RestoreDefaults()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to restore the default terminal options?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -223,6 +226,7 @@ namespace FluentTerminal.App.ViewModels.Settings
BoldText = defaults.BoldText;
BackgroundOpacity = defaults.BackgroundOpacity;
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
ShowTextCopied = defaults.ShowTextCopied;
}
}
@@ -234,19 +238,10 @@ namespace FluentTerminal.App.ViewModels.Settings
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s);
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
Sizes = Enumerable.Range(1, 72);
_terminalOptions = _settingsService.GetTerminalOptions();
this.ObservableForProperty(x => x.BackgroundOpacity).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
{
_settingsService.SaveTerminalOptions(_terminalOptions);
});
this.ObservableForProperty(x => x.Padding).Throttle(TimeSpan.FromMilliseconds(800)).ObserveOn(RxApp.MainThreadScheduler).Subscribe(x =>
{
_settingsService.SaveTerminalOptions(_terminalOptions);
});
}
}
}
@@ -1,4 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
@@ -118,7 +119,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (parser == null)
{
await _dialogService.ShowMessageDialogAsnyc("Import theme failed", "No suitable parser found", DialogButton.OK).ConfigureAwait(false);
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
return;
}
@@ -130,7 +131,7 @@ namespace FluentTerminal.App.ViewModels.Settings
}
catch (Exception exception)
{
await _dialogService.ShowMessageDialogAsnyc("Import theme failed", exception.Message, DialogButton.OK).ConfigureAwait(false);
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK).ConfigureAwait(false);
}
}
}
@@ -9,15 +9,16 @@ namespace FluentTerminal.App.ViewModels
{
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView applicationView)
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IUpdateService updateService, IApplicationView applicationView, IApplicationLanguageService applicationLanguageService)
{
About = new AboutPageViewModel(settingsService, updateService, applicationView);
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, defaultValueProvider, trayProcessCommunicationService);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService);
Shell = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService);
Profiles = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService);
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService);
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService, applicationView, defaultValueProvider, trayProcessCommunicationService);
}
public event EventHandler Closed;
@@ -25,11 +26,12 @@ namespace FluentTerminal.App.ViewModels
public GeneralPageViewModel General { get; }
public KeyBindingsPageViewModel KeyBindings { get; }
public ProfilesPageViewModel Shell { get; }
public ProfilesPageViewModel Profiles { get; }
public TerminalPageViewModel Terminal { get; }
public ThemesPageViewModel Themes { get; }
public MousePageViewModel Mouse { get; }
public AboutPageViewModel About { get; }
public SshProfilesPageViewModel SshProfiles { get; }
public void NavigateToAboutPage()
{
@@ -1,4 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models;
@@ -14,76 +15,261 @@ namespace FluentTerminal.App.ViewModels
{
public class ShellProfileViewModel : ViewModelBase
{
private readonly IDialogService _dialogService;
private readonly IFileSystemService _fileSystemService;
private readonly ISettingsService _settingsService;
private string _arguments;
private ShellProfile _fallbackProfile;
private bool _inEditMode;
private bool _isDefault;
private string _location;
private LineEndingStyle _lineEndingStyle;
private string _name;
private bool _isNew;
private TabTheme _selectedTabTheme;
private TerminalTheme _selectedTerminalTheme;
private string _workingDirectory;
private readonly IApplicationView _applicationView;
private readonly IDefaultValueProvider _defaultValueProvider;
private bool _editingLineEndingStyle;
#region Static
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView, IDefaultValueProvider defaultValueProvider, Boolean isNew)
private static readonly LineEndingStyle[] LineEndingStylesArray =
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
#endregion Static
#region Fields
private ShellProfile _fallbackProfile;
protected readonly IDialogService DialogService;
protected readonly IFileSystemService FileSystemService;
protected readonly ISettingsService SettingsService;
protected readonly IApplicationView ApplicationView;
protected readonly IDefaultValueProvider DefaultValueProvider;
#endregion Fields
#region Properties
protected bool IsNew { get; set; }
public Guid Id { get; private set; }
public ShellProfile Model { get; private set; }
public bool PreInstalled { get; private set; }
private bool _useConPty;
public bool UseConPty
{
get => _useConPty;
set => Set(ref _useConPty, value);
}
private string _arguments;
public string Arguments
{
get => _arguments;
set => Set(ref _arguments, value);
}
private bool _inEditMode;
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
}
private bool _isDefault;
public bool IsDefault
{
get => _isDefault;
set => Set(ref _isDefault, value);
}
private string _location;
public string Location
{
get => _location;
set => Set(ref _location, value);
}
private string _name;
public string Name
{
get => _name;
set => Set(ref _name, value);
}
private TabTheme _selectedTabTheme;
public TabTheme SelectedTabTheme
{
get => _selectedTabTheme ?? (_selectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(_tabThemeId)));
set
{
if (value != null && Set(ref _selectedTabTheme, value))
{
_tabThemeId = value.Id;
}
}
}
private int _tabThemeId;
public int TabThemeId
{
get => _tabThemeId;
set
{
if (Set(ref _tabThemeId, value))
{
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(value));
}
}
}
private TerminalTheme _selectedTerminalTheme;
public TerminalTheme SelectedTerminalTheme
{
get => _selectedTerminalTheme ??
(_selectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(_terminalThemeId)));
set
{
if (value != null && Set(ref _selectedTerminalTheme, value))
{
_terminalThemeId = value.Id;
}
}
}
private Guid _terminalThemeId;
public Guid TerminalThemeId
{
get => _terminalThemeId;
set
{
if (Set(ref _terminalThemeId, value))
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
}
}
}
private string _workingDirectory;
public string WorkingDirectory
{
get => _workingDirectory;
set => Set(ref _workingDirectory, value);
}
private LineEndingStyle _lineEndingTranslation;
public LineEndingStyle LineEndingTranslation
{
get => _lineEndingTranslation;
set => Set(ref _lineEndingTranslation, value);
}
public ObservableCollection<TabTheme> TabThemes { get; }
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
#endregion Properties
#region Events
public event EventHandler Deleted;
public event EventHandler SetAsDefault;
#endregion Events
#region Commands
public IAsyncCommand BrowseForCustomShellCommand { get; }
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
public IAsyncCommand CancelEditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public IAsyncCommand SaveChangesCommand { get; }
public RelayCommand SetDefaultCommand { get; }
public IAsyncCommand RestoreDefaultsCommand { get; }
public IAsyncCommand AddKeyboardShortcutCommand { get; }
#endregion Commands
#region Constrcutor
public ShellProfileViewModel(ShellProfile shellProfile, ISettingsService settingsService,
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
IDefaultValueProvider defaultValueProvider, bool isNew)
{
Model = shellProfile;
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_applicationView = applicationView;
_defaultValueProvider = defaultValueProvider;
_isNew = isNew;
SettingsService = settingsService;
DialogService = dialogService;
FileSystemService = fileSystemService;
ApplicationView = applicationView;
DefaultValueProvider = defaultValueProvider;
IsNew = isNew;
_settingsService.ThemeAdded += OnThemeAdded;
_settingsService.ThemeDeleted += OnThemeDeleted;
SettingsService.ThemeAdded += OnThemeAdded;
SettingsService.ThemeDeleted += OnThemeDeleted;
TabThemes = new ObservableCollection<TabTheme>(settingsService.GetTabThemes());
TerminalThemes = new ObservableCollection<TerminalTheme>();
TerminalThemes.Add(new TerminalTheme
TerminalThemes = new ObservableCollection<TerminalTheme>
{
Id = Guid.Empty,
Name = "Default"
});
foreach (var theme in _settingsService.GetThemes())
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);
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), DialogService, string.Empty, false);
InitializeViewModelProperties(shellProfile);
InitializeViewModelPropertiesPrivate(shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
SaveChangesCommand = new RelayCommand(SaveChanges);
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
}
private void InitializeViewModelProperties(ShellProfile shellProfile)
#endregion Constrcutor
#region Methods
// Loads view model properties from the input shellProfile
private void InitializeViewModelPropertiesPrivate(ShellProfile shellProfile)
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id == shellProfile.TerminalThemeId);
Id = shellProfile.Id;
Name = shellProfile.Name;
Arguments = shellProfile.Arguments;
Location = shellProfile.Location;
WorkingDirectory = shellProfile.WorkingDirectory;
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id == shellProfile.TabThemeId);
TerminalThemeId = shellProfile.TerminalThemeId;
TabThemeId = shellProfile.TabThemeId;
PreInstalled = shellProfile.PreInstalled;
LineEndingStyle = shellProfile.LineEndingTranslation;
LineEndingTranslation = shellProfile.LineEndingTranslation;
UseConPty = shellProfile.UseConPty;
KeyBindings.Clear();
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
@@ -92,9 +278,41 @@ namespace FluentTerminal.App.ViewModels
}
}
// Loads view model properties from the input shellProfile
protected virtual void InitializeViewModelProperties(ShellProfile shellProfile) =>
InitializeViewModelPropertiesPrivate(shellProfile);
// Fills the input profile properties the view model properties. (Inverse of InitializeViewModelProperties()).
protected virtual Task FillProfileAsync(ShellProfile profile)
{
profile.Id = Id;
profile.Name = Name;
profile.Arguments = Arguments;
profile.Location = Location;
profile.WorkingDirectory = WorkingDirectory;
profile.TerminalThemeId = TerminalThemeId;
profile.TabThemeId = TabThemeId;
profile.PreInstalled = PreInstalled;
profile.LineEndingTranslation = LineEndingTranslation;
profile.UseConPty = UseConPty;
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
return Task.CompletedTask;
}
// Used to "remember" the current state.
protected virtual async Task<ShellProfile> CreateProfileAsync()
{
var profile = new ShellProfile();
await FillProfileAsync(profile);
return profile;
}
private void OnThemeAdded(object sender, TerminalTheme e)
{
_applicationView.RunOnDispatcherThread(() =>
ApplicationView.RunOnDispatcherThread(() =>
{
TerminalThemes.Add(e);
});
@@ -104,7 +322,7 @@ namespace FluentTerminal.App.ViewModels
{
if (SelectedTerminalTheme.Id == e)
{
_applicationView.RunOnDispatcherThread(() =>
ApplicationView.RunOnDispatcherThread(() =>
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(x => x.Id == Guid.Empty);
Model.TerminalThemeId = Guid.Empty;
@@ -116,138 +334,15 @@ namespace FluentTerminal.App.ViewModels
}
}
public event EventHandler Deleted;
public event EventHandler SetAsDefault;
public IAsyncCommand BrowseForCustomShellCommand { get; }
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
public IAsyncCommand CancelEditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public RelayCommand SaveChangesCommand { get; }
public RelayCommand SetDefaultCommand { get; }
public IAsyncCommand RestoreDefaultsCommand { get; }
public IAsyncCommand AddKeyboardShortcutCommand { get; }
public ObservableCollection<TabTheme> TabThemes { get; }
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ShellProfile Model { get; private set; }
public bool PreInstalled { get; private set; }
public string Arguments
public virtual async Task SaveChangesAsync()
{
get => _arguments;
set => Set(ref _arguments, value);
}
await FillProfileAsync(Model);
public Guid Id { get; private set; }
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
}
public bool IsDefault
{
get => _isDefault;
set => Set(ref _isDefault, value);
}
public string Location
{
get => _location;
set => Set(ref _location, value);
}
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public TabTheme SelectedTabTheme
{
get => _selectedTabTheme;
set
{
if (value != null)
{
Set(ref _selectedTabTheme, value);
}
}
}
public TerminalTheme SelectedTerminalTheme
{
get => _selectedTerminalTheme;
set
{
if (value != null)
{
Set(ref _selectedTerminalTheme, value);
}
}
}
public string WorkingDirectory
{
get => _workingDirectory;
set => Set(ref _workingDirectory, value);
}
public LineEndingStyle LineEndingStyle
{
get => _lineEndingStyle;
set
{
if (_lineEndingStyle != value && !_editingLineEndingStyle)
{
_editingLineEndingStyle = true;
_lineEndingStyle = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(DoNotModifyIsSelected));
RaisePropertyChanged(nameof(ToCRLFIsSelected));
RaisePropertyChanged(nameof(ToLFIsSelected));
_editingLineEndingStyle = false;
}
}
}
public bool DoNotModifyIsSelected
{
get => LineEndingStyle == LineEndingStyle.DoNotModify;
set => LineEndingStyle = LineEndingStyle.DoNotModify;
}
public bool ToCRLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToCRLF;
set => LineEndingStyle = LineEndingStyle.ToCRLF;
}
public bool ToLFIsSelected
{
get => LineEndingStyle == LineEndingStyle.ToLF;
set => LineEndingStyle = LineEndingStyle.ToLF;
}
public void SaveChanges()
{
Model.Arguments = Arguments;
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);
SettingsService.SaveShellProfile(Model);
KeyBindings.Editable = false;
InEditMode = false;
_isNew = false;
IsNew = false;
}
private async Task RestoreDefaults()
@@ -257,13 +352,14 @@ namespace FluentTerminal.App.ViewModels
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);
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Model = _defaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id == Model.Id);
Model = DefaultValueProvider.GetPreinstalledShellProfiles().FirstOrDefault(x => x.Id == Model.Id);
InitializeViewModelProperties(Model);
_settingsService.SaveShellProfile(Model);
SettingsService.SaveShellProfile(Model);
}
}
@@ -274,7 +370,7 @@ namespace FluentTerminal.App.ViewModels
private async Task BrowseForCustomShell()
{
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
var file = await FileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
if (file != null)
{
Location = file.Path;
@@ -283,7 +379,7 @@ namespace FluentTerminal.App.ViewModels
private async Task BrowseForWorkingDirectory()
{
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
var directory = await FileSystemService.BrowseForDirectory().ConfigureAwait(true);
if (directory != null)
{
WorkingDirectory = directory;
@@ -292,43 +388,22 @@ namespace FluentTerminal.App.ViewModels
private async Task CancelEdit()
{
if (_isNew)
if (IsNew)
{
await Delete();
}
else
{
ShellProfile changedProfile = new ShellProfile
{
Arguments = Arguments,
Location = Location,
Name = Name,
WorkingDirectory = WorkingDirectory,
TabThemeId = SelectedTabTheme.Id,
TerminalThemeId = SelectedTerminalTheme.Id,
LineEndingTranslation = _lineEndingStyle,
KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList()
};
ShellProfile changedProfile = await CreateProfileAsync();
if (!_fallbackProfile.Equals(changedProfile))
if (!_fallbackProfile.EqualTo(changedProfile))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDiscardChanges"), 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);
}
// Cancelled, so rollback
InitializeViewModelProperties(_fallbackProfile);
KeyBindings.Editable = false;
InEditMode = false;
@@ -349,7 +424,7 @@ namespace FluentTerminal.App.ViewModels
private async Task Delete()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this theme?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -359,16 +434,7 @@ namespace FluentTerminal.App.ViewModels
private void Edit()
{
// 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
};
_fallbackProfile = Model.Clone();
KeyBindings.Editable = true;
InEditMode = true;
@@ -378,5 +444,7 @@ namespace FluentTerminal.App.ViewModels
{
SetAsDefault?.Invoke(this, EventArgs.Empty);
}
#endregion Methods
}
}
@@ -0,0 +1,277 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public class SshProfileViewModel : ShellProfileViewModel, ISshConnectionInfo
{
#region Fields
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
// To prevent validating the existence of the same file multiple times because it's kinda expensive
private string _validatedIdentityFile;
#endregion Fields
#region Properties
private string _host;
public string Host
{
get => _host;
set => Set(ref _host, value);
}
private ushort _sshPort;
public ushort SshPort
{
get => _sshPort;
set => Set(ref _sshPort, value);
}
private string _username;
public string Username
{
get => _username;
set => Set(ref _username, value);
}
private string _identityFile;
public string IdentityFile
{
get => _identityFile;
set => Set(ref _identityFile, value);
}
private bool _useMosh;
public bool UseMosh
{
get => _useMosh;
set => Set(ref _useMosh, value);
}
private ushort _moshPortFrom;
public ushort MoshPortFrom
{
get => _moshPortFrom;
set => Set(ref _moshPortFrom, value);
}
private ushort _moshPortTo;
public ushort MoshPortTo
{
get => _moshPortTo;
set => Set(ref _moshPortTo, value);
}
#endregion Properties
#region Commands
public IAsyncCommand BrowseForIdentityFileCommand { get; }
#endregion Commands
#region Constructor
public SshProfileViewModel(SshProfile sshProfile, ISettingsService settingsService,
IDialogService dialogService, IFileSystemService fileSystemService, IApplicationView applicationView,
IDefaultValueProvider defaultValueProvider,
ITrayProcessCommunicationService trayProcessCommunicationService, bool isNew) : base(
sshProfile ?? new SshProfile(), settingsService, dialogService, fileSystemService, applicationView,
defaultValueProvider, isNew)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
InitializeViewModelPropertiesPrivate(sshProfile ?? new SshProfile());
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
}
#endregion Constructor
#region Methods
// Fills the remaining view model properties (those which aren't filled by the base method) from the input sshProfile
private void InitializeViewModelPropertiesPrivate(SshProfile sshProfile)
{
Host = sshProfile.Host;
SshPort = sshProfile.SshPort;
Username = sshProfile.Username;
if (string.IsNullOrEmpty(Username))
{
_trayProcessCommunicationService.GetUserName().ContinueWith(t =>
{
var username = t.Result;
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
{
ApplicationView.RunOnDispatcherThread(() => Username = username);
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
}
IdentityFile = sshProfile.IdentityFile;
UseMosh = sshProfile.UseMosh;
MoshPortFrom = sshProfile.MoshPortFrom;
MoshPortTo = sshProfile.MoshPortTo;
}
protected override void InitializeViewModelProperties(ShellProfile sshProfile)
{
base.InitializeViewModelProperties(sshProfile);
InitializeViewModelPropertiesPrivate((SshProfile)sshProfile);
}
protected override async Task FillProfileAsync(ShellProfile profile)
{
await base.FillProfileAsync(profile);
profile.Location = _useMosh ? ShellLocation.Mosh : ShellLocation.SSH;
profile.Arguments = GetArgumentsString();
profile.WorkingDirectory = null;
var sshProfile = (SshProfile)profile;
sshProfile.Host = Host;
sshProfile.SshPort = SshPort;
sshProfile.Username = Username;
sshProfile.IdentityFile = IdentityFile;
sshProfile.UseMosh = UseMosh;
sshProfile.MoshPortFrom = MoshPortFrom;
sshProfile.MoshPortTo = MoshPortTo;
}
public virtual Task AcceptChangesAsync() => FillProfileAsync(Model);
protected override async Task<ShellProfile> CreateProfileAsync()
{
var profile = new SshProfile();
await FillProfileAsync(profile);
return profile;
}
private string GetArgumentsString()
{
StringBuilder sb = new StringBuilder();
if (_sshPort != SshProfile.DefaultSshPort)
sb.Append($"-p {_sshPort:#####} ");
if (!string.IsNullOrEmpty(_identityFile))
sb.Append($"-i \"{_identityFile}\" ");
sb.Append($"{_username}@{_host}");
if (_useMosh)
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
return sb.ToString();
}
public override async Task SaveChangesAsync()
{
var result = await ValidateAsync();
if (result != SshConnectionInfoValidationResult.Valid)
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"),
result.GetErrorString(Environment.NewLine), DialogButton.OK);
return;
}
await AcceptChangesAsync();
SettingsService.SaveSshProfile((SshProfile) Model, IsNew);
KeyBindings.Editable = false;
InEditMode = false;
IsNew = false;
}
private async Task BrowseForIdentityFile()
{
var file = await FileSystemService.OpenFile(new[] { "*" }).ConfigureAwait(true);
if (file != null)
{
IdentityFile = file.Path;
}
}
public async Task<SshConnectionInfoValidationResult> ValidateAsync()
{
var result = this.GetValidationResult();
var identityFile = _identityFile;
if (!string.IsNullOrEmpty(identityFile) && !string.Equals(identityFile, _validatedIdentityFile,
StringComparison.OrdinalIgnoreCase))
{
// Here we need to take into account that files from ssh config dir can be provided by name, without full path.
string fullPath;
if (identityFile.Contains(Path.PathSeparator))
{
fullPath = identityFile;
}
else
{
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
if (string.IsNullOrEmpty(sshConfigDir))
{
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
return result;
}
fullPath = Path.Combine(sshConfigDir, identityFile);
}
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
{
_validatedIdentityFile = identityFile;
IdentityFile = fullPath;
}
else
{
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
}
}
return result;
}
public void SetValidatedIdentityFile(string identityFile)
{
_validatedIdentityFile = identityFile;
IdentityFile = identityFile;
}
#endregion Methods
}
}
@@ -1,4 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
@@ -7,26 +8,35 @@ using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public class TerminalViewModel : ViewModelBase
{
#region Static
private static readonly Regex SshTitleRx = new Regex(@"^\w+@\S+:", RegexOptions.Compiled);
#endregion Static
private readonly IKeyboardCommandService _keyboardCommandService;
private readonly IDispatcherTimer _resizeOverlayTimer;
private bool _isSelected;
private bool _newOutput;
private string _resizeOverlayContent;
private bool _hasNewOutput;
private bool _hasExitedWithError;
private string _searchText;
private bool _showResizeOverlay;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private TerminalTheme _terminalTheme;
private string _title;
private TerminalOptions _terminalOptions;
private string _tabTitle;
private string _shellTitle;
private bool _hasCustomTitle;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, string startupDirectory, ShellProfile shellProfile,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
{
SettingsService = settingsService;
@@ -35,12 +45,13 @@ namespace FluentTerminal.App.ViewModels
SettingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged += OnKeyBindingsChanged;
_terminalOptions = SettingsService.GetTerminalOptions();
TrayProcessCommunicationService = trayProcessCommunicationService;
DialogService = dialogService;
_keyboardCommandService = keyboardCommandService;
ApplicationSettings = applicationSettings;
StartupDirectory = startupDirectory;
ApplicationView = applicationView;
ClipboardService = clipboardService;
@@ -50,10 +61,6 @@ namespace FluentTerminal.App.ViewModels
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(FindNext);
FindPreviousCommand = new RelayCommand(FindPrevious);
@@ -61,17 +68,16 @@ namespace FluentTerminal.App.ViewModels
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
EditTitleCommand = new AsyncCommand(EditTitle);
if (!string.IsNullOrWhiteSpace(StartupDirectory))
{
ShellProfile.WorkingDirectory = StartupDirectory;
}
Terminal = new Terminal(TrayProcessCommunicationService);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
Terminal.OutputReceived += Terminal_OutputReceived;
Terminal.SizeChanged += Terminal_SizeChanged;
Terminal.TitleChanged += Terminal_TitleChanged;
Terminal.Exited += Terminal_Exited;
Terminal.Closed += Terminal_Closed;
Overlay = new OverlayViewModel(dispatcherTimer);
}
public event EventHandler Activated;
@@ -82,6 +88,8 @@ namespace FluentTerminal.App.ViewModels
public event EventHandler<TerminalOptions> OptionsChanged;
public event EventHandler SearchStarted;
public event EventHandler<TerminalTheme> ThemeChanged;
public event EventHandler<string> ShellTitleChanged;
public event EventHandler<string> CustomTitleChanged;
public ApplicationSettings ApplicationSettings { get; private set; }
@@ -119,8 +127,7 @@ namespace FluentTerminal.App.ViewModels
{
if (IsSelected)
{
ApplicationView.Title = Title ?? string.Empty;
NewOutput = false;
HasNewOutput = false;
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
@@ -131,16 +138,22 @@ namespace FluentTerminal.App.ViewModels
public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) ||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null);
public bool NewOutput
public bool HasNewOutput
{
get => _newOutput;
set => Set(ref _newOutput, value);
get => _hasNewOutput;
set => Set(ref _hasNewOutput, value);
}
public string ResizeOverlayContent
public bool HasExitedWithError
{
get => _resizeOverlayContent;
set => Set(ref _resizeOverlayContent, value);
get => _hasExitedWithError;
set
{
if (Set(ref _hasExitedWithError, value) && value)
{
HasNewOutput = false;
}
}
}
public string SearchText
@@ -155,31 +168,12 @@ namespace FluentTerminal.App.ViewModels
public ShellProfile ShellProfile { get; }
public bool ShowResizeOverlay
{
get => _showResizeOverlay;
set
{
Set(ref _showResizeOverlay, value);
if (value)
{
if (_resizeOverlayTimer.IsEnabled)
{
_resizeOverlayTimer.Stop();
}
_resizeOverlayTimer.Start();
}
}
}
public bool ShowSearchPanel
{
get => _showSearchPanel;
set => Set(ref _showSearchPanel, value);
}
public string StartupDirectory { get; }
public TabTheme TabTheme
{
get => _tabTheme;
@@ -195,20 +189,55 @@ namespace FluentTerminal.App.ViewModels
public Terminal Terminal { get; private set; }
public OverlayViewModel Overlay { get; private set; }
public TerminalTheme TerminalTheme
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
}
public string Title
public string TabTitle
{
get => _title;
get => _tabTitle;
set
{
if (Set(ref _title, value) && IsSelected)
var title = value?.Trim() ?? string.Empty;
if (ShellProfile is SshProfile)
{
ApplicationView.Title = Title;
// For SshProfile we are adjusting title.
if (title.Equals("ssh", StringComparison.OrdinalIgnoreCase) ||
title.EndsWith("\\ssh.exe", StringComparison.OrdinalIgnoreCase))
{
title = $"[ssh] {I18N.Translate("Authenticate")}";
}
else if (title.Equals("mosh", StringComparison.OrdinalIgnoreCase) ||
title.EndsWith("\\mosh.exe", StringComparison.OrdinalIgnoreCase))
{
title = $"[mosh] {I18N.Translate("Authenticate")}";
}
else if (SshTitleRx.IsMatch(title))
{
title = $"[ssh] {title}";
}
}
if (Set(ref _tabTitle, title))
{
CustomTitleChanged?.Invoke(this, title);
}
}
}
public string ShellTitle
{
get => _shellTitle;
set
{
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
{
TabTitle = value;
}
}
}
@@ -217,6 +246,10 @@ namespace FluentTerminal.App.ViewModels
public Task Close()
{
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
return Terminal.Close();
}
@@ -227,10 +260,19 @@ namespace FluentTerminal.App.ViewModels
public async Task EditTitle()
{
var result = await DialogService.ShowInputDialogAsync("Edit Title");
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"));
if (result != null)
{
Title = result;
if (string.IsNullOrWhiteSpace(result))
{
_hasCustomTitle = false;
TabTitle = ShellTitle;
}
else
{
_hasCustomTitle = true;
TabTitle = result;
}
}
}
@@ -280,30 +322,30 @@ namespace FluentTerminal.App.ViewModels
});
}
private void OnKeyBindingsChanged(object sender, EventArgs e)
private async void OnKeyBindingsChanged(object sender, EventArgs e)
{
KeyBindingsChanged?.Invoke(this, EventArgs.Empty);
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
}
private void OnResizeOverlayTimerFinished(object sender, object e)
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
_resizeOverlayTimer.Stop();
ShowResizeOverlay = false;
_terminalOptions = e;
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
}
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
private void SelectTabTheme(string id)
{
OptionsChanged?.Invoke(this, e);
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
}
private void SelectTabTheme(string name)
private void Terminal_Exited(object sender, int exitCode)
{
TabTheme = TabThemes.FirstOrDefault(t => t.Name == name);
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
}
private void Terminal_Closed(object sender, EventArgs e)
{
Closed?.Invoke(this, EventArgs.Empty);
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
@@ -314,6 +356,10 @@ namespace FluentTerminal.App.ViewModels
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
ClipboardService.SetText(selection);
if(_terminalOptions.ShowTextCopied)
{
Overlay.Show(I18N.Translate("TextCopied"));
}
break;
}
case nameof(Command.Paste):
@@ -322,7 +368,7 @@ namespace FluentTerminal.App.ViewModels
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(content).ConfigureAwait(true);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
}
@@ -332,7 +378,7 @@ namespace FluentTerminal.App.ViewModels
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await Terminal.Write(content).ConfigureAwait(true);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
}
@@ -353,30 +399,30 @@ namespace FluentTerminal.App.ViewModels
}
}
private void Terminal_OutputReceived(object sender, string e)
private void Terminal_OutputReceived(object sender, byte[] e)
{
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
NewOutput = true;
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
}
}
private void Terminal_SizeChanged(object sender, TerminalSize e)
{
ResizeOverlayContent = $"{e.Columns} x {e.Rows}";
ShowResizeOverlay = true;
Overlay.Show($"{e.Columns} x {e.Rows}");
}
private void Terminal_TitleChanged(object sender, string e)
{
Title = e;
ShellTitle = e;
ShellTitleChanged?.Invoke(this, e);
}
private async Task TryClose()
{
if (ApplicationSettings.ConfirmClosingTabs)
{
var result = await DialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to close this tab?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmCloseTab"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.Cancel)
{
@@ -344,7 +344,7 @@ namespace FluentTerminal.App.ViewModels
if (!_fallbackTheme.Equals(changedTheme))
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to discard all changes?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -392,7 +392,7 @@ namespace FluentTerminal.App.ViewModels
private async Task Delete()
{
var result = await _dialogService.ShowMessageDialogAsnyc("Please confirm", "Are you sure you want to delete this theme?", DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteTheme"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Timer = System.Timers.Timer;
namespace FluentTerminal.App.ViewModels.Utilities
{
public class InputBuffer
{
private readonly Timer _timer = new Timer();
private readonly Func<string, Task> _writeCallback;
private readonly List<string> _buffer = new List<string>();
private readonly object _lock = new object();
public InputBuffer(Func<string, Task> writeCallback)
{
_timer.AutoReset = true;
_timer.Elapsed += _timer_Elapsed;
_writeCallback = writeCallback;
_timer.Interval = 5;
_timer.Start();
}
private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
lock (_lock)
{
var data = string.Concat(_buffer);
_buffer.Clear();
_writeCallback.Invoke(data);
}
}
public void Write(string data)
{
lock (_lock)
{
_buffer.Add(data);
}
}
}
}
@@ -12,7 +12,7 @@ namespace FluentTerminal.App.Adapters
{
private readonly AppServiceConnection _appServiceConnection;
public event EventHandler<IDictionary<string, string>> MessageReceived;
public event EventHandler<ValueSet> MessageReceived;
public AppServiceConnectionAdapter(AppServiceConnection appServiceConnection)
{
@@ -22,35 +22,16 @@ namespace FluentTerminal.App.Adapters
private void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
var messageType = (string)args.Request.Message[MessageKeys.Type];
var messageContent = (string)args.Request.Message[MessageKeys.Content];
MessageReceived?.Invoke(this, new Dictionary<string, string>
{
[MessageKeys.Type] = messageType,
[MessageKeys.Content] = messageContent
});
MessageReceived?.Invoke(this, args.Request.Message);
}
public async Task<IDictionary<string, string>> SendMessageAsync(IDictionary<string, string> message)
public async Task<ValueSet> SendMessageAsync(ValueSet message)
{
var valueSet = new ValueSet();
foreach (var pair in message)
{
valueSet.Add(pair.Key, pair.Value);
}
var response = await _appServiceConnection.SendMessageAsync(valueSet).AsTask();
var response = await _appServiceConnection.SendMessageAsync(message).AsTask();
if (response.Status == AppServiceResponseStatus.Success)
{
var responseDict = new Dictionary<string, string>();
foreach (var pair in response.Message)
{
responseDict.Add(pair.Key, (string)pair.Value);
}
return responseDict;
return response.Message;
}
return null;
}
@@ -14,20 +14,32 @@ namespace FluentTerminal.App.Adapters
{
private readonly ApplicationView _applicationView;
private readonly CoreDispatcher _dispatcher;
private bool _closed;
public event CloseRequestedHandler CloseRequested;
public event EventHandler Closed;
public ApplicationViewAdapter()
{
_applicationView = ApplicationView.GetForCurrentView();
_applicationView.Consolidated += _applicationView_Consolidated;
_dispatcher = CoreApplication.GetCurrentView().Dispatcher;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequest;
Logger.Instance.Debug("Created ApplicationViewAdapter for ApplicationView with Id: {Id}", _applicationView.Id);
}
private void _applicationView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
Closed?.Invoke(this, EventArgs.Empty);
}
public int Id => _applicationView.Id;
public string Title
{
get => _applicationView.Title;
set => _applicationView.Title = value;
set => _applicationView.Title = value ?? string.Empty;
}
public bool IsApiContractPresent(string api, ushort version)
@@ -40,9 +52,16 @@ namespace FluentTerminal.App.Adapters
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
}
public Task<bool> TryClose()
public async Task<bool> TryClose()
{
return _applicationView.TryConsolidateAsync().AsTask();
if (_closed)
{
Logger.Instance.Debug("ApplicationViewAdapter.TryClose was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
return true;
}
Logger.Instance.Debug("TryClose ApplicationView with Id: {Id}", _applicationView.Id);
_closed = await _applicationView.TryConsolidateAsync().AsTask();
return _closed;
}
public bool ToggleFullScreen()
@@ -63,6 +82,7 @@ namespace FluentTerminal.App.Adapters
var deferral = e.GetDeferral();
var args = new CancelableEventArgs();
if (CloseRequested != null)
{
await CloseRequested.Invoke(this, args);
@@ -70,6 +90,11 @@ namespace FluentTerminal.App.Adapters
e.Handled = args.Cancelled;
if (!e.Handled)
{
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
}
deferral.Complete();
}
}
@@ -1,5 +1,6 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.Utilities;
using System;
using System.Threading.Tasks;
using Windows.UI.Popups;
@@ -29,14 +30,19 @@ namespace FluentTerminal.App.Adapters
public void AddButton(DialogButton button)
{
_messageDialog.Commands.Add(new UICommand(button.ToString()));
var command = new UICommand
{
Label = I18N.Translate(button.ToString()),
Id = button
};
_messageDialog.Commands.Add(command);
}
public async Task<DialogButton> ShowAsync()
{
var result = await _messageDialog.ShowAsync();
return Enum.Parse<DialogButton>(result.Label);
return (DialogButton)result.Id;
}
}
}
+2 -1
View File
@@ -37,7 +37,8 @@
<converters:EnumValueToVisibiltyConverter x:Key="EnumValueToVisibiltyConverter" />
<converters:TabThemeSelectedConverter x:Key="TabThemeSelectedConverter" />
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
<converters:EnumToDescriptionConverter x:Key="EnumToDescriptionConverter" />
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
<converters:I18NConverter x:Key="I18NConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
Regular → Executable
+667 -327
View File
@@ -1,327 +1,667 @@
using Autofac;
using FluentTerminal.App.Adapters;
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;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.ApplicationModel.Core;
using Windows.Storage;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using IContainer = Autofac.IContainer;
namespace FluentTerminal.App
{
public sealed partial class App : Application
{
public TaskCompletionSource<int> _trayReady = new TaskCompletionSource<int>();
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private bool _alreadyLaunched;
private ApplicationSettings _applicationSettings;
private readonly IContainer _container;
private readonly List<MainViewModel> _mainViewModels;
private SettingsViewModel _settingsViewModel;
private int? _settingsWindowId;
private IAppServiceConnection _appServiceConnection;
private BackgroundTaskDeferral _appServiceDeferral;
public App()
{
_mainViewModels = new List<MainViewModel>();
InitializeComponent();
UnhandledException += OnUnhandledException;
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
RoamingSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings),
KeyBindings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.KeyBindingsContainerName, ApplicationDataCreateDisposition.Always)),
ShellProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings.CreateContainer(Constants.ShellProfilesContainerName, ApplicationDataCreateDisposition.Always)),
Themes = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ThemesContainerName, ApplicationDataCreateDisposition.Always))
};
var builder = new ContainerBuilder();
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
builder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>().SingleInstance();
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();
builder.RegisterType<ITermThemeParser>().As<IThemeParser>().SingleInstance();
builder.RegisterType<FluentTerminalThemeParser>().As<IThemeParser>().SingleInstance();
builder.RegisterType<ClipboardService>().As<IClipboardService>().SingleInstance();
builder.RegisterType<FileSystemService>().As<IFileSystemService>().SingleInstance();
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();
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
builder.RegisterInstance(applicationDataContainers);
_container = builder.Build();
_settingsService = _container.Resolve<ISettingsService>();
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_trayProcessCommunicationService = _container.Resolve<ITrayProcessCommunicationService>();
_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)
{
if (string.IsNullOrWhiteSpace(commandLineActivated.Operation.Arguments))
{
return;
}
var arguments = commandLineActivated.Operation.Arguments.Split(' ', 2);
var command = arguments[0];
var parameter = arguments.Length > 1 ? arguments[1] : string.Empty;
if (_alreadyLaunched)
{
if (command == "settings")
{
await ShowSettings().ConfigureAwait(true);
}
else if (command == "new")
{
if (_applicationSettings.NewTerminalLocation == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
{
await _mainViewModels.Last().AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
}
else
{
await CreateNewTerminalWindow(parameter, false).ConfigureAwait(true);
}
}
}
else
{
if (command == "settings")
{
var viewModel = _container.Resolve<SettingsViewModel>();
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
}
else if (command == "new")
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminal(parameter, false, Guid.Empty).ConfigureAwait(true);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
}
}
}
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<MainViewModel>();
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, false, string.Empty).ConfigureAwait(true);
}
}
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails details)
{
if (details.CallerPackageFamilyName == Package.Current.Id.FamilyName)
{
_appServiceDeferral = args.TaskInstance.GetDeferral();
args.TaskInstance.Canceled += OnTaskCanceled;
_appServiceConnection = new AppServiceConnectionAdapter(details.AppServiceConnection);
_trayReady.SetResult(0);
}
}
}
private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
_appServiceDeferral?.Complete();
_appServiceDeferral = null;
_appServiceConnection = null;
Application.Current.Exit();
}
private async Task CreateMainView(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
{
await StartSystemTray().ConfigureAwait(true);
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
rootFrame = new Frame();
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
if (viewModel is MainViewModel mainViewModel)
{
mainViewModel.Closed += OnMainViewModelClosed;
mainViewModel.NewWindowRequested += OnNewWindowRequested;
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(mainViewModel);
}
rootFrame.Navigate(pageType, viewModel);
}
_alreadyLaunched = true;
Window.Current.Activate();
}
private async Task CreateNewTerminalWindow(string startupDirectory, bool showProfileSelection)
{
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, bool showProfileSelection, object parameter)
{
int windowId = 0;
TViewModel viewModel = default;
await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
viewModel = _container.Resolve<TViewModel>();
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = ExtendViewIntoTitleBar;
var frame = new Frame();
frame.Navigate(pageType, viewModel);
Window.Current.Content = frame;
Window.Current.Activate();
windowId = ApplicationView.GetForCurrentView().Id;
});
if (viewModel is MainViewModel mainViewModel && parameter is string directory)
{
mainViewModel.Closed += OnMainViewModelClosed;
mainViewModel.NewWindowRequested += OnNewWindowRequested;
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
_mainViewModels.Add(mainViewModel);
await mainViewModel.AddTerminal(directory, showProfileSelection, Guid.Empty).ConfigureAwait(true);
}
if (viewModel is SettingsViewModel settingsViewModel)
{
_settingsViewModel = settingsViewModel;
_settingsViewModel.Closed += OnSettingsClosed;
}
return windowId;
}
private void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
_applicationSettings = e;
}
private void OnMainViewModelClosed(object sender, EventArgs e)
{
if (sender is MainViewModel viewModel)
{
viewModel.Closed -= OnMainViewModelClosed;
viewModel.NewWindowRequested -= OnNewWindowRequested;
viewModel.ShowSettingsRequested -= OnShowSettingsRequested;
_mainViewModels.Remove(viewModel);
}
}
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
await CreateNewTerminalWindow(string.Empty, e.ShowProfileSelection).ConfigureAwait(true);
}
private void OnSettingsClosed(object sender, EventArgs e)
{
_settingsViewModel.Closed -= OnSettingsClosed;
_settingsViewModel = null;
_settingsWindowId = null;
}
private void OnShowAboutRequested(object sender, EventArgs e)
{
ShowAbout().ConfigureAwait(true);
}
private async void OnShowSettingsRequested(object sender, EventArgs e)
{
await ShowSettings().ConfigureAwait(true);
}
private async Task ShowAbout()
{
await ShowSettings().ConfigureAwait(true);
_settingsViewModel.NavigateToAboutPage();
}
private async Task ShowSettings()
{
if (_settingsViewModel == null)
{
_settingsWindowId = await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true, false, null).ConfigureAwait(true);
}
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
}
private async Task StartSystemTray()
{
var launch = FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("AppLaunchedParameterGroup").AsTask();
var clearCache = WebView.ClearTemporaryWebDataAsync().AsTask();
await Task.WhenAll(launch, clearCache, _trayReady.Task).ConfigureAwait(true);
_trayProcessCommunicationService.Initialize(_appServiceConnection);
}
}
}
using Autofac;
using CommandLine;
using FluentTerminal.App.Adapters;
using FluentTerminal.App.CommandLineArguments;
using FluentTerminal.App.Dialogs;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Adapters;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.EventArgs;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.App.ViewModels;
using FluentTerminal.App.Views;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.ApplicationModel.Core;
using Windows.Storage;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using FluentTerminal.App.Utilities;
using IContainer = Autofac.IContainer;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App
{
// ReSharper disable once RedundantExtendsListEntry
public sealed partial class App : Application
{
private readonly TaskCompletionSource<int> _trayReady = new TaskCompletionSource<int>();
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly Lazy<ISshHelperService> _sshHelperService;
private readonly IDialogService _dialogService;
private bool _alreadyLaunched;
private bool _isLaunching;
private ApplicationSettings _applicationSettings;
private readonly IContainer _container;
private readonly List<MainViewModel> _mainViewModels;
private SettingsViewModel _settingsViewModel;
private int? _settingsWindowId;
private IAppServiceConnection _appServiceConnection;
private BackgroundTaskDeferral _appServiceDeferral;
private Parser _commandLineParser;
private int? _activeWindowId;
public App()
{
_mainViewModels = new List<MainViewModel>();
InitializeComponent();
UnhandledException += OnUnhandledException;
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
RoamingSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings),
KeyBindings = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.KeyBindingsContainerName, ApplicationDataCreateDisposition.Always)),
ShellProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings.CreateContainer(Constants.ShellProfilesContainerName, ApplicationDataCreateDisposition.Always)),
Themes = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ThemesContainerName, ApplicationDataCreateDisposition.Always)),
SshProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.SshProfilesContainerName, ApplicationDataCreateDisposition.Always))
};
var builder = new ContainerBuilder();
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
builder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>().SingleInstance();
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();
builder.RegisterType<ITermThemeParser>().As<IThemeParser>().SingleInstance();
builder.RegisterType<FluentTerminalThemeParser>().As<IThemeParser>().SingleInstance();
builder.RegisterType<ClipboardService>().As<IClipboardService>().SingleInstance();
builder.RegisterType<FileSystemService>().As<IFileSystemService>().SingleInstance();
builder.RegisterType<SystemFontService>().As<ISystemFontService>().SingleInstance();
builder.RegisterType<ShellProfileSelectionDialog>().As<IShellProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<SshProfileSelectionDialog>().As<ISshProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<CreateKeyBindingDialog>().As<ICreateKeyBindingDialog>().InstancePerDependency();
builder.RegisterType<InputDialog>().As<IInputDialog>().InstancePerDependency();
builder.RegisterType<MessageDialogAdapter>().As<IMessageDialog>().InstancePerDependency();
builder.RegisterType<SshInfoDialog>().As<ISshConnectionInfoDialog>().InstancePerDependency();
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
builder.RegisterType<SshHelperService>().As<ISshHelperService>().SingleInstance();
builder.RegisterType<ApplicationLanguageService>().As<IApplicationLanguageService>().SingleInstance();
builder.RegisterInstance(applicationDataContainers);
_container = builder.Build();
_settingsService = _container.Resolve<ISettingsService>();
_settingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
_trayProcessCommunicationService = _container.Resolve<ITrayProcessCommunicationService>();
_sshHelperService = new Lazy<ISshHelperService>(() => _container.Resolve<ISshHelperService>());
_dialogService = _container.Resolve<IDialogService>();
_applicationSettings = _settingsService.GetApplicationSettings();
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
settings.Converters.Add(new StringEnumConverter(typeof(CamelCaseNamingStrategy)));
return settings;
};
_commandLineParser = new Parser(settings =>
{
settings.CaseSensitive = false;
settings.CaseInsensitiveEnumValues = true;
});
}
private void OnUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.Instance.Error(e.Exception, "Unhandled Exception");
}
private static IEnumerable<string> SplitArguments(string arguments)
{
var chars = arguments.ToCharArray();
var inQuote = false;
for (var i = 0; i < chars.Length; i++)
{
if (chars[i] == '"')
{
inQuote = !inQuote;
}
if (!inQuote && chars[i] == ' ')
{
chars[i] = '\n';
}
}
foreach (var value in new string(chars).Split('\n'))
{
yield return value.Trim('"');
}
}
protected override async void OnActivated(IActivatedEventArgs args)
{
if (args is ProtocolActivatedEventArgs protocolActivated)
{
MainViewModel mainViewModel = null;
if (!_alreadyLaunched)
{
// App wasn't launched before double clicking a shortcut, so we have to create a window
// in order to be able to communicate with user.
mainViewModel = _container.Resolve<MainViewModel>();
await CreateMainView(typeof(MainPage), mainViewModel, true);
}
bool isSsh;
try
{
isSsh = _sshHelperService.Value.IsSsh(protocolActivated.Uri);
}
catch (Exception ex)
{
await new MessageDialog($"Invalid link: {ex.Message}", "Invalid Link").ShowAsync();
mainViewModel?.ApplicationView.TryClose();
return;
}
if (isSsh)
{
SshProfileViewModel connectionInfo;
try
{
connectionInfo = (SshProfileViewModel) _sshHelperService.Value.ParseSsh(protocolActivated.Uri);
}
catch (Exception ex)
{
await new MessageDialog($"Invalid link: {ex.Message}", "Invalid Link").ShowAsync();
mainViewModel?.ApplicationView.TryClose();
return;
}
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks &&
string.IsNullOrEmpty(connectionInfo.Username))
{
connectionInfo.Username = await _trayProcessCommunicationService.GetUserName();
}
SshConnectionInfoValidationResult result = await connectionInfo.ValidateAsync();
if (result == SshConnectionInfoValidationResult.Valid)
{
await connectionInfo.AcceptChangesAsync();
}
else
{
// Link is valid, but incomplete (i.e. username missing), so we need to show dialog.
connectionInfo =
(SshProfileViewModel) await _dialogService.ShowSshConnectionInfoDialogAsync(connectionInfo);
if (connectionInfo == null)
{
// User clicked "Cancel" in the dialog.
mainViewModel?.ApplicationView.TryClose();
return;
}
}
if (mainViewModel == null)
await CreateTerminal(connectionInfo.Model, _applicationSettings.NewTerminalLocation);
else
await mainViewModel.AddTerminalAsync(connectionInfo.Model);
return;
}
await new MessageDialog($"Invalid link: {protocolActivated.Uri}", "Invalid Link").ShowAsync();
mainViewModel?.ApplicationView.TryClose();
return;
}
if (args is CommandLineActivatedEventArgs commandLineActivated)
{
if (string.IsNullOrWhiteSpace(commandLineActivated.Operation.Arguments))
{
return;
}
_commandLineParser.ParseArguments(SplitArguments(commandLineActivated.Operation.Arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
{
if (verb is SettingsVerb settingsVerb)
{
if (!settingsVerb.Import && !settingsVerb.Export)
{
await ShowSettings().ConfigureAwait(true);
}
else if (settingsVerb.Export)
{
var exportFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var settings = _settingsService.ExportSettings();
await FileIO.WriteTextAsync(exportFile, settings);
await new MessageDialog($"{I18N.Translate("SettingsExported")} {exportFile.Path}").ShowAsync();
}
else if (settingsVerb.Import)
{
var file = await ApplicationData.Current.LocalFolder.GetFileAsync("config.json");
var content = await FileIO.ReadTextAsync(file);
_settingsService.ImportSettings(content);
await new MessageDialog($"{I18N.Translate("SettingsImported")} {file.Path}").ShowAsync();
}
}
else if (verb is NewVerb newVerb)
{
var profile = default(ShellProfile);
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
{
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
}
if (profile == null)
{
profile = _settingsService.GetDefaultShellProfile();
}
if (!string.IsNullOrWhiteSpace(newVerb.Directory))
{
profile.WorkingDirectory = newVerb.Directory;
}
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
else if (verb is RunVerb runVerb)
{
var profile = new ShellProfile
{
Id = Guid.Empty,
Location = null,
Arguments = runVerb.Command,
WorkingDirectory = runVerb.Directory
};
if (!string.IsNullOrWhiteSpace(runVerb.Theme))
{
var theme = _settingsService.GetThemes().FirstOrDefault(x => x.Name.Equals(runVerb.Theme, StringComparison.CurrentCultureIgnoreCase));
if (theme != null)
{
profile.TerminalThemeId = theme.Id;
}
}
if (string.IsNullOrWhiteSpace(profile.WorkingDirectory))
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = runVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: runVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
});
}
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
if (_isLaunching)
{
return;
}
_isLaunching = true;
if (!_alreadyLaunched)
{
await InitializeLogger();
// TODO: Check the reason for such strange using of tasks!
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
var viewModel = _container.Resolve<MainViewModel>();
if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
await viewModel.AddLocalTabOrWindowAsync(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
}
else
{
await viewModel.AddLocalTabAsync();
}
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
Window.Current.Activate();
}
else if (_mainViewModels.Count == 0)
{
await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
}
else if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
var location = _applicationSettings.NewTerminalLocation;
var profile = _settingsService.GetShellProfile(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
await CreateTerminal(profile, location).ConfigureAwait(true);
}
_isLaunching = false;
}
private static async Task InitializeLogger()
{
var logDirectory = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("Logs", CreationCollisionOption.OpenIfExists);
var logFile = Path.Combine(logDirectory.Path, "fluentterminal.app.log");
var configFile = await logDirectory.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var configContent = await FileIO.ReadTextAsync(configFile);
if (string.IsNullOrWhiteSpace(configContent))
{
configContent = JsonConvert.SerializeObject(new Logger.Configuration());
await FileIO.WriteTextAsync(configFile, configContent);
}
var config = JsonConvert.DeserializeObject<Logger.Configuration>(configContent) ?? new Logger.Configuration();
Logger.Instance.Initialize(logFile, config);
}
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails details)
{
if (details.CallerPackageFamilyName == Package.Current.Id.FamilyName)
{
_appServiceDeferral = args.TaskInstance.GetDeferral();
args.TaskInstance.Canceled += OnTaskCanceled;
_appServiceConnection = new AppServiceConnectionAdapter(details.AppServiceConnection);
_trayReady.SetResult(0);
}
}
}
private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
_appServiceDeferral?.Complete();
_appServiceDeferral = null;
_appServiceConnection = null;
// ReSharper disable once ArrangeStaticMemberQualifier
Application.Current.Exit();
}
private async Task CreateMainView(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
{
await StartSystemTray().ConfigureAwait(true);
if (!(Window.Current.Content is Frame rootFrame))
{
rootFrame = new Frame();
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
if (viewModel is MainViewModel mainViewModel)
{
mainViewModel.Closed += OnMainViewModelClosed;
mainViewModel.NewWindowRequested += OnNewWindowRequested;
mainViewModel.ShowSettingsRequested += OnShowSettingsRequested;
mainViewModel.ShowAboutRequested += OnShowAboutRequested;
mainViewModel.ActivatedMv += OnMainViewActivated;
_mainViewModels.Add(mainViewModel);
}
rootFrame.Navigate(pageType, viewModel);
}
_alreadyLaunched = true;
Window.Current.Activate();
}
private async Task<MainViewModel> CreateNewTerminalWindow()
{
var viewModel = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
viewModel.Closed += OnMainViewModelClosed;
viewModel.NewWindowRequested += OnNewWindowRequested;
viewModel.ShowSettingsRequested += OnShowSettingsRequested;
viewModel.ShowAboutRequested += OnShowAboutRequested;
viewModel.ActivatedMv += OnMainViewActivated;
_mainViewModels.Add(viewModel);
return viewModel;
}
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool extendViewIntoTitleBar)
{
int windowId = 0;
TViewModel viewModel = default;
await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
viewModel = _container.Resolve<TViewModel>();
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
var frame = new Frame();
frame.Navigate(pageType, viewModel);
Window.Current.Content = frame;
Window.Current.Activate();
windowId = ApplicationView.GetForCurrentView().Id;
});
if (viewModel is SettingsViewModel settingsViewModel)
{
_settingsViewModel = settingsViewModel;
_settingsViewModel.Closed += OnSettingsClosed;
_settingsWindowId = windowId;
}
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(windowId);
return viewModel;
}
private void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
{
_applicationSettings = e;
}
private void OnMainViewModelClosed(object sender, EventArgs e)
{
if (sender is MainViewModel viewModel)
{
Logger.Instance.Debug("MainViewModel with ApplicationView Id: {@id} closed.", viewModel.ApplicationView.Id);
viewModel.Closed -= OnMainViewModelClosed;
viewModel.NewWindowRequested -= OnNewWindowRequested;
viewModel.ShowSettingsRequested -= OnShowSettingsRequested;
viewModel.ShowAboutRequested -= OnShowAboutRequested;
viewModel.ActivatedMv -= OnMainViewActivated;
if (_activeWindowId == viewModel.ApplicationView.Id)
{
_activeWindowId = 0;
}
_mainViewModels.Remove(viewModel);
}
}
private void OnMainViewActivated(object sender, EventArgs e)
{
if (sender is MainViewModel viewModel)
{
Logger.Instance.Debug("MainViewModel with ApplicationView Id: {@id} activated.", viewModel.ApplicationView.Id);
_activeWindowId = viewModel.ApplicationView.Id;
}
}
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
ShellProfile profile = null;
switch (e.Action)
{
case NewWindowAction.StartDefaultLocalTerminal:
break;
case NewWindowAction.ShowProfileSelection:
profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshProfileSelection:
profile = await _sshHelperService.Value.GetSavedSshProfileAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshInfoDialog:
profile = await _sshHelperService.Value.GetSshProfileAsync(new SshProfile());
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.StartLocalTerminal:
profile = _settingsService.GetShellProfile(e.ProfileId);
break;
case NewWindowAction.StartSshTerminal:
profile = _settingsService.GetSshProfile(e.ProfileId);
break;
default:
throw new NotImplementedException("this ProfileSelection not implemented yet");
}
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
if (profile == null)
{
await viewModel.AddLocalTabAsync();
}
else
{
await viewModel.AddTerminalAsync(profile);
}
}
private void OnSettingsClosed(object sender, EventArgs e)
{
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
_settingsViewModel.Closed -= OnSettingsClosed;
_settingsViewModel = null;
_settingsWindowId = null;
}
private void OnShowAboutRequested(object sender, EventArgs e)
{
ShowAbout().ConfigureAwait(true);
}
private async void OnShowSettingsRequested(object sender, EventArgs e)
{
await ShowSettings().ConfigureAwait(true);
}
private async Task ShowAbout()
{
await ShowSettings().ConfigureAwait(true);
_settingsViewModel.NavigateToAboutPage();
}
private async Task CreateTerminal(ShellProfile profile, NewTerminalLocation location)
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminalAsync(profile);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
}
else if (location == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
{
MainViewModel item = _mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId);
if (item != null)
{
await item.AddTerminalAsync(profile);
}
else
{
await _mainViewModels.Last().AddTerminalAsync(profile);
}
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await viewModel.AddTerminalAsync(profile);
}
}
private async Task ShowSettings()
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<SettingsViewModel>();
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
}
else if (_settingsViewModel == null)
{
await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true).ConfigureAwait(true);
}
else
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
}
}
private async Task StartSystemTray()
{
var launch = FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("AppLaunchedParameterGroup").AsTask();
await Task.WhenAll(launch, _trayReady.Task).ConfigureAwait(true);
_trayProcessCommunicationService.Initialize(_appServiceConnection);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 B

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 B

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

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