Compare commits

...
261 Commits
Author SHA1 Message Date
Felix Seidl 68208ff0ec increase version 2020-08-07 14:56:35 +02:00
DennisandEric Cornelissen 2d7d1810a9 Update Dutch translations (#822)
* Update Dutch translations

Add new translations.
Fix typo's.

* Update FluentTerminal.App/Strings/nl/Resources.resw

Fix typo

Co-authored-by: Eric Cornelissen <ericornelissen@gmail.com>

* Update FluentTerminal.App/Strings/nl/Resources.resw

Fix typo

Co-authored-by: Eric Cornelissen <ericornelissen@gmail.com>

* Update FluentTerminal.App/Strings/nl/Resources.resw

Cut English text from Dutch translation

Co-authored-by: Eric Cornelissen <ericornelissen@gmail.com>

* Update FluentTerminal.App/Strings/nl/Resources.resw

Fix typo

Co-authored-by: Eric Cornelissen <ericornelissen@gmail.com>

* Update FluentTerminal.App/Strings/nl/Resources.resw

Fix typo

Co-authored-by: Eric Cornelissen <ericornelissen@gmail.com>

* Update Resources.resw

Fix typo

* Fix typo

Co-authored-by: Eric Cornelissen <ericornelissen@gmail.com>

Co-authored-by: Eric Cornelissen <ericornelissen@gmail.com>
2020-08-06 14:53:21 +02:00
dependabot[bot] cbc90cc94c Bump elliptic from 6.5.2 to 6.5.3 in /FluentTerminal.Client (#825)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-01 23:01:17 +02:00
Felix Seidl 0d48fac129 Add missing store constant for x64
fixes #768
2020-07-21 22:21:45 +02:00
Felix Seidl 099f7c701d Add Esperanto and Uyghur localizations 2020-07-21 22:08:17 +02:00
RhavoXandFelix Seidl 2057c6d189 Fixed almost all warnings in the project. (#820)
* Fixed almost all warnings in the project. Only lanuage warning remains for the bundle project due to weird AZ language detection issues.

* changed binding mode to onetime to avoid binding warnings

Co-authored-by: Felix Seidl <f.seidl@me.com>
2020-07-20 21:55:44 +02:00
RhavoX 56ed197b02 Fix for a crash in search popup. (#817)
The crash occured because the popup is treated like the mainwindow and the main window grabs keypresses. Because of this a paste command can be executed on the main window when attempting to paste into the search box (when ctrl-c is used as a paste binding).
Adding an empty binding for copy and paste resolves this. Additionally it was required to prevent the terminal view from handling paste requests to prevent pasting to searchbox and terminal at the same time.
2020-07-18 10:28:52 +02:00
Loreno10andFelix Seidl 5642e755af ADD base for increasing and decreasing font size with keys (#728)
* ADD base for increasing and decreasing font size with keys

* finish resize implementation

Co-authored-by: Felix Seidl <f.seidl@me.com>
2020-07-15 20:00:56 +02:00
RhavoX 981eb9b362 fixes #761 (#814)
Also copies title to other tabs
2020-07-15 17:39:41 +02:00
RhavoX dd6917e1f7 Allows to hide dialog on escape press even when textbox has focus. (#815)
Also regains focus on wrong input when enter is used instead of allowing the terminal in the background to grab it.
2020-07-15 07:46:34 +02:00
RhavoX e9769a776d fixes #790 (#813) 2020-07-15 07:43:15 +02:00
RhavoX 1068ca98e8 Fix for lack of focus on search textbox. Reason being that attempt to focus was set before the control was visible most of the time. (#812) 2020-07-13 21:04:10 +02:00
Felix Seidl c2c93878c7 Update README.md 2020-06-28 17:22:51 +02:00
Felix Seidl cded0ad97a Update readme 2020-06-28 17:18:40 +02:00
Felix Seidl fb299121a3 Fix localisation of tab close button tooltip
fixes #764
2020-06-28 17:12:48 +02:00
Felix Seidl cce7b6e0f1 update dependencies 2020-06-28 17:06:05 +02:00
Felix Seidl c169dd496b update to latest xterm.js 2020-06-28 17:03:24 +02:00
kocuvan 70d117cb64 Added Slovenian translation (#789) 2020-06-06 22:45:33 +02:00
Max Hora 39e8f2ba7a Export/import settings for default shell profile and default theme. (#773) 2020-06-01 10:52:59 +02:00
Felix Seidl 0a8c186a98 update xterm 2020-05-30 11:41:14 +02:00
Felix Seidl ea23b37947 Update nuget dependencies 2020-05-30 11:32:38 +02:00
Max Hora ea50883d1c Enhance OpenSSH output on restart of session. (#767) 2020-05-24 14:09:08 +02:00
Felix Seidl 1c08a6d486 Increase version 2020-05-23 10:30:19 +02:00
Felix Seidl 9f75007e21 Added croatian translation 2020-05-09 11:04:31 +02:00
Felix Seidl 38e1c4f68f temporarily remove smart buffer 2020-05-02 21:07:46 +02:00
Felix Seidl 57cfa6e99a Update translations 2020-05-02 20:55:50 +02:00
Felix Seidl 1e7831c9a5 revert Win2D to fix build issues 2020-05-02 20:49:24 +02:00
Felix Seidl 16730d2810 update dependencies 2020-04-25 23:39:32 +02:00
Max Hora 4ca061baa9 Fixes random hanging on closing of ConPty session by disposing pseudo console before its writing pipe. (#733) 2020-04-23 08:04:37 +02:00
Felix Seidl b753dcbf4d Update translations 2020-04-20 21:33:01 +02:00
Felix Seidl 7dc2c85d62 Update main.yml 2020-04-15 22:19:02 +02:00
Felix Seidl 5e76cf9967 Fix CI 2020-04-15 22:10:15 +02:00
Felix Seidl 8999ded059 Fix search panel icon for older windows 10 versions
fixes #721
2020-04-15 22:00:39 +02:00
Felix Seidl 6d83e357d6 Revert to official xterm.js release and update dependencies 2020-04-13 12:06:19 +02:00
Max Hora 05c385342c Fixes performance issues of multiple ConPty sessions with fast output. (#715) 2020-04-12 23:00:47 +02:00
Felix Seidl 0dc1a1cd5d don't treat AltGr like Alt+Ctrl
fixes #699
2020-04-10 15:03:19 +02:00
Felix Seidl e1fc980779 fix missing translations for tab color names 2020-04-10 14:00:40 +02:00
Felix Seidl ccd8c1eaa4 improve settings import/export 2020-04-10 13:52:23 +02:00
Restia666Ashdoll ad1a933207 Update README.md (#707) 2020-04-08 08:04:46 +02:00
Felix Seidl 8d7c57d6fe allow more keybindings (again)
fixes #697
2020-04-05 18:33:43 +02:00
Felix Seidl 8d039f59ef prevent conpty from changing the title to its executable location
fixes #687
2020-04-05 17:43:53 +02:00
Felix Seidl cb8d3cabc7 fix missing tab color names 2020-04-05 17:23:47 +02:00
Felix Seidl 579def4f63 Fix tab color for unselected tabs
fixes #688
2020-04-05 17:19:08 +02:00
Max Hora 5d54d448b0 Rollback to use double quotes around identity file for ssh cli cmd. (#698) 2020-04-05 13:28:46 +02:00
Max HoraandFelix Seidl dec492e39a Reconnect failed tab. (#682)
* Reconnect failed tab.

* Reconnect session overlay; Reconnect tab command is removed from context menus.

* some ui changes + localization

Co-authored-by: Felix Seidl <f.seidl@me.com>
2020-03-31 20:01:09 +02:00
Felix Seidl 20540937fe Update main.yml 2020-03-27 12:26:04 +01:00
Felix Seidl 3dd29d7bb7 Update readme 2020-03-27 12:05:35 +01:00
Felix Seidl 90ac2b0c7c Increase version 2020-03-27 11:56:47 +01:00
Felix Seidl e459255b23 Update translations 2020-03-25 19:27:45 +01:00
Felix Seidl 9165716899 Update main.yml 2020-03-25 19:14:16 +01:00
Felix Seidl 49cf63c6b8 use xterm fork to fix selection color issues 2020-03-25 18:42:47 +01:00
Felix Seidl d0b5f99e28 fix issue displaying the selected terminal theme in settings 2020-03-25 17:49:17 +01:00
Felix Seidl e1b28d2f83 fix crash on quick launch to new window (#672) 2020-03-25 17:47:32 +01:00
Felix Seidl 24a2a23b23 compress large tile png to fix CI 2020-03-24 23:15:38 +01:00
Felix Seidl 25536e9bcb New icon 2020-03-24 23:01:33 +01:00
Felix Seidl 2f984fc411 addded chinese simplified, indonesian and arabic localizations 2020-03-24 21:48:59 +01:00
Aleksandar Pesic 596eee712b "Use buffer" profile settings implemented. (#680) 2020-03-23 18:54:06 +01:00
Felix Seidl 7b18037862 Update to latest UWP version 2020-03-21 10:05:16 +01:00
Felix Seidl 41cffb1a1c fix security issues 2020-03-20 16:32:35 +01:00
Felix Seidl 6ddfba93d9 fix app menu placement 2020-03-15 11:18:31 +01:00
Felix Seidl 6a1e12c83a Fix crash when opening menu in older Windows 10 versions
fixes #528
2020-03-15 10:54:48 +01:00
Felix Seidl e24261ab02 added new mouse action: copy selection or paste if none
fixes #298
2020-03-07 14:25:08 +01:00
Felix Seidl 60d294b20f improved icons in app menu 2020-03-07 11:59:23 +01:00
Felix Seidl 1ee051a106 remove obsolete line ending translation setting 2020-03-07 11:41:19 +01:00
Felix Seidl ea1a167dd3 improve paste implementation 2020-03-07 11:09:21 +01:00
Felix Seidl 1a91a6b22e small layout fix 2020-03-07 10:36:30 +01:00
Felix Seidl 941c525409 fix ~ input on german keyboard layout 2020-03-05 15:59:26 +01:00
Felix Seidl d7f741c2ad Move show text copied notification option to general settings 2020-03-04 22:21:24 +01:00
Felix Seidl ab73b28582 Use profile name as default tab title
fixes #639
2020-03-03 22:40:36 +01:00
Felix Seidl 95ccf3a984 Removed profile selection dialog 2020-03-03 22:10:33 +01:00
Felix Seidl 93eccc3470 Localize search options, focus search on open 2020-03-03 21:33:05 +01:00
Felix Seidl f86f20321d Rework tab context menu 2020-03-03 18:39:38 +01:00
Felix Seidl 475e2da999 Reworked context menu 2020-03-01 13:04:26 +01:00
Felix Seidl 985cac99e6 Use MDL2 icons for settings 2020-02-29 22:18:55 +01:00
Felix Seidl 832b9bdf97 Align resize overlay style to new search overlay 2020-02-29 21:45:40 +01:00
Max Risuhin 264502e1e6 Disable WebView zooming to prevent crash on too small font size. (#673) 2020-02-29 20:40:34 +01:00
Felix Seidl 6015dbda2f fix crash on ctrl+f when search is open 2020-02-29 20:40:01 +01:00
Felix Seidl d879ee02fc better background color for search overlay 2020-02-29 20:39:43 +01:00
Felix Seidl 27c55c6f09 Reworked search view, added search options 2020-02-29 19:09:54 +01:00
Felix Seidl 395e380a3f fix flash of white background on new window 2020-02-29 16:20:52 +01:00
Felix Seidl ae6e61d6fa Rework themes settings 2020-02-29 15:30:46 +01:00
Felix Seidl 5ae7151916 Added handler for xterm.js onBinary (supposed to handle legacy mouse reports) 2020-02-29 13:24:09 +01:00
Felix Seidl 0e2821eeca Added option to use non opaque background
fixes #279
2020-02-29 11:46:29 +01:00
Felix Seidl 3ef374f79d Remove obsolete project settings 2020-02-25 18:48:18 +01:00
Felix Seidl 9413cdc500 Update dependencies, fix solution config 2020-02-25 18:22:51 +01:00
Felix Seidl 80d57342f9 use unicode 11 2020-02-25 18:22:07 +01:00
Felix Seidl 089ec5dc54 Update translations 2020-02-25 18:21:49 +01:00
Felix Seidl a9ec669a27 add new unicode addon 2020-02-24 18:55:28 +01:00
Aleksandar Pesic 7ed503390d Improved TabBar. (#668) 2020-02-24 11:47:04 +01:00
Felix Seidl 33bc06a366 Update xterm.js, use official serialize addon
#fixes #652
2020-02-10 19:20:55 +01:00
Aleksandar Pesic 9c6a7ea2e2 Fixed a bug with profile change in history commands not being preserved. (#661) 2020-02-06 08:47:51 +01:00
Max Risuhin 3126d7b672 Removes domain name from user name. (#660) 2020-02-05 17:13:19 +01:00
Felix Seidl b5be5e4995 Update translations 2020-02-01 12:07:34 +01:00
Felix Seidl 6a0b4b13f2 Update README.md 2020-01-31 19:03:08 +01:00
Marcus Perryman d3ad7b3797 adjustments that allow CI build step to run locally (#659)
* adjustments that allow CI build step to run locally

* stupid typo!
2020-01-31 19:02:11 +01:00
Aleksandar Pesic 6ae99e641b Fixes a bug with Copy crashing the app. (#656) 2020-01-23 07:11:42 +01:00
Felix Seidl d1bf35607e Update dependencies 2020-01-12 12:26:46 +01:00
Aleksandar Pesic 603bcdffa4 Fixing a bug with new window crashing the app (#650)
* Continuation options and code cosmetics in async methods.

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

* async/await in FluentTerminal.SystemTray project.

* Reviewing async/await pattern in FluentTerminal.App.Services project.

* Fixed a bug with new windows crashing the app.
2020-01-09 13:17:47 +01:00
Aleksandar Pesic 71f2d56301 async/await pattern improved in FluentTerminal.App.Services project (#649)
* Continuation options and code cosmetics in async methods.

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

* async/await in FluentTerminal.SystemTray project.

* Reviewing async/await pattern in FluentTerminal.App.Services project.
2020-01-09 11:49:41 +01:00
Aleksandar Pesic 7f816d96cc async/await pattern improved in FluentTerminal.SystemTray project (#648)
* Continuation options and code cosmetics in async methods.

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

* async/await in FluentTerminal.SystemTray project.
2020-01-08 18:42:59 +01:00
Aleksandar Pesic 5a2e2a5292 async / await pattern improved in FluentTerminal.App.ViewModels project (#647)
* Continuation options and code cosmetics in async methods.

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.
2020-01-08 18:40:50 +01:00
Felix Seidl 73f0c3e313 Update README.md 2020-01-05 11:47:17 +01:00
Aleksandar Pesic ff94887899 Bug with env var names becoming lowercase fixed. (#644) 2020-01-04 12:54:20 +01:00
Aleksandar Pesic a8a357dc4b Command validation bug fixed (#642)
* App crash on validation bug fixed

* Fixed command validation bug.
2020-01-01 14:13:58 +01:00
Aleksandar Pesic 21d76a5245 App crash on validation bug fixed (#641) 2020-01-01 14:11:51 +01:00
Felix Seidl 0627495113 Update main.yml 2019-12-31 15:19:01 +01:00
Felix Seidl 3397760955 Update main.yml 2019-12-31 15:02:57 +01:00
Felix Seidl ec9169306f Update main.yml 2019-12-31 14:43:08 +01:00
Felix Seidl 1e48cb85d2 Update main.yml 2019-12-31 14:37:02 +01:00
Felix Seidl 055e265f2f Update main.yml 2019-12-31 14:34:56 +01:00
Felix Seidl f692453ee2 Update main.yml 2019-12-31 14:23:57 +01:00
Felix Seidl 272cc36788 Update main.yml 2019-12-31 14:09:05 +01:00
Felix Seidl 5948ee6a4c Create main.yml 2019-12-31 14:06:21 +01:00
Felix Seidl 47fc720df0 Added scoop to readme 2019-12-28 10:55:17 +01:00
Aleksandar Pesic 8008ba1298 BufferedReader feature (#636)
* Extracting BufferedReader feature from my other branch to make a separate PR.

* Renaming a file.

* Code cosmetics.

* Code cosmetics.
2019-12-27 09:03:31 +01:00
Mayank Thakur 7251beb2f1 Changed README languages supported (#631)
Indian to Hindi.
Indian is not a language
2019-12-26 22:22:10 +01:00
Felix Seidl 79c48e4852 Update xterm.js 2019-12-14 11:17:07 +01:00
Aleksandar Pesic a709220e3a Fixing a bug... (#628) 2019-12-12 22:37:59 +01:00
Aleksandar Pesic f5d23bce49 Async patterns in TrayProcessCommunicationService reviewed (#625)
* Reviewed async pattern in App.xaml.cs.

* Reviewed async pattern throughout FluentTerminal.App project.

* Async patterns in TrayProcessCommunicationService reviewed.

* Minor changes.

* Replaced ContinueWith with async/await in TrayProcessCommunicationService.

* Minor change.
2019-12-12 13:21:41 +01:00
Aleksandar Pesic be4d1d5cab Reviewed async pattern throughout FluentTerminal.App project (#624)
* Reviewed async pattern in App.xaml.cs.

* Reviewed async pattern throughout FluentTerminal.App project.
2019-12-12 13:17:39 +01:00
Aleksandar Pesic 8dad2eb81b Fixed app-crashes-on-tab-detach bug. (#627) 2019-12-12 13:14:43 +01:00
Aleksandar Pesic 8019709534 Reviewed async pattern in App.xaml.cs. (#623) 2019-12-07 10:01:15 +01:00
Aleksandar Pesic 83b9b0d457 CS4014 warning solved (#622)
* Fixing async patterns in IThemeParser.

* CS4014 warning solved.
2019-12-05 15:26:15 +01:00
Aleksandar Pesic e559c3ee38 Fixing async patterns in IThemeParser. (#621) 2019-12-05 12:48:45 +01:00
Aleksandar Pesic a829c2e135 Removing Task.Run or replacing it with Task.Factory.StartNew. (#620) 2019-12-05 11:24:31 +01:00
Aleksandar Pesic 173baf899d Changes in getting profiles from ISettingsService. Also few more minor changes. (#617) 2019-12-04 11:35:50 +01:00
Aleksandar Pesic ad2f45f062 Fixing TerminalBridge. (#619) 2019-12-04 10:20:23 +01:00
Aleksandar Pesic 85d04eab4b Overlay binding errors fixed, #3 - the best solution (#616)
* Overlay binding bug fixed - the best solution.

* Comment only.
2019-12-03 20:36:23 +01:00
Aleksandar Pesic ec8ad133d1 Fixed binding errors in Settings -> SSH Profiles by removing some obsolete code. (#614) 2019-12-03 17:50:20 +01:00
Aleksandar Pesic c8b4d2ed8f Select terminal after closing issue fixed. (#612) 2019-12-03 17:48:34 +01:00
Aleksandar Pesic b377ac8e38 Hidding "Mosh Prots" in "SSH Info" dialog if "Use Mosh" isn't selected. (#611) 2019-12-03 17:47:34 +01:00
Aleksandar Pesic 12e0d5496d Resize delay and options changed delay reduced to improve responsiveness. (#610) 2019-12-03 17:47:13 +01:00
Aleksandar Pesic 8593b5ac5f Fixed a bug with keyboard shortcuts crushing the app. (#607) 2019-12-02 23:46:19 +01:00
Aleksandar Pesic 122dfccc00 Fixing resizing bug. (#606) 2019-12-02 17:31:05 +01:00
Aleksandar Pesic 7e95f151ed Removing CustomTitleChanged event (#604)
* Dispatcher reviewed and improved.

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

* After merge fixes, and some minor changes.

* DelayedAction fixed and some UI bug fixed.

* Removing TerminalViewModel.CustomTitleChanged event because it's covered by PropertyChanged anyway.
2019-12-02 14:17:02 +01:00
Aleksandar Pesic 087c9aacbb Minor changes in app menu implementation. (#605) 2019-12-02 14:10:55 +01:00
Aleksandar Pesic 5bea17da29 Improved DelayAction and a bug fixed (#603)
* Dispatcher reviewed and improved.

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

* After merge fixes, and some minor changes.

* DelayedAction fixed and some UI bug fixed.
2019-12-02 14:09:08 +01:00
Aleksandar Pesic af25086090 XtermTerminalView improvements and some async simplifications (#602)
* Dispatcher reviewed and improved.

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

* After merge fixes, and some minor changes.
2019-12-02 12:49:21 +01:00
Felix Seidl 80673696e9 Added separators and a quit item to the menu 2019-12-01 13:08:09 +01:00
Felix Seidl 1668908489 Fix menu placement 2019-12-01 11:55:59 +01:00
Felix Seidl 18164f1075 Change default to cascading menu 2019-12-01 11:55:46 +01:00
Felix Seidl c461cdc37e Added localizations for Arabic (Iraq), Azerbaijani, Japanese, Korean and Ukrainian 2019-12-01 11:37:13 +01:00
Aleksandar Pesic bd843c77b2 XtermTerminalView initialization simplified (#599)
* Dispatcher reviewed and improved.

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.
2019-12-01 10:01:00 +01:00
Aleksandar Pesic 9f3ba75ce6 Dispatcher usage improvements (#598)
* Dispatcher reviewed and improved.

* Minor refactoring.
2019-12-01 09:47:24 +01:00
Aleksandar Pesic a3fbb17615 Replacing DebouncedAction with DelayedAction. (#597) 2019-11-30 17:53:19 +01:00
Felix Seidl eb8c4003a3 Update xterm.js 2019-11-30 13:04:14 +01:00
Felix Seidl a4559ae168 update dependencies, removed some unused build configurations 2019-11-30 12:47:03 +01:00
Aleksandar Pesic 03f0adbdd6 Cascading "New Tab" / "New Window" menu (#595)
* Added icon to "Recent >" menu item.

* Refactored app menu implementation, and implemented keyboard accelerators. Still doesn't work though.

* Few things fixed, but still doesn't work.

* Clicking menu items works.

* Fixing an issue with resources.

* New menu binding implemented. Still doesn't work though.

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

* Further refactoring app menu binding, in order to make it more general.

* Renaming menu items in order to make them usable for both new tab and new window commands.

* Changing menu item keyboard accelerators based on new tab / window settings.

* Menu item keyboard accelerators are dynamically changing with chainging terminals in new tabs/windows option.

* New tab / window settings honored for top level menu items.

* New window mechanism simplified. Terminal-launching code refactored to be more readable. A bug with "SSH Info" fixed.

* Merging the latest changed.

* Fixing some after-merge issues.

* Minor refactoring.

* Minor refactoring.

* Default profile menu item now contains the profile name. Still doesn't update though.

* Default profile name in the menu item now updates.

* Updating resources from POEditor.

* New tab/window cascading menues implemented.

* Reverting translations back to the correct (the latest) version.

* Undoing translation changes from the last commit. I've made these by mistake.

* Configurable tab/window cascading menu.

* "Application menu type" settings value added.

* Minor spelling fix.

* Fixing some after-merge issues.
2019-11-28 21:10:16 +01:00
Aleksandar Pesic 17c0a3b438 Menu items now honor "Open new terminals as" settings (#594)
* Added icon to "Recent >" menu item.

* Refactored app menu implementation, and implemented keyboard accelerators. Still doesn't work though.

* Few things fixed, but still doesn't work.

* Clicking menu items works.

* Fixing an issue with resources.

* New menu binding implemented. Still doesn't work though.

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

* Further refactoring app menu binding, in order to make it more general.

* Renaming menu items in order to make them usable for both new tab and new window commands.

* Changing menu item keyboard accelerators based on new tab / window settings.

* Menu item keyboard accelerators are dynamically changing with chainging terminals in new tabs/windows option.

* New tab / window settings honored for top level menu items.

* New window mechanism simplified. Terminal-launching code refactored to be more readable. A bug with "SSH Info" fixed.

* Merging the latest changed.

* Fixing some after-merge issues.

* Minor refactoring.

* Minor refactoring.

* Default profile menu item now contains the profile name. Still doesn't update though.

* Default profile name in the menu item now updates.

* Reverting translations back to the correct (the latest) version.

* Undoing translation changes from the last commit. I've made these by mistake.
2019-11-27 20:48:10 +01:00
Paulo Elienay II e1652d99dc Fixing some gramatical errors and unusual words (#592)
Why "texto em negrito" and not "texto negrito"? https://support.office.com/pt-br/article/Colocar-o-texto-em-negrito-7D24F3E1-74C1-47F2-88B0-2601779F7CB2
Why "papel de parede" and not "plano de fundo"? https://trends.google.com.br/trends/explore?q=papel%20de%20parede,plano%20de%20fundo
2019-11-26 22:08:50 +01:00
Paulo Elienay II 1c029fc70e It's "brazilian", not "brasilian" (#591)
If that's wrong in any other file I would ask to fix there too
2019-11-26 22:05:25 +01:00
Aleksandar Pesic 7ab42ec1de Menu item keyboard accelerators implemented (#589)
* Added icon to "Recent >" menu item.

* Refactored app menu implementation, and implemented keyboard accelerators. Still doesn't work though.

* Few things fixed, but still doesn't work.

* Clicking menu items works.

* Fixing an issue with resources.

* New menu binding implemented. Still doesn't work though.

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

* Further refactoring app menu binding, in order to make it more general.

* New window mechanism simplified. Terminal-launching code refactored to be more readable. A bug with "SSH Info" fixed.

* Merging the latest changed.
2019-11-26 22:03:22 +01:00
Aleksandar Pesic 4b21a5abb6 New window mechanism simplified. Terminal-launching code refactored to be more readable. A bug with "SSH Info" fixed. (#593) 2019-11-25 11:56:53 +01:00
Aleksandar Pesic b78e775285 Fixed a bug with Tab selection in Quick Launch dialog (#588) 2019-11-22 12:19:22 +01:00
Aleksandar Pesic a872d126ce "Recent" menu implemented (#585)
* Refactoring CommandHistoryService to prepare "Recent" menu item feature.

* "Recent" menu implemented, but still not bound to commands.

* "Recent" menu implemented.
2019-11-20 18:17:40 +01:00
Aleksandar Pesic 5bc679d8e6 Enter selection in "Quick Launch" immediately launches the command (#582)
* Implementing a hack that allows Tab-selection in "Quick Launch" dialog.

* Removing some not needed code.

* Attempt closing ContentDialog programaticaly.

* Fixing a bug caused by new changes.

* Immediate launch on 'Enter' implemented.
2019-11-17 10:36:53 +01:00
Aleksandar Pesic ae2b4a481d Tab-selection in "Quick Launch" dialog (#581)
* Implementing a hack that allows Tab-selection in "Quick Launch" dialog.

* Removing some not needed code.

* Fixing a bug caused by new changes.
2019-11-17 09:51:57 +01:00
Aleksandar Pesic d90c403060 Select command with mosue in "Quick Launch" dialog (#579)
* Implemented selecting command from the list in "Quick Launch" with mouse.

* Added short comment / explanation.
2019-11-16 10:03:59 +01:00
Aleksandar Pesic 3a8fcaf099 Implemented keyboard shortcuts on window. (#578) 2019-11-16 10:02:34 +01:00
Aleksandar Pesic 473732b5ca Logging UnobservedTaskException (#577) 2019-11-13 07:23:03 +01:00
Daisuke Sugiyama 8fa971c4e5 Fix activation problem when opening a new window in a new desktop (#574) 2019-11-09 12:36:58 +01:00
Aleksandar Pesic fd48f021e0 Fixing a bug, adding docstring, improving MOSH_README, upgrading mosh.exe (#572)
* Fixing a bug, adding docstring, and improving MOSH_README.

* Trivial change.

* Another trivial change in MOSH_README.

* And another trivial...

* Improved mosh.exe.
2019-11-09 12:22:58 +01:00
Aleksandar Pesic 6868bfa7ca New mosh implemented (#571)
* Mosh executables changed.

* New mosh from dialog.

* CommandHistoryService implemented.

* Backward compatibility for command history implemented.

* Implemented mosh backwards compatibility for saved profiles. But the app doesn't work!

* Saved profiles are backwards compatible, and the app works.

* Adding readme file for new mosh.
2019-11-07 17:10:10 +01:00
Jenia Dysin dcde49c7b3 Added hebrew translation (#567)
* Added hebrew translation.

* Updated script and readme
2019-11-05 23:21:46 +01:00
Jenia Dysin 21631f5cca Add font weight setting (#560)
* Added font weight setting functionality

* Removed BoldText setting
2019-10-30 22:57:36 +01:00
Max Risuhin 89a75ba782 Default to WinPty settings option. (#562) 2019-10-30 22:53:01 +01:00
Max Risuhin 7893e804a4 Handle IOException on user input write to pty. (#563) 2019-10-28 07:20:53 +01:00
Aleksandar Pesic f19d457209 Implement xterm paste instead of writing to input stream (#557)
* Implement xterm paste instead of writing to input stream. It solves https://github.com/jumptrading/FluentTerminal/issues/201

* Use existing InvokeScriptAsync wrapper
2019-10-22 22:19:32 +02:00
Jenia Dysin 2d94753690 Add ability to copy link location (#547)
* Finished implementing ILinkMatcherOptions.

* Need to implement custom event handler for hover

* For some reason this only works on first try...

* Removed clearSelection from leaveCallback

* Finished implementing link right click logic.
2019-10-20 10:41:07 +02:00
Leonardo Di Stasio 37b4fd283a Add theme background image feature (#545)
* Add a background image property in theme

* Add the browse command to theme settings page

The goal was to create a command that permit to browse user pictures.
The user can now select is picture by browsing file or to simply copy the
link of the image in the textbox.

* Try to show the image that we download

* Change the save image method in file service

We call the method `ImportImageFile` so that we can show a picker dialog
and directly import the image in the roaming folder.
We make some check to avoid the application crash, still need to test

* Reorganize the UI for image import

* Add background brush converter

Now that we have a background image we need to set the background according
to the theme color or the selected image.
To do this we need to use the `ThemeViewModel` and the new converter to
select the right brush to use (`ImageBrush` or ` AcrylicBrush`)

* Use the File object insted of a image path

* Add background style to content and toolbar

* Create the ImageFile object

* Use the ImageFile object so that we can serialize the theme

* Remove commented code

* Remove the base64 string of the image file

* Disable if it's a preinstalled theme

* Add new event when background image changed

* Make the RemoveImportedImage method async

* Subscribe to background image event and change background in code behind

* Add the delete background image command

* Remove unused converters that we created

* Save background image in temporary folder

* Remove the temporary images and save the last one

* Implement the cancel and delete background

* Show the background image in terminal

* Split the image file methods in another interface

* Change the name of image saving method

* Prepare the object and methods to export encoded image

* Add import method to deserialize correctly the theme

* Remove the private from the set accessor

* Implememnt the import method

* Implement the conversion base64 image to real image

* Create the background image and import it

* Fix some actions bug

* Get the new image file name after import

* Fix some bugs when the image is deleted

* Move the background image in terminal view

* Translate the background image text

* Add the delete background command

* Fix the background image deletion if we click on cancel

* Add background opacity property to terminal view model

* Need to create a theme changed event

If we don't notify the view that the selected theme has changed with
another theme that has the same background but a different background image
we'll never see this image appear.
In fact the background change is triggered by the background color change.

* Remove the background from the theme settings page

* Fix error of last edited image

* Fix image background aspect ratio
2019-10-20 10:30:52 +02:00
François-Joseph du Fou 46b8bb037a Add localisations (#544)
- Indian
- Italian
- Dutch
- Polish
- Portuguese (Brasilian)
- Romanian
- Russians
2019-10-16 20:41:13 +02:00
Aleksandar Pesic 073b15c5f8 Fixing a memory leak after tab open/close (#548)
* Implementing IDisposable on XtermTerminalView.

* Push tab leak fix.

* Undo dummy change.
2019-10-16 20:40:48 +02:00
FelixSe b08fb11a78 Increase version, hopefully fixed some packaging issues 2019-10-12 14:05:06 +02:00
FelixSe f5c91c10e3 Added localizations: italian, dutch, polish 2019-10-12 11:03:18 +02:00
Mark Kopenga 223fafcf1c Added dutch translations (#543) 2019-10-09 22:11:13 +02:00
Aleksandar Pesic cbdf629b49 Fixed a memory leak with opening / closing windows. (#542) 2019-10-07 22:47:30 +02:00
FelixSe 614e86f38a Update dependencies 2019-10-07 18:13:16 +02:00
Max Risuhin 1519fc1f1a Revert back passing of full ssh.exe path to allow WinPTY locate executable without error. (#537) 2019-10-03 11:28:14 +02:00
Thomas P 9eb31ec872 Enable app downgrading in the install script (#535)
Sometimes when an update has an issue it can be useful to downgrade, this is what this change allows for (especially when using Chocolatey for package management).

The [official Powershell documentation](https://docs.microsoft.com/en-us/powershell/module/appx/add-appxpackage?view=win10-ps) was not very verbose about this flag - https://github.com/MicrosoftDocs/windows-powershell-docs/issues/1398

So I'm assuming the flag has a similar behaviour as the one described here: https://docs.microsoft.com/en-us/uwp/schemas/appinstallerschema/element-force-update-from-any-version
2019-10-01 21:38:11 +02:00
FelixSe 5c568e6c33 Set minimum font size to 2
fixes #533
2019-09-30 21:18:16 +02:00
FelixSe b07c56247a Added portugese (brasilian) localization 2019-09-30 21:12:02 +02:00
FelixSe ad82c96d8c Added hindi localization 2019-09-30 21:00:54 +02:00
FelixSe 2bca8b345f Update translations, add Romanian localization 2019-09-30 18:11:02 +02:00
barkbork 6c2ee1bfff Fixed a typo in README.md (#530) 2019-09-26 21:26:12 +02:00
FelixSe 83ead9a1d4 Increase version 2019-09-22 16:08:27 +02:00
FelixSe 3b78de25a4 Add missing store association file 2019-09-22 14:20:01 +02:00
Felix e36704b2b1 Cleanup packaging related stuff 2019-09-22 13:37:36 +02:00
Felix 1fd253ff6b fix npm audit findings 2019-09-22 11:28:05 +02:00
FelixSe 3ed69a048b Use current directory for CLI activation if none specified 2019-09-21 12:14:27 +02:00
FelixSe 43ed08e0e2 Update translations 2019-09-19 22:29:28 +02:00
FelixSe e89721130b Update xterm.js 2019-09-18 22:42:59 +02:00
FelixSe 7cbcce3404 fix links opening twice 2019-09-18 22:42:49 +02:00
FelixSe 708152ee19 Restore toggle window functionality 2019-09-18 22:31:15 +02:00
Felix 26317d9ed2 [WIP] Use WebAllowedObject for communication between App and WebView (#522) 2019-09-18 21:04:23 +02:00
Aleksandar Pesic d4de37fd15 Replacing TerminalOptionsChanged event with MVVMLight Messenger implementation. (#521) 2019-09-17 12:53:19 +02:00
Aleksandar Pesic 88fdd2fec5 Replaces events with MVVMLight Messenger (#519)
* ThemeAdded and ThemeRemoved events replaced by MVVMLight Messenger.

* ApplicationSettingsChanged event replaced by MVVMLight Messenger implementation.

* CurrentThemeChanged event replaced by MVVMLight Messenger implementation.

* SettingsService.KeyBindingsChanged event replaced with MVVMLight Messenger implementation.

* Further simplifying KeyBindingsChangedMessage propagation.

* ShellProfileAdded, ShellProfileDeleted, SshProfileAdded and SshProfileDeleted events replaced by MVVMLight Messenger implementation.
2019-09-15 10:37:31 +02:00
FelixSe 3493bbcf17 Fix package creation problems 2019-09-14 14:24:34 +02:00
FelixSe 7ffa71d254 Cleanup unused configurations 2019-09-13 15:58:14 +02:00
FelixSe aa21efb86b Some UI fixes 2019-09-13 11:14:30 +02:00
FelixSe 54548564dd cleanup package manifest 2019-09-13 10:41:33 +02:00
FelixSe 1f3dcc3332 Fix unit tests 2019-09-13 10:23:04 +02:00
FelixSe dfb2de91f9 Update dependencies, fix some layout issues 2019-09-13 10:16:52 +02:00
FelixSe 33c2fa07b8 Import original selection colors in iTerm theme parser 2019-09-12 22:27:16 +02:00
FelixSe 3bf35c4d5d Reworked about page
fixes #167
2019-09-12 22:11:36 +02:00
Felix 912dddaa6c Update README.md 2019-09-12 20:54:41 +02:00
Felix bad0f85f6b Use UWP package project (#518)
* Use UWP package project

* Add binary dependencies to version control
2019-09-12 20:53:21 +02:00
FelixSe b46dcb193f Make weblinks clickable
fixes #354
2019-09-12 11:23:06 +02:00
FelixSe 2c0aa31be7 Update UpdateTranslations.ps1 2019-09-11 22:47:20 +02:00
FelixSe 01a672130c Added russian localization 2019-09-11 17:08:18 +02:00
FelixSe a1a37d33f9 Update translations 2019-09-11 17:00:03 +02:00
FelixSe 48216e342b Update xterm.js 2019-09-11 16:08:45 +02:00
Jenia Dysin 1a442ff002 Fixed overlap in Settings page. (#517)
* Fixed overlap in Settings page.

* use same size for column definitions
2019-09-11 16:00:51 +02:00
Max Risuhin dbe4aad8ea Avoid memory leaks after terminal window is closed. (#516)
* - Force GC.Collect() on closing of terminal;
- Prepare MainPage, MainViewModel, TabBar, TerminalBridge for disposal.

* Request _dispatcherJobs Task cancellation ASAP on terminal closing.
2019-09-10 01:00:54 +03:00
Max Risuhin 8d9571f5d8 Catch WinPTY exceptions thrown by WinPtySession.Resize on WinPTY internal error during terminal resize request. 2019-09-03 18:59:11 +03:00
Max Risuhin 93939d08bb Terminal settings option to customize XTermJS word separator chars list. 2019-09-02 15:07:08 +03:00
Daisuke Sugiyama 219e342315 Support Shift+Click and "Open in new desktop" feature (#513)
fixes #81
fixes #512
2019-09-01 20:50:49 +02:00
François-Joseph du Fou e11556bd9a New button (#511)
add a dowload button
2019-08-31 22:13:27 +02:00
FelixSe 917cacb18b Fixed notification not showing when copying text with selection
fixes #442
2019-08-31 14:31:22 +02:00
FelixSe b154c7b5aa Set correct profile migration version for new profiles 2019-08-31 14:20:29 +02:00
FelixSe c8ca8e55df Do not clone keybindings when duplicating profiles
fixes  #474
2019-08-31 14:16:26 +02:00
Felix 39cecde560 Add option to edit environment variables for profiles (#508)
fixes #470
2019-08-30 15:23:03 +02:00
FelixSe c9c029f821 Fix some sonar findings 2019-08-30 15:03:58 +02:00
Max Risuhin ecd40f5c43 Avoid crash on user input processing before ConPTY is fully initialized. (#510) 2019-08-30 12:25:12 +02:00
Max Risuhin d14236d502 Validate new terminal position index on insert. (#507) 2019-08-29 21:59:10 +02:00
Max Risuhin 5eecbc6718 Crash on close; Resizing leads to freezed terminals. (#505)
* - Prevent crash on close of uninitilized terminal.
- Reset Action reference from DebouncedAction instance.

* Define CloseCommand as AsyncCommand.
2019-08-28 14:39:58 +02:00
Max Risuhin 09e4381ea7 Extend list of word separators for text selection. (#504) 2019-08-28 07:41:18 +02:00
Max Risuhin cc991c6761 Ignore Win32Exception on attempt to kill pty process tree. (#503) 2019-08-27 23:41:29 +02:00
François-Joseph du Fou defec12c04 Add contributors button (#502)
* Add contributors button

Add contributors button (Shields.io button)

* Update README.md
2019-08-27 13:14:43 +02:00
François-Joseph du Fou ef1c5fcfc5 Add version + new top (#500)
version after the buils status, sonarcloud and gitter,
new top in the center for the logo and the name
it's juste beautiful ^^
2019-08-26 14:28:51 +02:00
François-Joseph du Fou 0d12bf6b4a Add logo (#498)
logo for more visibility
2019-08-26 07:38:19 +02:00
François-Joseph du Fou 2e432967af adding available languages (#496)
* adding available languages

English, German, Spanish, French, Simplified Chinese added in languages available

* Add link to POEditor
2019-08-25 11:11:56 +02:00
François-Joseph du Fou 8e53a6909f Add French localization (#495)
* Add French localization

French can use the app ;')

* Add french localization to app
2019-08-24 17:53:22 +02:00
FelixSe 38b3e4ec94 Fixed issues with selection colors in imported themes (fixes #334) 2019-08-24 17:18:58 +02:00
FelixSe 6c00c1f497 Removed some margin 2019-08-24 17:17:23 +02:00
FelixSe 121b3d6dc1 Use WinUI 2019-08-24 16:48:43 +02:00
FelixSe d2eed2b4fc Update nuget packages 2019-08-24 13:22:23 +02:00
Max Risuhin 5decbe9b86 Avoid static FleckLog.LogAction reset. (#494) 2019-08-24 13:12:15 +02:00
FelixSe 91ba648095 Update to latest xterm version 2019-08-24 12:32:49 +02:00
FelixSe 919b4930b9 Fix failing test 2019-08-24 11:45:01 +02:00
François-Joseph du Fou fb3c76184c Multi-language support: Add fr-fr (#492)
all translate in french, I do not know which modifier to change to take into account if it is not automatic
2019-08-24 11:04:42 +02:00
Max Risuhin d72a64ce67 Resolve GC's disposal issues. (#493) 2019-08-24 10:48:35 +02:00
Menno Finlay-Smits 07248bd28d Ensure processes are killed for ConPTY sessions (#482)
- Clarified Dispose logic for ConPTY Terminal and Process
- Disposing a ConPTY Process now kills it and its children
- ConPTY Terminal now cleans up everything in its Dispose
- Wired up missing Dispose calls in various finalizers
- Removed CTRL_CLOSE_EVENT handling. This event never seems to actually happen.

Fixes https://github.com/jumptrading/FluentTerminal/issues/94
2019-08-20 08:02:30 +02:00
Max Risuhin 37c948c9ac Middle ellipsised tab titles. (#490) 2019-08-18 11:24:05 +02:00
Max Risuhin 98b6e99743 Duplicate session command. (#489) 2019-08-15 20:45:58 +02:00
Max Risuhin 0ffdf236a1 Do not send pointer events to WebView while dragging of tab happens. (#485) 2019-08-15 20:44:08 +02:00
Aleksandar Pesic 3f78aff86a Ignore [Delete] key over profile name in auto-suggest box. (#486) 2019-08-15 18:39:38 +02:00
Max Risuhin 601cca612c Limit mute looping to 3 seconds timeout. More debug logs. (#484) 2019-08-14 11:48:31 +02:00
Aleksandar Pesic 901cbda824 Saving command history only on actually successful session (#481)
* Implement delayed saving.

* Delayed command history saving improved.

* Fixing a bug with SSH profiles in "quick command" dialog.

* Opening auto-suggest list on down arrow implemented.

* Minor change.

* Removing an obsolete settings option.
2019-08-13 21:27:24 +02:00
Max Risuhin afb50f1bdd Serialize whole xterm.js buffer trimmed from the end. (#480) 2019-08-13 21:22:14 +02:00
Max Risuhin 08877785aa Show tab close button on hover. (#476)
* Show tab close button on hover.

* Advanced tab closing options.
2019-08-12 22:03:25 +02:00
Max Risuhin 71282eb65d Tab dropping extra space; Preserve drop position. (#478)
* Support tab dropping to another window at hovered position.

* Extended tab drop landing area visible only during dragging.
2019-08-12 20:07:59 +02:00
Nicolas Ramz edfb890638 FIXED: clicking on about entry in settings does not deselect previous item fixes #465 (#477) 2019-08-12 20:04:16 +02:00
Aleksandar Pesic e88fb78e9d Quick command implementation (#472)
* Separating "SSH Info" and "Quick Launch", and updating translations.

* Adding "Quick Launch" to menu.

* Auto-complete, initial version.

* Builds, but not finished.

* Improved, but there are still some bugs...

* Minor changes.

* Refactroing request/response identifiers.

* Improving command history.

* Minor refactoring.

* Deleting history by using "Del" key.

* Handling mosh special case in quick commands.

* SSH command handled as any other command in the path.

* New resource strings.

* Fixing quick command links.

* Fixing after-merging bugs.

* Improving filtering.

* Finishing filtering.

* Changing placeholder text.

* Minor refactoring.

* Updating resources.

* Renaming NewRemoteTab back to NewSshTab.
2019-08-08 08:02:09 +02:00
Menno Finlay-Smits 93e52f3209 Remove the "Always show tabs" option (#473)
Now that tags are draggable between windows, this option could cause
confusion.
2019-08-06 21:47:57 +02:00
Max Risuhin 3af29314e7 Mute spawned conhost audio session to avoid bell sound playing. (#468) 2019-08-03 11:08:33 +02:00
Max Risuhin 8b18773da9 Web browser like tabs (#467)
* Web browser like tabs.

* Turn on AlwaysShowTabs by default.

* removed unused event handler
2019-07-31 21:16:22 +02:00
Max Risuhin 8d710fe6d7 ListView templating caused items reordering issues. (#464) 2019-07-30 13:32:09 +02:00
Max Risuhin 010f3fdd7e Upgrade xterm.js to v3.15.0-beta70. (#457) 2019-07-19 06:41:04 +02:00
Max Risuhin 0749d9e17d Run background process to keep conhost audio session active. (#452)
Don't mute audio device.
2019-07-16 22:20:43 +02:00
Menno Finlay-Smits 8a1ebd89ad Standardise on naming of SSH related functionality (#410)
In some places code and UI referring to SSH connections and profiles
used the word "remote". Standardise on "SSH" (this is used more often
anyway).
2019-07-16 22:18:59 +02:00
Max Risuhin 594ebdd36a Settings option to dump terminal output into file. (#438)
* Settings option to dump terminal output into file.

* Notify tray process on settings change.
2019-07-16 22:17:45 +02:00
515 changed files with 39719 additions and 7954 deletions

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
name: CI
on: [push]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- name: Build client
working-directory: ./FluentTerminal.Client
run: |
yarn
yarn build
- name: Setup MSBuild.exe
uses: warrenbuckley/Setup-MSBuild@v1
- name: MSBuild
run: msbuild FluentTerminal.sln -t:build -p:Configuration=Debug -p:Platform=x64 -m -restore
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Services.Test
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
{
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowCreateKeyBindingDialog();
@@ -36,9 +36,9 @@ namespace FluentTerminal.App.Services.Test
var title = string.Empty;
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("title");
}
@@ -49,9 +49,9 @@ namespace FluentTerminal.App.Services.Test
var title = _fixture.Create<string>();
var content = string.Empty;
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("content");
}
@@ -61,9 +61,9 @@ namespace FluentTerminal.App.Services.Test
{
var title = _fixture.Create<string>();
var content = _fixture.Create<string>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content);
showMessageDialogAsnyc.Should().Throw<ArgumentException>().And.ParamName.Should().Be("buttons");
}
@@ -75,30 +75,19 @@ namespace FluentTerminal.App.Services.Test
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var messageDialog = new Mock<IMessageDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ISshProfileSelectionDialog>);
var dialogService = new DialogService(() => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
messageDialog.Verify(x => x.ShowAsync(), Times.Once);
}
[Fact]
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
{
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<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>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowInputDialogAsync(title);
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -11,13 +11,13 @@
</ItemGroup>
<ItemGroup>
<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="AutoFixture" Version="4.11.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@@ -10,6 +10,8 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
using Xunit;
namespace FluentTerminal.App.Services.Test
@@ -36,7 +38,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -58,6 +61,7 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -86,6 +90,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -111,6 +116,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -134,6 +140,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -155,11 +162,13 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var currentThemeChangedEventInvoked = false;
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
settingsService.SaveCurrentThemeId(currentThemeId);
@@ -178,7 +187,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -203,10 +213,12 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = roamingSettings.Object,
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.CurrentThemeChanged += (s, e) => currentThemeChangedEventInvoked = true;
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
settingsService.SaveTheme(theme);
@@ -225,7 +237,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -247,7 +260,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -270,7 +284,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -293,7 +308,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -316,7 +332,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -338,7 +355,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -360,10 +378,12 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.TerminalOptionsChanged += (s, e) => terminalOptionsChangedEventInvoked = true;
Messenger.Default.Register<TerminalOptionsChangedMessage>(this,
message => terminalOptionsChangedEventInvoked = true);
settingsService.SaveTerminalOptions(terminalOptions);
@@ -383,7 +403,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -405,7 +426,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -427,10 +449,12 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.ApplicationSettingsChanged += (s, e) => applicationSettingsChangedEventInvoked = true;
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this,
a => applicationSettingsChangedEventInvoked = true);
settingsService.SaveApplicationSettings(applicationSettings);
@@ -453,7 +477,8 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -477,7 +502,8 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -502,7 +528,8 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -525,10 +552,12 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
@@ -546,7 +575,8 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -571,10 +601,12 @@ namespace FluentTerminal.App.Services.Test
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.KeyBindingsChanged += (s, e) => keyBindingsChangedEventInvoked = true;
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
settingsService.ResetKeyBindings();
@@ -594,6 +626,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
@@ -619,6 +652,7 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
@@ -640,6 +674,7 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -649,7 +684,7 @@ namespace FluentTerminal.App.Services.Test
var result = settingsService.GetShellProfiles();
result.Should().BeEquivalentTo(shellProfiles);
result.Select(p => p.EqualTo(shellProfiles.FirstOrDefault(x => x.Id == p.Id)).Should().BeTrue());
shellProfilesContainer.Verify(x => x.GetAll(), Times.Once);
}
@@ -662,6 +697,7 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -683,6 +719,7 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
@@ -707,6 +744,7 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = localSettings.Object,
@@ -735,6 +773,7 @@ namespace FluentTerminal.App.Services.Test
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = roamingSettings.Object,
@@ -48,7 +48,7 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
var result = await trayProcessCommunicationService.CreateTerminal(id, terminalSize, shellProfile, sessionType);
var result = await trayProcessCommunicationService.CreateTerminalAsync(id, terminalSize, shellProfile, sessionType);
result.Should().BeEquivalentTo(response);
}
@@ -68,9 +68,9 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.ResizeTerminal(terminalId, terminalSize);
await trayProcessCommunicationService.ResizeTerminalAsync(terminalId, terminalSize);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == ResizeTerminalRequest.Identifier)), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
}
[Fact]
@@ -88,7 +88,7 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.Write(terminalId, data);
await trayProcessCommunicationService.WriteAsync(terminalId, data);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
}
@@ -107,9 +107,9 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.CloseTerminal(terminalId);
await trayProcessCommunicationService.CloseTerminalAsync(terminalId);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == TerminalExitedRequest.Identifier)), Times.Once);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
}
[Fact]
@@ -121,7 +121,7 @@ namespace FluentTerminal.App.Services.Test
var message = new ValueSet
{
[MessageKeys.Type] = TerminalExitedRequest.Identifier,
[MessageKeys.Type] = (byte) MessageIdentifiers.TerminalExitedRequest,
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
};
var settingsService = new Mock<ISettingsService>();
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Storage;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App.Services.Adapters
{
@@ -20,6 +21,11 @@ namespace FluentTerminal.App.Services.Adapters
_applicationDataContainer.Values.Remove(key);
}
public void Clear()
{
_applicationDataContainer.Values.Clear();
}
public IEnumerable<object> GetAll()
{
return _applicationDataContainer.Values.Select(x => x.Value);
@@ -51,7 +57,8 @@ namespace FluentTerminal.App.Services.Adapters
public void WriteValueAsJson<T>(string name, T value)
{
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value);
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value,
PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
}
}
}
+1
View File
@@ -10,5 +10,6 @@
public const string SshCommandName = "ssh";
public const string MoshCommandName = "mosh";
public const string ExecutedCommandsContainerName = "HistoryContainer";
public const string TerminalViewModelStateId = "TerminalViewModelState";
}
}
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IAboutDialog
{
Task ShowAsync();
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ICustomCommandDialog
{
Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null);
}
}
@@ -1,12 +0,0 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IShellProfileSelectionDialog
{
ShellProfile SelectedProfile { get; }
Task<ShellProfile> SelectProfile();
}
}
@@ -1,12 +0,0 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshProfileSelectionDialog
{
SshProfile SelectedProfile { get; }
Task<SshProfile> SelectProfile();
}
}
@@ -0,0 +1,11 @@
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.Services.EventArgs
{
public class NewTabRequestedEventArgs : System.EventArgs
{
public DragEventArgs DragEventArgs { get; set; }
public int Position { get; set; }
}
}
@@ -1,10 +1,13 @@
using System;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.EventArgs
{
public class NewWindowRequestedEventArgs : System.EventArgs
{
public Models.Enums.NewWindowAction Action { get; set; }
public Guid ProfileId { get; set; }
public ShellProfile Profile { get; }
public NewWindowRequestedEventArgs(ShellProfile profile) =>
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
}
}
@@ -0,0 +1,23 @@
using System;
namespace FluentTerminal.App.Services.Exceptions
{
public class ReadTextFileException : Exception
{
public ReadTextFileException() : base()
{
}
public ReadTextFileException(string message) : base(message)
{
}
public ReadTextFileException(string message, Exception innerException) : base(message, innerException)
{
}
protected ReadTextFileException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluentTerminal.App.Services.Exceptions
{
public class SaveTextFileException : Exception
{
public SaveTextFileException() : base()
{
}
public SaveTextFileException(string message) : base(message)
{
}
public SaveTextFileException(string message, Exception innerException) : base(message, innerException)
{
}
protected SaveTextFileException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
@@ -6,12 +6,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="PListNet" Version="3.0.0" />
<PackageReference Include="RestSharp" Version="106.6.9" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="MvvmLightLibsStd10" Version="5.4.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="PListNet" Version="3.2.0" />
<PackageReference Include="RestSharp" Version="106.11.4" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
</ItemGroup>
@@ -14,6 +14,8 @@ namespace FluentTerminal.App.Services
void Delete(string key);
void Clear();
IEnumerable<object> GetAll();
}
}
@@ -1,6 +1,7 @@
using FluentTerminal.App.Services.EventArgs;
using System;
using System.Threading.Tasks;
using Windows.UI.Core;
namespace FluentTerminal.App.Services
{
@@ -14,9 +15,13 @@ namespace FluentTerminal.App.Services
int Id { get; }
string Title { get; set; }
Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true);
Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false);
Task<bool> TryClose();
Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false);
Task<bool> TryCloseAsync();
bool ToggleFullScreen();
bool IsApiContractPresent(string api, ushort version);
@@ -4,7 +4,7 @@ namespace FluentTerminal.App.Services
{
public interface IClipboardService
{
Task<string> GetText();
Task<string> GetTextAsync();
void SetText(string text);
}
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface ICommandHistoryService
{
/// <summary>
/// Returns the most recent <paramref name="top"/> commands from history.
/// </summary>
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
/// <param name="top">Maximal number of items to return.</param>
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
/// already have them loaded.</param>
List<ExecutedCommand> GetHistoryRecentFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profilesProvider = null);
/// <summary>
/// Returns the most used <paramref name="top"/> commands from history.
/// </summary>
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
/// <param name="top">Maximal number of items to return.</param>
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
/// already have them loaded.</param>
List<ExecutedCommand> GetHistoryMostUsedFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profilesProvider = null);
void MarkUsed(ShellProfile profile);
void Delete(ExecutedCommand executedCommand);
}
}
@@ -5,21 +5,23 @@ namespace FluentTerminal.App.Services
{
public enum DialogButton
{
// ReSharper disable once InconsistentNaming
OK,
Cancel
}
public interface IDialogService
{
Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons);
Task<ShellProfile> ShowProfileSelectionDialogAsync();
Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons);
Task<KeyBinding> ShowCreateKeyBindingDialog();
Task<string> ShowInputDialogAsync(string title);
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null);
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null);
Task ShowAboutDialogAsync();
}
}
@@ -1,16 +0,0 @@
using System;
namespace FluentTerminal.App.Services
{
public interface IDispatcherTimer
{
void Start();
void Stop();
TimeSpan Interval { get; set; }
bool IsEnabled { get; }
event EventHandler<object> Tick;
}
}
@@ -6,10 +6,12 @@ namespace FluentTerminal.App.Services
{
public interface IFileSystemService
{
Task<File> OpenFile(IEnumerable<string> fileTypes);
Task<File> OpenFileAsync(IEnumerable<string> fileTypes);
Task<string> BrowseForDirectory();
Task<string> BrowseForDirectoryAsync();
Task SaveTextFile(string name, string fileTypeDescription, string fileType, string content);
Task SaveTextFileAsync(string name, string fileTypeDescription, string fileType, string content);
Task<ImageFile> SaveImageInRoamingAsync(ImageFile imageFile);
}
}
@@ -0,0 +1,19 @@
using FluentTerminal.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IImageFileSystemService
{
Task<ImageFile> ImportTemporaryImageFileAsync(IEnumerable<string> fileTypes);
Task RemoveTemporaryBackgroundThemeImageAsync();
Task RemoveImportedImageAsync(string fileName);
string EncodeImage(ImageFile imageFile);
Task<ImageFile> ImportThemeImageAsync(ImageFile backgroundImage, string encodedImage);
}
}
@@ -8,5 +8,6 @@ namespace FluentTerminal.App.Services
void SendCommand(string command);
void DeregisterCommandHandler(string command);
void ClearCommandHandlers();
}
}
@@ -6,17 +6,6 @@ namespace FluentTerminal.App.Services
{
public interface ISettingsService
{
event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
event EventHandler<Guid> CurrentThemeChanged;
event EventHandler KeyBindingsChanged;
event EventHandler<ShellProfile> ShellProfileAdded;
event EventHandler<Guid> ShellProfileDeleted;
event EventHandler<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);
@@ -24,24 +13,26 @@ namespace FluentTerminal.App.Services
TerminalTheme GetCurrentTheme();
Guid GetCurrentThemeId();
ShellProfile GetDefaultShellProfile();
SshProfile GetDefaultSshProfile();
Guid GetDefaultShellProfileId();
Guid GetDefaultSshProfileId();
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
IEnumerable<ShellProfile> GetShellProfiles();
IEnumerable<SshProfile> GetSshProfiles();
/// <summary>
/// Returns union of <see cref="GetShellProfiles"/> and <see cref="GetSshProfiles"/>.
/// </summary>
IEnumerable<ShellProfile> GetAllProfiles();
IEnumerable<TabTheme> GetTabThemes();
TerminalOptions GetTerminalOptions();
TerminalTheme GetTheme(Guid id);
IEnumerable<TerminalTheme> GetThemes();
void ResetKeyBindings();
void SaveApplicationSettings(ApplicationSettings applicationSettings);
void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id);
void SaveDefaultSshProfileId(Guid id);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
void SaveTerminalOptions(TerminalOptions terminalOptions);
void SaveTheme(TerminalTheme theme, bool newTheme = false);
ShellProfile GetShellProfile(Guid id);
@@ -0,0 +1,9 @@
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface IShellProfileMigrationService
{
void Migrate(ShellProfile profile);
}
}
@@ -5,8 +5,8 @@ namespace FluentTerminal.App.Services
{
public interface IStartupTaskService
{
Task DisableStartupTask();
Task<StartupTaskStatus> EnableStartupTask();
Task<StartupTaskStatus> GetStatus();
Task DisableStartupTaskAsync();
Task<StartupTaskStatus> EnableStartupTaskAsync();
Task<StartupTaskStatus> GetStatusAsync();
}
}
@@ -10,5 +10,7 @@ namespace FluentTerminal.App.Services
IEnumerable<string> SupportedFileTypes { get; }
Task<TerminalTheme> Parse(string fileName, Stream fileContent);
Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent);
}
}
@@ -12,30 +12,43 @@ namespace FluentTerminal.App.Services
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType);
Task ResizeTerminal(byte id, TerminalSize size);
Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause);
Task UpdateToggleWindowKeyBindings();
Task ResizeTerminalAsync(byte id, TerminalSize size);
Task Write(byte terminalId, byte[] data);
Task UpdateToggleWindowKeyBindingsAsync();
Task WriteAsync(byte terminalId, byte[] data);
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
Task CloseTerminal(byte terminalId);
Task<GetAvailablePortResponse> GetAvailablePort();
void UnsubscribeFromTerminalOutput(byte terminalId);
Task CloseTerminalAsync(byte terminalId);
byte GetNextTerminalId();
Task<string> GetUserName();
Task<string> GetUserNameAsync();
Task SaveTextFileAsync(string path, string content);
Task<string> ReadTextFileAsync(string path);
Task<string> GetSshConfigDirAsync();
Task<string[]> GetFilesFromSshConfigDirAsync();
Task<bool> CheckFileExistsAsync(string path);
void MuteTerminal(bool mute);
Task MuteTerminalAsync(bool mute);
void UpdateSettings(ApplicationSettings settings);
Task<string> GetCommandPathAsync(string command);
Task QuitApplicationAsync();
}
}
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services
{
public interface IUpdateService
{
Task CheckForUpdate(bool notifyNoUpdate = false);
Task CheckForUpdateAsync(bool notifyNoUpdate = false);
Version GetCurrentVersion();
Task<Version> GetLatestVersionAsync();
}
@@ -1,9 +1,9 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using Windows.Foundation.Metadata;
using Windows.Storage;
namespace FluentTerminal.App.Services.Implementation
{
@@ -11,6 +11,14 @@ namespace FluentTerminal.App.Services.Implementation
{
public ApplicationSettings GetDefaultApplicationSettings()
{
var logDirectoryPath = string.Empty;
try
{
// Accessing ApplicationData.Current during run of unit tests causes
// "System.InvalidOperationException : The process has no package identity" exception.
logDirectoryPath = ApplicationData.Current.LocalCacheFolder.Path;
} catch (InvalidOperationException) { }
return new ApplicationSettings
{
ConfirmClosingTabs = false,
@@ -18,19 +26,23 @@ namespace FluentTerminal.App.Services.Implementation
UnderlineSelectedTab = false,
InactiveTabColorMode = InactiveTabColorMode.Background,
NewTerminalLocation = NewTerminalLocation.Tab,
TabWindowCascadingAppMenu = true,
TabsPosition = TabsPosition.Top,
CopyOnSelect = false,
MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu,
AlwaysShowTabs = false,
ShowNewOutputIndicator = false,
EnableTrayIcon = true,
ShowCustomTitleInTitlebar = true,
UseMoshByDefault = true,
AutoFallbackToWindowsUsernameInLinks = true,
UseQuickSshConnectByDefault = false,
RTrimCopiedLines = true,
MuteTerminalBeeps = true
MuteTerminalBeeps = true,
EnableLogging = false,
PrintableOutputOnly = true,
LogDirectoryPath = logDirectoryPath,
UseConPty = true,
ShowTextCopied = false
};
}
@@ -90,6 +102,30 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.DuplicateTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.DuplicateTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.D
}
};
case Command.ReconnectTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ReconnectTab),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.R
}
};
case Command.NewTab:
return new List<KeyBinding>
{
@@ -102,18 +138,6 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ConfigurableNewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ConfigurableNewTab),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewSshTab:
return new List<KeyBinding>
{
@@ -138,31 +162,30 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.SavedSshNewTab:
case Command.NewCustomCommandTab:
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),
Command = nameof(Command.NewCustomCommandTab),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.U
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewCustomCommandWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewCustomCommandWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
@@ -199,18 +222,6 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ConfigurableNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ConfigurableNewWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ShowSettings:
return new List<KeyBinding>
{
@@ -311,10 +322,39 @@ namespace FluentTerminal.App.Services.Implementation
Key = (int)ExtendedVirtualKey.L
}
};
case Command.IncreaseFontSize:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.IncreaseFontSize),
Ctrl = true,
Key = (int)ExtendedVirtualKey.Plus
}
};
case Command.DecreaseFontSize:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.DecreaseFontSize),
Ctrl = true,
Key = (int)ExtendedVirtualKey.Minus
}
};
case Command.ResetFontSize:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ResetFontSize),
Ctrl = true,
Key = (int)ExtendedVirtualKey.Number0
}
};
default:
throw new InvalidOperationException($"Default keybindings for Command '{command}' are missing");
}
return null;
}
public Guid GetDefaultShellProfileId()
@@ -329,7 +369,7 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme
{
Id = 0,
Name = I18N.Translate("TabTheme.None"),
Name = "TabTheme.None",
BackgroundOpacity = double.NaN,
BackgroundPointerOverOpacity = double.NaN,
BackgroundPressedOpacity = double.NaN,
@@ -340,37 +380,37 @@ namespace FluentTerminal.App.Services.Implementation
new TabTheme
{
Id = 1,
Name = I18N.Translate("TabTheme.Red"),
Name = "TabTheme.Red",
Color = "#E81123"
},
new TabTheme
{
Id = 2,
Name = I18N.Translate("TabTheme.Green"),
Name = "TabTheme.Green",
Color = "#10893E"
},
new TabTheme
{
Id = 3,
Name = I18N.Translate("TabTheme.Blue"),
Name = "TabTheme.Blue",
Color = "#0078D7"
},
new TabTheme
{
Id = 4,
Name = I18N.Translate("TabTheme.Purple"),
Name = "TabTheme.Purple",
Color = "#881798"
},
new TabTheme
{
Id = 5,
Name = I18N.Translate("TabTheme.Orange"),
Name = "TabTheme.Orange",
Color = "#FF8C00"
},
new TabTheme
{
Id = 6,
Name = I18N.Translate("TabTheme.Teal"),
Name = "TabTheme.Teal",
Color = "#00B7C3"
}
};
@@ -386,10 +426,12 @@ namespace FluentTerminal.App.Services.Implementation
ScrollBarStyle = ScrollBarStyle.Hidden,
FontFamily = "Consolas",
FontSize = 13,
BoldText = false,
FontWeight = 400,
BackgroundOpacity = 0.8,
Padding = 12,
ScrollBackLimit = 1000
ScrollBackLimit = 1000,
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;",
UseAcrylicBackground = true
};
}
@@ -406,12 +448,17 @@ namespace FluentTerminal.App.Services.Implementation
{
Id = GetDefaultShellProfileId(),
Name = "Powershell",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+
UseBuffer = false,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
@@ -429,12 +476,17 @@ namespace FluentTerminal.App.Services.Implementation
{
Id = Guid.Parse("ab942a61-7673-4755-9bd8-765aff91d9a3"),
Name = "CMD",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\Windows\System32\cmd.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
UseBuffer = true,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
@@ -452,12 +504,17 @@ namespace FluentTerminal.App.Services.Implementation
{
Id= Guid.Parse("e5785ad6-584f-40cb-bdcd-d5b3b3953e7f"),
Name = "WSL",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.ToLF,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
UseBuffer = true, //TODO: Set to false if the buffer causes issues with WSL.
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
@@ -7,24 +7,24 @@ namespace FluentTerminal.App.Services.Implementation
{
public class DialogService : IDialogService
{
private readonly Func<IShellProfileSelectionDialog> _shellProfileSelectionDialogFactory;
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;
private readonly Func<ICustomCommandDialog> _customCommandDialogFactory;
private readonly Func<IAboutDialog> _aboutDialogFactory;
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
public DialogService(Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
Func<ISshProfileSelectionDialog> sshProfileSelectionDialogFactory)
Func<ICustomCommandDialog> customCommandDialogFactory,
Func<IAboutDialog> aboutDialogFactory)
{
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
_messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory;
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
_sshProfileSelectionDialogFactory = sshProfileSelectionDialogFactory;
_customCommandDialogFactory = customCommandDialogFactory;
_aboutDialogFactory = aboutDialogFactory;
}
public Task<KeyBinding> ShowCreateKeyBindingDialog()
@@ -34,7 +34,7 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.CreateKeyBinding();
}
public Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons)
public Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons)
{
if (string.IsNullOrWhiteSpace(title))
{
@@ -71,21 +71,17 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.GetInput();
}
public Task<ShellProfile> ShowProfileSelectionDialogAsync()
{
var dialog = _shellProfileSelectionDialogFactory();
return dialog.SelectProfile();
}
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) =>
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
public Task<SshProfile> ShowSshProfileSelectionDialogAsync()
{
var dialog = _sshProfileSelectionDialogFactory();
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
_customCommandDialogFactory().GetCustomCommandAsync(input);
return dialog.SelectProfile();
public Task ShowAboutDialogAsync()
{
var dialog = _aboutDialogFactory();
return dialog.ShowAsync();
}
}
}
@@ -9,9 +9,20 @@ namespace FluentTerminal.App.Services.Implementation
{
public class FluentTerminalThemeParser : IThemeParser
{
public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".flutecolors" };
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".flutecolors" };
public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent)
{
return DeserializeThemeAsync<ExportedTerminalTheme>(fileName, fileContent);
}
public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
{
return DeserializeThemeAsync<TerminalTheme>(fileName, fileContent);
}
private async Task<T> DeserializeThemeAsync<T>(string fileName, Stream fileContent)
where T : TerminalTheme
{
if (string.IsNullOrWhiteSpace(fileName))
{
@@ -25,8 +36,8 @@ namespace FluentTerminal.App.Services.Implementation
using (var streamReader = new StreamReader(fileContent))
{
var content = await streamReader.ReadToEndAsync();
var theme = JsonConvert.DeserializeObject<TerminalTheme>(content);
var content = await streamReader.ReadToEndAsync().ConfigureAwait(false);
var theme = JsonConvert.DeserializeObject<T>(content);
theme.PreInstalled = false;
theme.Id = Guid.NewGuid();
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
// ReSharper disable InconsistentNaming
namespace FluentTerminal.App.Services.Implementation
{
@@ -14,7 +15,7 @@ namespace FluentTerminal.App.Services.Implementation
{
private const byte Opacity30Percent = 77;
public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".itermcolors" };
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".itermcolors" };
private static class ITermThemeKeys
{
@@ -36,7 +37,7 @@ namespace FluentTerminal.App.Services.Implementation
public const string Ansi15Color = "Ansi 15 Color";
public const string BackgroundColor = "Background Color";
public const string BoldColor = "Bold Color";
//public const string BoldColor = "Bold Color";
public const string CursorColor = "Cursor Color";
public const string CursorTextColor = "Cursor Text Color";
public const string ForegroundColor = "Foreground Color";
@@ -51,7 +52,7 @@ namespace FluentTerminal.App.Services.Implementation
public const string RedComponent = "Red Component";
}
public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
{
if (string.IsNullOrWhiteSpace(fileName))
{
@@ -65,13 +66,13 @@ namespace FluentTerminal.App.Services.Implementation
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
return new TerminalTheme
return Task.FromResult(new TerminalTheme
{
Name = Path.GetFileNameWithoutExtension(fileName),
Colors = GetColors(node),
Id = Guid.NewGuid(),
PreInstalled = false
};
});
}
private TerminalColors GetColors(DictionaryNode themeDictionary)
@@ -83,6 +84,8 @@ namespace FluentTerminal.App.Services.Implementation
Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]),
CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]),
Selection = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor], Opacity30Percent),
SelectionForeground = GetColorString(themeDictionary[ITermThemeKeys.SelectedTextColor]),
SelectionBackground = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor]),
Black = GetColorString(themeDictionary[ITermThemeKeys.Ansi0Color]),
Red = GetColorString(themeDictionary[ITermThemeKeys.Ansi1Color]),
Green = GetColorString(themeDictionary[ITermThemeKeys.Ansi2Color]),
@@ -110,8 +113,6 @@ namespace FluentTerminal.App.Services.Implementation
var green = dictionaryNode[ITermThemeColorKeys.GreenComponent] as RealNode ?? throw new ParseThemeException("Green node value was not a real number");
var blue = dictionaryNode[ITermThemeColorKeys.BlueComponent] as RealNode ?? throw new ParseThemeException("Blue node value was not a real number");
var color = $"#{alpha:X2}{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
if (alpha == byte.MaxValue)
{
return $"#{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
@@ -133,5 +134,30 @@ namespace FluentTerminal.App.Services.Implementation
{
return (alpha / 256.0).ToString(CultureInfo.InvariantCulture);
}
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent)
{
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentNullException(nameof(fileName));
}
if (fileContent == null)
{
throw new ArgumentNullException(nameof(fileContent));
}
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
var terminalTheme = new TerminalTheme
{
Name = Path.GetFileNameWithoutExtension(fileName),
Colors = GetColors(node),
Id = Guid.NewGuid(),
PreInstalled = false
};
return Task.FromResult(new ExportedTerminalTheme(terminalTheme, string.Empty));
}
}
}
@@ -42,5 +42,10 @@ namespace FluentTerminal.App.Services.Implementation
throw new KeyNotFoundException($"No handler registered for command: {command}");
}
public void ClearCommandHandlers()
{
_commandHandlers.Clear();
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.Text.RegularExpressions;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Implementation
{
// The purpose of this class is to ensure backward compatibility of mosh profiles and command history
// TODO: This class should be probably deleted in few months or so
public static class MoshBackwardCompatibility
{
private static readonly string MoshCommandExe = $"{Constants.MoshCommandName}.exe";
private static readonly Regex OldFormatRx = new Regex(@".+\s(?<ports>\d{1,5}:\d{1,5})$", RegexOptions.Compiled);
private static readonly Regex TargetRx =
new Regex(@"(^|\s+)(?<user>[^\s@;]+)@(?<host>[^\s@:]+)(:(?<port>\d{1,5}))?\s+", RegexOptions.Compiled);
private static bool IsMoshProfile(ShellProfile profile) =>
Constants.MoshCommandName.Equals(profile.Location, StringComparison.OrdinalIgnoreCase) ||
MoshCommandExe.Equals(profile.Location, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Checks if the profile contains obsolete mosh syntax, and changes it if it does.
/// </summary>
/// <param name="profile">The profile. Note that the method doesn't change the input profile at all, but creates and
/// returns its clone if there's something to change.</param>
/// <remarks>
/// The method actually checks if <see cref="ShellProfile.Arguments"/> of the input <paramref name="profile"/> is in
/// obsolete <c>mosh</c> format <c>[ssh_arguments] [target] [mosh_port_range]</c>, and if it is changes it to the new
/// format <c>--ssh="[ssh_arguments]" -p [mosh_ports] [target]</c>.
/// </remarks>
/// <returns>
/// If the method detected that the input profile is obsolete mosh profile, it will create its clone with the updated
/// command line arguments, and return it. Otherwise it will simply return unchanged input profile (the same reference
/// as the input profile). It allows us to simply check if anything is changed by comparing the input and output
/// profiles for reference equality.
/// </returns>
public static ShellProfile FixProfile(ShellProfile profile)
{
// If there aren't any arguments, or if the profile isn't a mosh profile, there's nothing to do. Just return.
if (string.IsNullOrWhiteSpace(profile.Arguments) || !IsMoshProfile(profile)) return profile;
var arguments = profile.Arguments.Trim();
var moshPortsMatch = OldFormatRx.Match(arguments);
if (!moshPortsMatch.Success) return profile;// Arguments don't end with mosh ports (i.e. 60000:61000), so it isn't an obsolete format.
var targetMatches = TargetRx.Matches(arguments);
if (targetMatches.Count != 1) return profile;// Target part (user@host:port) not found or found multiple times. Cannot reliably determine the target, so don't change.
var targetMatch = targetMatches[0];
// Removing [mosh_ports] from the original arguments
arguments = arguments.Substring(0, arguments.Length - moshPortsMatch.Groups["ports"].Length).Trim();
var newProfile = profile.Clone();
// Removing [target] from the original arguments, thus getting "pure" ssh arguments
var sshArguments = targetMatch.Index > 0 ? arguments.Substring(0, targetMatch.Index).Trim() : string.Empty;
if (targetMatch.Index + targetMatch.Length < arguments.Length)
{
sshArguments += $" {arguments.Substring(targetMatch.Index + targetMatch.Length).Trim()}";
}
// If the ssh port is defined in the original target (i.e. user@host:port), which was allowed in the obsolete mosh, extracting the port part into a separate ssh argument
if (targetMatch.Groups["port"].Success && uint.TryParse(targetMatch.Groups["port"].Value, out var port) &&
port != SshProfile.DefaultSshPort)
{
sshArguments += $" -p {targetMatch.Groups["port"].Value}";
}
// Replacing any double quotes in ssh arguments with single quotes, since ssh arguments will be double-quoted as a whole
sshArguments = sshArguments.Replace("\"", "'").Trim();
arguments = $"-p {moshPortsMatch.Groups["ports"].Value}";
if (!string.IsNullOrEmpty(sshArguments))
{
// If there were some ssh arguments, add them as defined in the new format
arguments += $" --ssh=\"ssh {sshArguments}\"";
}
// Finally append the target to the arguments
arguments += $" {targetMatch.Groups["user"]}@{targetMatch.Groups["host"]}";
newProfile.Arguments = arguments;
return newProfile;
}
}
}
@@ -5,6 +5,9 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
namespace FluentTerminal.App.Services.Implementation
{
@@ -60,7 +63,8 @@ namespace FluentTerminal.App.Services.Implementation
TerminalOptions = GetTerminalOptions(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>()
SshProfiles = new List<SshProfile>(),
DefaultSettings = GetDefaultSettings(),
};
foreach (var theme in GetThemes().Where(x => !x.PreInstalled))
@@ -68,7 +72,7 @@ namespace FluentTerminal.App.Services.Implementation
config.Themes.Add(theme);
}
foreach (var profile in GetShellProfiles().Where(x => !x.PreInstalled))
foreach (var profile in GetShellProfiles())
{
config.Profiles.Add(profile);
}
@@ -78,7 +82,16 @@ namespace FluentTerminal.App.Services.Implementation
config.SshProfiles.Add(profile);
}
return JsonConvert.SerializeObject(config);
return JsonConvert.SerializeObject(config, PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
}
private Dictionary<string, string> GetDefaultSettings()
{
return new Dictionary<string, string>
{
[DefaultShellProfileKey] = GetDefaultShellProfileId().ToString(),
[CurrentThemeKey] = GetCurrentThemeId().ToString(),
};
}
public void ImportSettings(string serializedSettings)
@@ -90,7 +103,8 @@ namespace FluentTerminal.App.Services.Implementation
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>(),
TerminalOptions = GetTerminalOptions()
TerminalOptions = GetTerminalOptions(),
DefaultSettings = new Dictionary<string, string>(),
};
JsonConvert.PopulateObject(serializedSettings, config);
@@ -114,22 +128,18 @@ namespace FluentTerminal.App.Services.Implementation
SaveTheme(theme, existingTheme != null);
}
// Can't create pre-installed profiles
foreach (var profile in config.Profiles.Where(x => !x.PreInstalled))
foreach (var profile in config.Profiles)
{
var existingProfile = GetShellProfile(profile.Id);
var isNew = existingProfile.EqualTo(default);
var isNew = existingProfile == 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);
// those cannot be edited
profile.Name = existingProfile.Name;
profile.Location = existingProfile.Location;
SaveShellProfile(profile, false);
continue;
}
SaveShellProfile(profile, isNew);
@@ -138,41 +148,35 @@ namespace FluentTerminal.App.Services.Implementation
foreach (var profile in config.SshProfiles)
{
var existingProfile = GetSshProfile(profile.Id);
var isNew = existingProfile.EqualTo(default(SshProfile));
var isNew = existingProfile == default;
SaveSshProfile(profile, isNew);
}
SaveTerminalOptions(config.TerminalOptions);
if (config.DefaultSettings.ContainsKey(DefaultShellProfileKey))
{
SaveDefaultShellProfileId(new Guid(config.DefaultSettings[DefaultShellProfileKey]));
}
if (config.DefaultSettings.ContainsKey(CurrentThemeKey))
{
SaveCurrentThemeId(new Guid(config.DefaultSettings[CurrentThemeKey]));
}
}
public event EventHandler<ApplicationSettings> ApplicationSettingsChanged;
public event EventHandler<TerminalTheme> ThemeAdded;
public event EventHandler<Guid> CurrentThemeChanged;
public event EventHandler<Guid> ThemeDeleted;
public event EventHandler KeyBindingsChanged;
public event EventHandler<ShellProfile> ShellProfileAdded;
public event EventHandler<Guid> ShellProfileDeleted;
public event EventHandler<SshProfile> SshProfileAdded;
public event EventHandler<Guid> SshProfileDeleted;
public event EventHandler<TerminalOptions> TerminalOptionsChanged;
public void DeleteShellProfile(Guid id)
{
_shellProfiles.Delete(id.ToString());
ShellProfileDeleted?.Invoke(this, id);
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
}
public void DeleteSshProfile(Guid id)
{
_sshProfiles.Delete(id.ToString());
SshProfileDeleted?.Invoke(this, id);
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
@@ -189,7 +193,7 @@ namespace FluentTerminal.App.Services.Implementation
}
}
ThemeDeleted?.Invoke(this, id);
Messenger.Default.Send(new ThemeDeletedMessage(id));
}
public ApplicationSettings GetApplicationSettings()
@@ -231,13 +235,6 @@ 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)
{
@@ -256,14 +253,6 @@ namespace FluentTerminal.App.Services.Implementation
}
return _defaultValueProvider.GetDefaultShellProfileId();
}
public Guid GetDefaultSshProfileId()
{
if (_localSettings.TryGetValue(DefaultSshProfileKey, out object value))
{
return (Guid)value;
}
return System.Guid.Empty;
}
public IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings()
{
@@ -278,15 +267,43 @@ namespace FluentTerminal.App.Services.Implementation
public IEnumerable<ShellProfile> GetShellProfiles()
{
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string)x)).ToList();
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string) x))
.Select(MoshBackwardCompatibilityFixProfile);
}
public IEnumerable<SshProfile> GetSshProfiles()
{
if (_sshProfiles == null)
return new List<SshProfile>();
return _sshProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x))
.Select(MoshBackwardCompatibilityFixProfile).Cast<SshProfile>();
}
public IEnumerable<ShellProfile> GetAllProfiles()
{
return GetShellProfiles().Union(GetSshProfiles());
}
private ShellProfile MoshBackwardCompatibilityFixProfile(ShellProfile profile)
{
var fixedProfile = MoshBackwardCompatibility.FixProfile(profile);
if (ReferenceEquals(fixedProfile, profile))
{
// Nothing changed
return fixedProfile;
}
if (fixedProfile is SshProfile sshProfile)
{
DeleteSshProfile(profile.Id);
SaveSshProfile(sshProfile);
}
else
return _sshProfiles.GetAll()
.Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x)).ToList();
{
DeleteShellProfile(profile.Id);
SaveShellProfile(fixedProfile);
}
return fixedProfile;
}
public IEnumerable<TabTheme> GetTabThemes()
@@ -316,29 +333,32 @@ namespace FluentTerminal.App.Services.Implementation
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
}
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
{
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
ApplicationSettingsChanged?.Invoke(this, applicationSettings);
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
{
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void SaveCurrentThemeId(Guid id)
{
_roamingSettings.SetValue(CurrentThemeKey, id);
CurrentThemeChanged?.Invoke(this, id);
Messenger.Default.Send(new CurrentThemeChangedMessage(id));
}
public void SaveDefaultShellProfileId(Guid id)
{
_localSettings.SetValue(DefaultShellProfileKey, id);
}
public void SaveDefaultSshProfileId(Guid id)
{
_localSettings.SetValue(DefaultSshProfileKey, id);
Messenger.Default.Send(new DefaultShellProfileChangedMessage(id));
}
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
@@ -349,7 +369,7 @@ namespace FluentTerminal.App.Services.Implementation
}
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
@@ -357,30 +377,39 @@ namespace FluentTerminal.App.Services.Implementation
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
KeyBindingsChanged?.Invoke(this, System.EventArgs.Empty);
Messenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
ShellProfileAdded?.Invoke(this, shellProfile);
Messenger.Default.Send(new ShellProfileAddedMessage(shellProfile));
}
else
{
Messenger.Default.Send(new ShellProfileChangedMessage(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);
Messenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
SshProfileAdded?.Invoke(this, sshProfile);
Messenger.Default.Send(new ShellProfileAddedMessage(sshProfile));
}
else
{
Messenger.Default.Send(new ShellProfileChangedMessage(sshProfile));
}
}
public void SaveTerminalOptions(TerminalOptions terminalOptions)
{
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
TerminalOptionsChanged?.Invoke(this, terminalOptions);
Messenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions));
}
public void SaveTheme(TerminalTheme theme, bool newTheme = false)
@@ -389,12 +418,12 @@ namespace FluentTerminal.App.Services.Implementation
if (theme.Id == GetCurrentThemeId())
{
CurrentThemeChanged?.Invoke(this, theme.Id);
Messenger.Default.Send(new CurrentThemeChangedMessage(theme.Id));
}
if (newTheme)
{
ThemeAdded?.Invoke(this, theme);
Messenger.Default.Send(new ThemeAddedMessage(theme));
}
}
}
@@ -0,0 +1,32 @@
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services.Implementation
{
public class ShellProfileMigrationService : IShellProfileMigrationService
{
private readonly Dictionary<int, Action<ShellProfile>> _migrationSteps = new Dictionary<int, Action<ShellProfile>>
{
[1] = new Action<ShellProfile>(profile =>
{
profile.EnvironmentVariables["TERM"] = "xterm-256color";
})
};
public void Migrate(ShellProfile profile)
{
while(profile.MigrationVersion < ShellProfile.CurrentMigrationVersion)
{
ApplyMigrationStep(profile, profile.MigrationVersion + 1);
}
}
private void ApplyMigrationStep(ShellProfile profile, int targetVersion)
{
_migrationSteps[targetVersion](profile);
profile.MigrationVersion = targetVersion;
}
}
}
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services.Implementation
{
public class ThemeParserFactory : IThemeParserFactory
{
private IEnumerable<IThemeParser> _parsers;
private readonly IEnumerable<IThemeParser> _parsers;
public IEnumerable<string> SupportedFileTypes { get; }
@@ -1,4 +1,5 @@
using FluentTerminal.Models;
using FluentTerminal.App.Services.Exceptions;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Requests;
using FluentTerminal.Models.Responses;
@@ -7,14 +8,35 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App.Services.Implementation
{
public class TrayProcessCommunicationService : ITrayProcessCommunicationService
{
#region Static
private static string _userName;
private static string _sshConfigDir;
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
private static ValueSet CreateMessage(IMessage content)
{
return new ValueSet
{
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(content,
PreserveDictionaryKeyCaseContractResolver.SerializerSettings)
};
}
#endregion Static
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers;
// ReSharper disable once RedundantDefaultMemberInitializer
private byte _nextTerminalId = 0;
public event EventHandler<TerminalExitStatus> TerminalExited;
@@ -30,21 +52,7 @@ namespace FluentTerminal.App.Services.Implementation
return _nextTerminalId++;
}
public async Task<GetAvailablePortResponse> GetAvailablePort()
{
var request = new GetAvailablePortRequest();
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<GetAvailablePortResponse>((string)responseMessage[MessageKeys.Content]);
Logger.Instance.Debug("Received GetAvailablePortResponse: {@response}", response);
return response;
}
private string _userName;
public async Task<string> GetUserName()
public async Task<string> GetUserNameAsync()
{
if (!string.IsNullOrEmpty(_userName))
{
@@ -52,52 +60,56 @@ namespace FluentTerminal.App.Services.Implementation
return _userName;
}
GetUserNameResponse response;
var response = await GetResponseAsync<StringValueResponse>(new GetUserNameRequest()).ConfigureAwait(false);
// No need to crash for username, so try/catch
try
if (response.Success)
{
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new GetUserNameRequest()));
response = JsonConvert.DeserializeObject<GetUserNameResponse>((string)responseMessage[MessageKeys.Content]);
_userName = response.Value;
}
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]);
var response =
await GetResponseAsync<CommonResponse>(new SaveTextFileRequest {Path = path, Content = content})
.ConfigureAwait(false);
if (!response.Success)
{
throw new Exception(string.IsNullOrEmpty(response.Error) ? "Failed to save the file." : response.Error);
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error)
? "Failed to save the file."
: response.Error);
}
}
private static string _sshConfigDir;
public async Task<string> ReadTextFileAsync(string path)
{
var response = await GetResponseAsync<StringValueResponse>(new ReadTextFileRequest { Path = path }).ConfigureAwait(false);
if (!response.Success)
{
throw new ReadTextFileException(string.IsNullOrWhiteSpace(response.Error) ? "Failed to read the file." : response.Error);
}
return response.Value;
}
public async Task<string> GetSshConfigDirAsync()
{
if (string.IsNullOrEmpty(_sshConfigDir))
if (!string.IsNullOrEmpty(_sshConfigDir))
{
var response = await GetSshConfigFolderAsync(false);
return _sshConfigDir;
}
if (response?.Success ?? false)
{
_sshConfigDir = response.Path;
}
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{ IncludeContent = false }).ConfigureAwait(false);
if (response.Success)
{
_sshConfigDir = response.Path;
}
return _sshConfigDir;
@@ -105,45 +117,40 @@ namespace FluentTerminal.App.Services.Implementation
public async Task<string[]> GetFilesFromSshConfigDirAsync()
{
var response = await GetSshConfigFolderAsync(false);
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{IncludeContent = true}).ConfigureAwait(false);
if (response == null || !response.Success)
{
return null;
}
if (string.IsNullOrEmpty(_sshConfigDir))
if (response.Success)
{
_sshConfigDir = response.Path;
return response.Files;
}
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]);
return null;
}
public async Task<bool> CheckFileExistsAsync(string path)
{
var responseMessage =
await _appServiceConnection.SendMessageAsync(CreateMessage(new CheckFileExistsRequest {Path = path}));
var response = await GetResponseAsync<CommonResponse>(new CheckFileExistsRequest {Path = path})
.ConfigureAwait(false);
return JsonConvert.DeserializeObject<CommonResponse>((string) responseMessage[MessageKeys.Content]).Success;
return response.Success;
}
public void MuteTerminal(bool mute)
public Task MuteTerminalAsync(bool mute)
{
_appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest { Mute = mute }));
return _appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest {Mute = mute}));
}
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
public void UpdateSettings(ApplicationSettings settings)
{
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
}
public async Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType)
{
var request = new CreateTerminalRequest
{
@@ -155,14 +162,19 @@ namespace FluentTerminal.App.Services.Implementation
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>((string)responseMessage[MessageKeys.Content]);
var response = await GetResponseAsync<CreateTerminalResponse>(request).ConfigureAwait(false);
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
return response;
}
public Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause)
{
return GetResponseAsync<PauseTerminalOutputResponse>(
new PauseTerminalOutputRequest {Id = id, Pause = pause});
}
public void Initialize(IAppServiceConnection appServiceConnection)
{
_appServiceConnection = appServiceConnection;
@@ -188,7 +200,7 @@ namespace FluentTerminal.App.Services.Implementation
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId);
}
break;
case TerminalExitedRequest.Identifier:
case (byte) MessageIdentifiers.TerminalExitedRequest:
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent);
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
@@ -200,15 +212,10 @@ namespace FluentTerminal.App.Services.Implementation
}
}
public Task ResizeTerminal(byte id, TerminalSize size)
public Task ResizeTerminalAsync(byte id, TerminalSize size)
{
var request = new ResizeTerminalRequest
{
TerminalId = id,
NewSize = size
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
return _appServiceConnection.SendMessageAsync(CreateMessage(new ResizeTerminalRequest
{TerminalId = id, NewSize = size}));
}
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
@@ -216,19 +223,23 @@ namespace FluentTerminal.App.Services.Implementation
_terminalOutputHandlers[terminalId] = callback;
}
public Task UpdateToggleWindowKeyBindings()
public void UnsubscribeFromTerminalOutput(byte id)
{
if (_terminalOutputHandlers.ContainsKey(id))
{
_terminalOutputHandlers.Remove(id);
}
}
public Task UpdateToggleWindowKeyBindingsAsync()
{
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
var request = new SetToggleWindowKeyBindingsRequest
{
KeyBindings = keyBindings
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
return _appServiceConnection.SendMessageAsync(CreateMessage(new SetToggleWindowKeyBindingsRequest
{KeyBindings = keyBindings}));
}
public Task Write(byte id, byte[] data)
public Task WriteAsync(byte id, byte[] data)
{
var message = new ValueSet
{
@@ -240,7 +251,7 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(message);
}
public Task CloseTerminal(byte terminalId)
public Task CloseTerminalAsync(byte terminalId)
{
var request = new TerminalExitedRequest(terminalId, -1);
@@ -249,13 +260,53 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
private ValueSet CreateMessage(IMessage content)
public async Task<string> GetCommandPathAsync(string command)
{
return new ValueSet
if (string.IsNullOrWhiteSpace(command))
{
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
};
throw new ArgumentException("Input value is null or empty.", nameof(command));
}
command = command.Trim();
var commandLower = command.ToLowerInvariant();
if (CommandPaths.TryGetValue(commandLower, out var path))
{
return path;
}
if (CommandErrors.TryGetValue(commandLower, out var error))
{
throw new Exception(error);
}
var response = await GetResponseAsync<StringValueResponse>(new GetCommandPathRequest {Command = command})
.ConfigureAwait(false);
if (response.Success)
{
CommandPaths[commandLower] = response.Value;
return response.Value;
}
CommandErrors[commandLower] = response.Error;
throw new Exception(response.Error);
}
public Task QuitApplicationAsync()
{
return _appServiceConnection.SendMessageAsync(CreateMessage(new QuitApplicationRequest()));
}
private async Task<T> GetResponseAsync<T>(IMessage request)
{
var messageResponse =
await _appServiceConnection.SendMessageAsync(CreateMessage(request)).ConfigureAwait(false);
return JsonConvert.DeserializeObject<T>((string)messageResponse[MessageKeys.Content]);
}
}
}
@@ -17,9 +17,9 @@ namespace FluentTerminal.App.Services.Implementation
_notificationService = notificationService;
}
public async Task CheckForUpdate(bool notifyNoUpdate = false)
public async Task CheckForUpdateAsync(bool notifyNoUpdate = false)
{
var latest = await GetLatestVersionAsync();
var latest = await GetLatestVersionAsync().ConfigureAwait(false);
if (latest > GetCurrentVersion())
{
_notificationService.ShowNotification("Update available",
@@ -42,7 +42,7 @@ namespace FluentTerminal.App.Services.Implementation
var restClient = new RestClient(apiEndpoint);
var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET);
var restResponse = await restClient.ExecuteTaskAsync(restRequest);
var restResponse = await restClient.ExecuteAsync(restRequest).ConfigureAwait(false);
if (restResponse.IsSuccessful)
{
dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content);
+53 -27
View File
@@ -2,6 +2,7 @@
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Text;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
@@ -10,14 +11,23 @@ namespace FluentTerminal.App.Services
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
private bool _closingFromUI = false;
private bool _exited = false;
private bool _closingFromUi;
private bool _exited;
private readonly bool _requireShellProcessStart;
private string _fallbackTitle;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService)
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
Id = _trayProcessCommunicationService.GetNextTerminalId();
Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId();
_requireShellProcessStart = !terminalId.HasValue;
}
public void Reconnect()
{
_exited = false;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
}
private void OnTerminalExited(object sender, TerminalExitStatus status)
@@ -30,10 +40,12 @@ namespace FluentTerminal.App.Services
_exited = true;
Exited?.Invoke(this, status.ExitCode);
if (_closingFromUI == true || status.ExitCode <= 0)
if (_closingFromUi || status.ExitCode <= 0)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
_trayProcessCommunicationService.TerminalExited -= OnTerminalExited;
}
public event EventHandler<int> Exited;
@@ -63,22 +75,23 @@ namespace FluentTerminal.App.Services
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public byte Id { get; }
public ShellProfile Profile { get; private set; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
public Task CloseAsync()
{
if (_exited)
{
Closed?.Invoke(this, System.EventArgs.Empty);
return;
return Task.CompletedTask;
}
_closingFromUI = true;
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
_closingFromUi = true;
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
return _trayProcessCommunicationService.CloseTerminalAsync(Id);
}
/// <summary>
@@ -87,7 +100,7 @@ namespace FluentTerminal.App.Services
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback();
return _selectedTextCallback?.Invoke();
}
/// <summary>
@@ -109,9 +122,9 @@ namespace FluentTerminal.App.Services
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
public async Task SetSizeAsync(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
await _trayProcessCommunicationService.ResizeTerminalAsync(Id, size).ConfigureAwait(false);
SizeChanged?.Invoke(this, size);
}
@@ -122,33 +135,46 @@ namespace FluentTerminal.App.Services
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
title = _fallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// 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)
public async Task<TerminalResponse> StartShellProcessAsync(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
{
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
Profile = shellProfile;
if (response.Success)
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState));
}
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
if (_requireShellProcessStart)
{
var response = await _trayProcessCommunicationService
.CreateTerminalAsync(Id, size, shellProfile, sessionType).ConfigureAwait(false);
if (response.Success)
{
_fallbackTitle = response.Name;
SetTitle(_fallbackTitle);
}
return response;
}
else
{
return await _trayProcessCommunicationService.PauseTerminalOutputAsync(Id, false);
}
return response;
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.Write(Id, data);
return _trayProcessCommunicationService.WriteAsync(Id, data);
}
/// <summary>
+10 -2
View File
@@ -8,10 +8,18 @@ namespace FluentTerminal.App.Services.Utilities
/// </summary>
public static class I18N
{
private static readonly ResourceLoader ResourceLoader = ResourceLoader.GetForViewIndependentUse();
public static string Translate(string resource)
{
var resourceLoader = ResourceLoader.GetForCurrentView();
return resourceLoader.GetString(resource.Replace('.', '/'));
return ResourceLoader.GetString(resource.Replace('.', '/'));
}
public static string TranslateWithFallback(string resource, string fallback)
{
var result = Translate(resource);
return string.IsNullOrEmpty(result) ? fallback : result;
}
}
}
@@ -0,0 +1,21 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace FluentTerminal.App.Services.Utilities
{
public class PreserveDictionaryKeyCaseContractResolver : DefaultContractResolver
{
public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
{ ContractResolver = new PreserveDictionaryKeyCaseContractResolver() };
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
contract.DictionaryKeyResolver = key => key;
return contract;
}
}
}
@@ -5,7 +5,7 @@ using System.Reflection;
namespace FluentTerminal.App.Services.Utilities
{
public class TerminalThemeContractResolver : DefaultContractResolver
public class TerminalThemeContractResolver : PreserveDictionaryKeyCaseContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
@@ -24,4 +24,4 @@ namespace FluentTerminal.App.Services.Utilities
return property;
}
}
}
}
@@ -0,0 +1,101 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
internal class DelayedHistorySaver : ISessionSuccessTracker
{
private const int DelayMilliseconds = 5000;
private readonly Action _saveAction;
private readonly object _lock = new object();
private CancellationTokenSource _cts;
private bool _sessionStarted;
private bool _outputReceived;
private bool _toSave = true;
internal DelayedHistorySaver(Action saveAction)
{
_saveAction = saveAction;
}
public void SetSuccessfulSessionStart()
{
lock (_lock)
{
if (!_toSave || _sessionStarted)
{
return;
}
_sessionStarted = true;
if (_outputReceived)
{
InitiateSave();
}
}
}
public void SetOutputReceived()
{
lock (_lock)
{
if (!_toSave || _outputReceived)
{
return;
}
_outputReceived = true;
if (_sessionStarted)
{
InitiateSave();
}
}
}
public void SetInvalid()
{
lock (_lock)
{
if (!_toSave || !(_cts is CancellationTokenSource cts))
{
return;
}
_toSave = false;
cts.Cancel();
cts.Dispose();
_cts = null;
}
}
private void InitiateSave()
{
_cts = new CancellationTokenSource();
Task.Delay(DelayMilliseconds, _cts.Token).ContinueWith(t =>
{
lock (_lock)
{
if (_toSave)
{
_saveAction.Invoke();
}
_toSave = false;
_cts?.Dispose();
_cts = null;
}
});
}
}
}
@@ -0,0 +1,25 @@
using GalaSoft.MvvmLight;
using System;
namespace FluentTerminal.App.ViewModels
{
public class EnvironmentVariableViewModel : ViewModelBase
{
private string _name;
private string _value;
public Guid Id { get; } = Guid.NewGuid();
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public string Value
{
get => _value;
set => Set(ref _value, value);
}
}
}
@@ -1,12 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fleck" Version="1.1.0" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
<PackageReference Include="MvvmLightLibsStd10" Version="5.4.1.1" />
</ItemGroup>
<ItemGroup>
@@ -14,4 +13,11 @@
<ProjectReference Include="..\FluentTerminal.Models\FluentTerminal.Models.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.UniversalApiContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.UniversalApiContract\5.0.0.0\Windows.Foundation.UniversalApiContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
namespace FluentTerminal.App.ViewModels
{
public interface ISessionSuccessTracker
{
void SetSuccessfulSessionStart();
void SetOutputReceived();
void SetInvalid();
}
}
@@ -0,0 +1,22 @@
using System;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public interface ITerminalView : IDisposable
{
Task ChangeFontSize(int fontSize);
Task ChangeThemeAsync(TerminalTheme theme);
Task ChangeKeyBindingsAsync();
Task ChangeOptionsAsync(TerminalOptions options);
Task InitializeAsync(TerminalViewModel viewModel);
Task ReconnectAsync();
void DisposalPrepare();
Task FindNextAsync(SearchRequest request);
Task FindPreviousAsync(SearchRequest request);
Task FocusTerminalAsync();
Task<string> SerializeXtermStateAsync();
void Paste(string text);
}
}
@@ -35,6 +35,7 @@ namespace FluentTerminal.App.ViewModels.Infrastructure
{
try
{
// TODO: If we're using class field _isExecuting, maybe we should introduce some locking?
_isExecuting = true;
await _execute();
}
@@ -12,7 +12,7 @@ namespace FluentTerminal.App.ViewModels.Infrastructure
bool CanExecute();
}
public interface IAsyncCommand<T> : ICommand
public interface IAsyncCommand<in T> : ICommand
{
Task ExecuteAsync(T parameter);
bool CanExecute(T parameter);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace FluentTerminal.App.ViewModels.Menu
{
public class ExpandableMenuItemViewModel : MenuItemViewModelBase
{
public ObservableCollection<MenuItemViewModelBase> SubItems { get; }
public ExpandableMenuItemViewModel(string text, IEnumerable<MenuItemViewModelBase> subItems = null,
string description = null, Mdl2Icon icon = null) : base(text, description, icon)
{
if (subItems == null)
{
SubItems = new ObservableCollection<MenuItemViewModelBase>();
}
else if (subItems is ObservableCollection<MenuItemViewModelBase> observableSubItems)
{
SubItems = observableSubItems;
}
else
{
SubItems = new ObservableCollection<MenuItemViewModelBase>(subItems);
}
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is ExpandableMenuItemViewModel otherExpandable) ||
SubItems.Count != otherExpandable.SubItems.Count)
{
return false;
}
return !SubItems.Where((t, i) => !t.EquivalentTo(otherExpandable.SubItems[i])).Any();
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluentTerminal.App.ViewModels.Menu
{
public sealed class Mdl2Icon
{
public string Color { get; }
public string Glyph { get; }
private Mdl2Icon(string glyph, string color = null)
{
Glyph = glyph;
Color = color;
}
public static Mdl2Icon Link(string color = null) => new Mdl2Icon("\uE71B", color);
public static Mdl2Icon Copy(string color = null) => new Mdl2Icon("\uE8C8", color);
public static Mdl2Icon Paste(string color = null) => new Mdl2Icon("\uE77F", color);
public static Mdl2Icon Edit(string color = null) => new Mdl2Icon("\uE70F", color);
public static Mdl2Icon Search(string color = null) => new Mdl2Icon("\uE721", color);
public static Mdl2Icon Add(string color = null) => new Mdl2Icon("\uE710", color);
public static Mdl2Icon Cancel(string color = null) => new Mdl2Icon("\uE711", color);
public static Mdl2Icon PaginationDotOutline10(string color = null) => new Mdl2Icon("\uF126", color);
public static Mdl2Icon PaginationDotSolid10(string color = null) => new Mdl2Icon("\uF127", color);
public static Mdl2Icon NewWindow(string color = null) => new Mdl2Icon("\uE78B", color);
public static Mdl2Icon History(string color = null) => new Mdl2Icon("\uE81C", color);
public static Mdl2Icon Settings(string color = null) => new Mdl2Icon("\uE713", color);
public static Mdl2Icon Info(string color = null) => new Mdl2Icon("\uE946", color);
public static Mdl2Icon FavoriteStar(string color = null) => new Mdl2Icon("\uE734", color);
public static Mdl2Icon Globe(string color = null) => new Mdl2Icon("\uE774", color);
public static Mdl2Icon Play(string color = null) => new Mdl2Icon("\uE768", color);
}
}
@@ -0,0 +1,159 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using Windows.System;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemKeyBindingViewModel : ViewModelBase
{
private int _key;
public int Key
{
get => _key;
set => Set(ref _key, value);
}
private int _keyModifiers;
// Corresponds to https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers
public int KeyModifiers
{
get => _keyModifiers;
private set => Set(ref _keyModifiers, value);
}
private bool _ctrl;
public bool Ctrl
{
get => _ctrl;
set
{
if (Set(ref _ctrl, value))
{
SetKeyModifiers();
}
}
}
private bool _alt;
public bool Alt
{
get => _alt;
set
{
if (Set(ref _alt, value))
{
SetKeyModifiers();
}
}
}
private bool _shift;
public bool Shift
{
get => _shift;
set
{
if (Set(ref _shift, value))
{
SetKeyModifiers();
}
}
}
private bool _windows;
public bool Windows
{
get => _windows;
set
{
if (Set(ref _windows, value))
{
SetKeyModifiers();
}
}
}
public bool IsExtendedVirtualKey => !Enum.IsDefined(typeof(VirtualKey), Key);
public string GetOverrideText()
{
var segments = new List<string>();
if (Ctrl) segments.Add("Ctrl");
if (Alt) segments.Add("Alt");
if (Windows) segments.Add("Windows");
if (Shift) segments.Add("Shift");
segments.Add(EnumHelper.GetEnumDescription((ExtendedVirtualKey)Key));
return string.Join("+", segments);
}
public MenuItemKeyBindingViewModel(int key = 0, bool ctrl = false, bool alt = false, bool shift = false,
bool windows = false)
{
_key = key;
_ctrl = ctrl;
_alt = alt;
_shift = shift;
_windows = windows;
SetKeyModifiers();
}
public MenuItemKeyBindingViewModel(KeyBinding keyBinding) : this(keyBinding.Key, keyBinding.Ctrl,
keyBinding.Alt, keyBinding.Shift, keyBinding.Meta)
{
}
private void SetKeyModifiers()
{
// Based on VirtualKeyModifiers enum values (https://docs.microsoft.com/en-us/uwp/api/windows.system.virtualkeymodifiers)
var result = 0;
if (_ctrl)
{
result |= 1;
}
if (_alt)
{
result |= 2;
}
if (_shift)
{
result |= 4;
}
if (_windows)
{
result |= 8;
}
KeyModifiers = result;
}
public bool EquivalentTo(MenuItemKeyBindingViewModel other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
return _key == other._key && _keyModifiers == other._keyModifiers;
}
}
}
@@ -0,0 +1,46 @@
using System;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemViewModel : MenuItemViewModelBase
{
#region Properties
private MenuItemKeyBindingViewModel _keyBinding;
public MenuItemKeyBindingViewModel KeyBinding
{
get => _keyBinding;
set => Set(ref _keyBinding, value);
}
public ICommand Command { get; }
#endregion Properties
#region Constructors
public MenuItemViewModel(string text, ICommand command, string description = null, Mdl2Icon icon = null,
MenuItemKeyBindingViewModel keyBinding = null) : base(text, description, icon)
{
_keyBinding = keyBinding;
Command = command ?? throw new ArgumentNullException(nameof(command));
}
#endregion Constructors
#region Methods
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is MenuItemViewModel menuItem)) return false;
return _keyBinding?.EquivalentTo(menuItem._keyBinding) ?? menuItem._keyBinding == null;
}
#endregion Methods
}
}
@@ -0,0 +1,61 @@
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemViewModelBase : ViewModelBase
{
#region Properties
private string _text;
public string Text
{
get => _text;
set => Set(ref _text, value);
}
private string _description;
public string Description
{
get => _description;
set => Set(ref _description, value);
}
private Mdl2Icon _icon;
public Mdl2Icon Icon
{
get => _icon;
set => Set(ref _icon, value);
}
#endregion Properties
#region Constructor
protected MenuItemViewModelBase(string text, string description, Mdl2Icon icon)
{
_text = text;
_description = description;
_icon = icon;
}
#endregion Constructor
#region Methods
public virtual bool EquivalentTo(MenuItemViewModelBase other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
return _text.NullableEqualTo(other._text) && _description.NullableEqualTo(other._description) &&
(_icon?.Equals(other._icon) ?? other._icon == null);
}
#endregion Methods
}
}
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuViewModel : ViewModelBase
{
public ObservableCollection<MenuItemViewModelBase> Items { get; }
public MenuViewModel(IEnumerable<MenuItemViewModelBase> items = null)
{
if (items == null)
{
Items = new ObservableCollection<MenuItemViewModelBase>();
}
else if (items is ObservableCollection<MenuItemViewModelBase> observableItems)
{
Items = observableItems;
}
else
{
Items = new ObservableCollection<MenuItemViewModelBase>(items);
}
}
public bool EquivalentTo(MenuViewModel other)
{
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
if (Items.Count != other.Items.Count) return false;
return !Items.Where((t, i) => !t.EquivalentTo(other.Items[i])).Any();
}
}
}
@@ -0,0 +1,28 @@
using System;
namespace FluentTerminal.App.ViewModels.Menu
{
public class RadioMenuItemViewModel : MenuItemViewModelBase
{
public object BindingSource { get; }
public string BindingPath { get; }
public string GroupName { get; }
public RadioMenuItemViewModel(string text, string groupName, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null)
: base(text, description, icon)
{
GroupName = groupName ?? throw new ArgumentNullException(nameof(groupName));
BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource));
BindingPath = bindingPath;
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is RadioMenuItemViewModel radioMenuItem)) return false;
return GroupName == radioMenuItem.GroupName && BindingSource == radioMenuItem.BindingSource && BindingPath == radioMenuItem.BindingPath;
}
}
}
@@ -0,0 +1,15 @@
namespace FluentTerminal.App.ViewModels.Menu
{
public class SeparatorMenuItemViewModel : MenuItemViewModelBase
{
public SeparatorMenuItemViewModel()
: base(string.Empty, string.Empty, null)
{
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
return other is SeparatorMenuItemViewModel;
}
}
}
@@ -0,0 +1,26 @@
using System;
namespace FluentTerminal.App.ViewModels.Menu
{
public class ToggleMenuItemViewModel : MenuItemViewModelBase
{
public object BindingSource { get; }
public string BindingPath { get; }
public ToggleMenuItemViewModel(string text, object bindingSource, string bindingPath = null, string description = null, Mdl2Icon icon = null)
: base(text, description, icon)
{
BindingSource = bindingSource ?? throw new ArgumentNullException(nameof(bindingSource));
BindingPath = bindingPath;
}
public override bool EquivalentTo(MenuItemViewModelBase other)
{
if (!base.EquivalentTo(other)) return false;
if (!(other is ToggleMenuItemViewModel toggleMenuItem)) return false;
return BindingSource == toggleMenuItem.BindingSource && BindingPath == toggleMenuItem.BindingPath;
}
}
}
@@ -1,20 +1,19 @@
using FluentTerminal.App.Services;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight;
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.ViewModels
{
public class OverlayViewModel : ViewModelBase
{
private readonly IDispatcherTimer _overlayTimer;
private readonly DispatcherTimer _overlayTimer;
private bool _showOverlay;
private string _overlayContent;
public OverlayViewModel(IDispatcherTimer dispatcherTimer)
// Important! The constructor has to be called from the UI thread.
public OverlayViewModel()
{
_overlayTimer = dispatcherTimer;
_overlayTimer.Interval = new TimeSpan(0, 0, 2);
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
_overlayTimer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 2)};
}
public bool ShowOverlay
@@ -37,13 +36,16 @@ namespace FluentTerminal.App.ViewModels
if (_overlayTimer.IsEnabled)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
}
_overlayTimer.Start();
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
}
private void OnResizeOverlayTimerFinished(object sender, object e)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
ShowOverlay = false;
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.App.Services;
@@ -14,10 +15,9 @@ using GalaSoft.MvvmLight.Command;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Base class for all shell profile view models. Implements logic for saving, editing, resetting, etc.
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class ShellProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
public abstract class ProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
{
#region Fields
@@ -25,6 +25,7 @@ namespace FluentTerminal.App.ViewModels
protected readonly ISettingsService SettingsService;
private List<KeyBinding> _bindingsBeforeEdit;
private Dictionary<string, string> _environmentVariablesBeforeEdit;
private string _nameBeforeEdit;
#endregion Fields
@@ -53,12 +54,15 @@ namespace FluentTerminal.App.ViewModels
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<EnvironmentVariableViewModel> EnvironmentVariables { get; } = new ObservableCollection<EnvironmentVariableViewModel>();
public T ProfileVm { get; protected set; }
#endregion Properties
#region Events
// Needs to be triggered from UI thread
public event EventHandler Deleted;
#endregion Events
@@ -79,7 +83,7 @@ namespace FluentTerminal.App.ViewModels
#region Constructor
protected ShellProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
protected ProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
{
SettingsService = settingsService;
DialogService = dialogService;
@@ -87,11 +91,20 @@ namespace FluentTerminal.App.ViewModels
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), dialogService, string.Empty, false);
foreach (var environmentVariable in shellProfile.EnvironmentVariables)
{
EnvironmentVariables.Add(new EnvironmentVariableViewModel
{
Name = environmentVariable.Key,
Value = environmentVariable.Value
});
}
Initialize(shellProfile);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
DeleteCommand = new AsyncCommand(DeleteAsync, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
CancelEditCommand = new AsyncCommand(CancelEditAsync);
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
}
@@ -100,6 +113,7 @@ namespace FluentTerminal.App.ViewModels
#region Methods
// Requires UI thread
// Loads view model properties from the input shellProfile
protected void Initialize(ShellProfile shellProfile)
{
@@ -115,14 +129,17 @@ namespace FluentTerminal.App.ViewModels
protected virtual bool HasChanges()
{
return ProfileVm.HasChanges() || !_nameBeforeEdit.NullableEqualTo(_name) ||
!(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ??
false);
return ProfileVm.HasChanges()
|| !_nameBeforeEdit.NullableEqualTo(_name)
|| !(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ?? false)
|| !(_environmentVariablesBeforeEdit?.SequenceEqual(EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value)) ?? false);
}
private async Task Delete()
// Requires UI thread
private async Task DeleteAsync()
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
// ConfigureAwait(true) because we need to trigger Deleted event from the calling (UI) thread.
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
@@ -141,50 +158,71 @@ namespace FluentTerminal.App.ViewModels
}
_bindingsBeforeEdit = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
_environmentVariablesBeforeEdit = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
_nameBeforeEdit = _name;
KeyBindings.Editable = true;
InEditMode = true;
}
private async Task CancelEdit()
// Requires UI thread
private async Task CancelEditAsync()
{
if (IsNew)
{
await Delete();
await DeleteAsync().ConfigureAwait(false);
return;
}
else
if (!HasChanges())
{
if (HasChanges())
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
KeyBindings.Editable = false;
InEditMode = false;
if (result == DialogButton.OK)
{
// Cancelled, so rollback
ProfileVm.RejectChanges();
return;
}
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
// ConfigureAwait(true) because we're setting some view-model properties afterwards
if (await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true) == DialogButton.OK)
{
// Cancelled, so rollback
// ConfigureAwait(true) because we're setting some view-model properties afterwards
await ProfileVm.RejectChangesAsync().ConfigureAwait(true);
KeyBindings.Editable = false;
InEditMode = false;
}
}
public virtual async Task SaveChangesAsync()
// Requires UI thread
private async Task SaveChangesAsync()
{
var error = await ProfileVm.AcceptChangesAsync();
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var error = await ProfileVm.AcceptChangesAsync().ConfigureAwait(true);
if (!string.IsNullOrEmpty(error))
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), error, DialogButton.OK);
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"), error, DialogButton.OK)
.ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Any(string.IsNullOrWhiteSpace))
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK).ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK).ConfigureAwait(false);
return;
}
@@ -194,6 +232,7 @@ namespace FluentTerminal.App.ViewModels
profile.Id = Id;
profile.Name = _name;
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
profile.EnvironmentVariables = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
if (profile is SshProfile sshProfile)
{
@@ -209,9 +248,9 @@ namespace FluentTerminal.App.ViewModels
IsNew = false;
}
public Task AddKeyboardShortcut()
private Task AddKeyboardShortcut()
{
return KeyBindings.ShowAddKeyBindingDialog();
return KeyBindings.ShowAddKeyBindingDialogAsync();
}
#endregion Methods
@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.ViewModels.Profiles
{
public class ExecutedCommand
{
public string Command { get; set; }
public string Args { get; set; }
public DateTime LastExecution { get; set; }
public int ExecutionCount { get; set; }
}
public class ExecutedCommandHistory
{
public List<ExecutedCommand> ExecutedCommands { get; set; }
}
}
@@ -59,7 +59,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
original)
true, original)
{
_fileSystemService = fileSystemService;
@@ -88,9 +88,9 @@ namespace FluentTerminal.App.ViewModels.Profiles
Initialize(profile);
}
protected override void CopyToProfile(ShellProfile profile)
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
base.CopyToProfile(profile);
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
profile.Location = _location;
profile.Arguments = _arguments;
@@ -99,7 +99,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync();
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
@@ -123,18 +123,22 @@ namespace FluentTerminal.App.ViewModels.Profiles
!Model.Arguments.NullableEqualTo(_arguments);
}
// Requires UI thread
private async Task BrowseForCustomShell()
{
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var file = await _fileSystemService.OpenFileAsync(new[] { ".exe" }).ConfigureAwait(true);
if (file != null)
{
Location = file.Path;
}
}
// Requires UI thread
private async Task BrowseForWorkingDirectory()
{
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var directory = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (directory != null)
{
WorkingDirectory = directory;
@@ -4,9 +4,10 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Windows.UI.Core;
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels.Profiles
@@ -17,17 +18,11 @@ namespace FluentTerminal.App.ViewModels.Profiles
/// </summary>
public abstract class ProfileProviderViewModelBase : ViewModelBase
{
#region Static
private static readonly LineEndingStyle[] LineEndingStylesArray =
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
#endregion Static
#region Fields
protected readonly ISettingsService SettingsService;
protected readonly IApplicationView ApplicationView;
private readonly bool _strictProfileType;
#endregion Fields
@@ -42,7 +37,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
{
if (value == null)
{
throw new ArgumentNullException();
throw new ArgumentNullException(nameof(value));
}
if (ReferenceEquals(_model, value))
@@ -50,7 +45,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return;
}
if (_model != null && _model.GetType() != value.GetType())
if (_strictProfileType && _model != null && _model.GetType() != value.GetType())
{
throw new ArgumentException($"The value has to be of type {_model.GetType().Name}.");
}
@@ -60,21 +55,11 @@ namespace FluentTerminal.App.ViewModels.Profiles
LoadFromProfile(value);
}
}
public ObservableCollection<TabTheme> TabThemes { get; }
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
private LineEndingStyle _lineEndingTranslation;
public LineEndingStyle LineEndingTranslation
{
get => _lineEndingTranslation;
set => Set(ref _lineEndingTranslation, value);
}
private TabTheme _selectedTabTheme;
public TabTheme SelectedTabTheme
@@ -126,13 +111,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
if (theme == null)
{
// How to handle attempt to setting null theme?
// 1) Throw an exception:
//throw new ArgumentNullException();
// 2) Ignore the attempt:
//return;
// 3) Use default theme (probably the best):
theme = TerminalThemes.First();
return;
}
else if (!TerminalThemes.Contains(theme))
{
@@ -169,15 +148,24 @@ namespace FluentTerminal.App.ViewModels.Profiles
set => Set(ref _useConPty, value);
}
private bool _useBuffer;
public bool UseBuffer
{
get => _useBuffer;
set => Set(ref _useBuffer, value);
}
#endregion Properties
#region Constructor
protected ProfileProviderViewModelBase(ISettingsService settingsService, IApplicationView applicationView,
ShellProfile original = null)
bool strictProfileType, ShellProfile original = null)
{
SettingsService = settingsService;
ApplicationView = applicationView;
_strictProfileType = strictProfileType;
_model = original ?? new ShellProfile();
@@ -187,12 +175,12 @@ namespace FluentTerminal.App.ViewModels.Profiles
TerminalThemes = new ObservableCollection<TerminalTheme>(settingsService.GetThemes());
TerminalThemes.Insert(0, new TerminalTheme { Id = Guid.Empty, Name = "Default" });
TerminalThemes.Insert(0, new TerminalTheme {Id = Guid.Empty, Name = "Default"});
SelectedTerminalTheme = TerminalThemes.First();
settingsService.ThemeAdded += OnThemeAdded;
settingsService.ThemeDeleted += OnThemeDeleted;
MessengerInstance.Register<ThemeAddedMessage>(this, OnThemeAdded);
MessengerInstance.Register<ThemeDeletedMessage>(this, OnThemeDeleted);
Initialize(Model);
}
@@ -201,35 +189,35 @@ namespace FluentTerminal.App.ViewModels.Profiles
#region Methods
private void OnThemeDeleted(object sender, Guid e)
private void OnThemeDeleted(ThemeDeletedMessage message)
{
ApplicationView.RunOnDispatcherThread(() =>
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(message.ThemeId));
if (theme == null)
{
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(e));
if (theme == null)
{
return;
}
return;
}
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
TerminalThemes.Remove(theme);
if (_terminalThemeId.Equals(e))
if (_terminalThemeId.Equals(message.ThemeId))
{
SelectedTerminalTheme = TerminalThemes.First();
}
}, false);
}, CoreDispatcherPriority.Low, true);
}
private void OnThemeAdded(object sender, TerminalTheme e)
private void OnThemeAdded(ThemeAddedMessage message)
{
ApplicationView.RunOnDispatcherThread(() => TerminalThemes.Add(e), false);
ApplicationView.ExecuteOnUiThreadAsync(() => TerminalThemes.Add(message.Theme), CoreDispatcherPriority.Low);
}
private void Initialize(ShellProfile profile)
{
LineEndingTranslation = profile.LineEndingTranslation;
UseConPty = profile.UseConPty;
UseBuffer = profile.UseBuffer;
TerminalThemeId = profile.TerminalThemeId;
TabThemeId = profile.TabThemeId;
}
@@ -239,12 +227,13 @@ namespace FluentTerminal.App.ViewModels.Profiles
Initialize(profile);
}
protected virtual void CopyToProfile(ShellProfile profile)
protected virtual Task CopyToProfileAsync(ShellProfile profile)
{
profile.LineEndingTranslation = _lineEndingTranslation;
profile.UseConPty = _useConPty;
profile.UseBuffer = _useBuffer;
profile.TerminalThemeId = _terminalThemeId;
profile.TabThemeId = _tabThemeId;
return Task.CompletedTask;
}
public virtual Task<string> ValidateAsync()
@@ -258,8 +247,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
/// </summary>
public virtual bool HasChanges()
{
return Model.LineEndingTranslation != _lineEndingTranslation ||
Model.UseConPty != _useConPty ||
return Model.UseConPty != _useConPty ||
Model.UseBuffer != _useBuffer ||
!Model.TerminalThemeId.Equals(_terminalThemeId) ||
Model.TabThemeId != _tabThemeId;
}
@@ -275,19 +264,19 @@ namespace FluentTerminal.App.ViewModels.Profiles
/// message returned by <see cref="ValidateAsync"/> method.</returns>
public async Task<string> AcceptChangesAsync(bool acceptIfInvalid = false)
{
var error = await ValidateAsync();
var error = await ValidateAsync().ConfigureAwait(false);
if (acceptIfInvalid || string.IsNullOrEmpty(error))
{
CopyToProfile(Model);
await CopyToProfileAsync(Model).ConfigureAwait(false);
}
return error;
}
public void RejectChanges()
public Task RejectChangesAsync()
{
ApplicationView.RunOnDispatcherThread(() => LoadFromProfile(Model), false);
return ApplicationView.ExecuteOnUiThreadAsync(() => LoadFromProfile(Model));
}
#endregion Methods
@@ -295,7 +284,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
#region Links/shortcuts related
private const string UseConPtyQueryStringName = "conpty";
private const string LineEndingQueryStringName = "lineending";
private const string UseBufferQueryStringName = "buffer";
private const string TerminalThemeIdQueryStringName = "theme";
private const string TabThemeIdQueryStringName = "tab";
@@ -335,8 +324,7 @@ URL={0}
public string GetBaseQueryString()
{
var queryString =
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
var queryString = $"{UseConPtyQueryStringName}={_useConPty}&{UseBufferQueryStringName}={_useBuffer}";
if (_tabThemeId != TabThemes.First().Id)
{
@@ -367,11 +355,11 @@ URL={0}
}
keyValue = queryStringParams.FirstOrDefault(t =>
LineEndingQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
UseBufferQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(keyValue?.Item2) && Enum.TryParse(keyValue.Item2, true, out LineEndingStyle lineEndingTranslation))
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useBuffer))
{
LineEndingTranslation = lineEndingTranslation;
UseConPty = useBuffer;
}
keyValue = queryStringParams.FirstOrDefault(t =>
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
@@ -21,11 +20,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
{
#region Static
private static readonly Regex CommandValidationRx = new Regex(@"^(?<cmd>[^\s\.]+)(\.exe)?(\s+(?<args>\S.*))?$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly string[] AcceptableCommands = { Constants.SshCommandName };
public static string GetErrorString(SshConnectionInfoValidationResult result, string separator = "; ") =>
string.Join(separator, GetErrors(result));
@@ -52,9 +46,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IFileSystemService _fileSystemService;
private readonly IApplicationDataContainer _historyContainer;
private bool _commandInputOriginal;
// To prevent validating the existence of the same file multiple times because it's kinda expensive
private string _validatedIdentityFile;
@@ -63,34 +54,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
#region Properties
private bool _commandInput;
public bool CommandInput
{
get => _commandInput;
set
{
if (Set(ref _commandInput, value) && _useMosh)
{
RaisePropertyChanged(nameof(MoshVisible));
}
}
}
#region Quick SSH properties
private string _command;
public string Command
{
get => _command;
set => Set(ref _command, value);
}
#endregion Quick SSH properties
#region Full UI properties
private string _host;
public string Host
@@ -128,13 +91,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public bool UseMosh
{
get => _useMosh;
set
{
if (Set(ref _useMosh, value) && !_commandInput)
{
RaisePropertyChanged(nameof(MoshVisible));
}
}
set => Set(ref _useMosh, value);
}
private ushort _moshPortFrom;
@@ -153,12 +110,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
set => Set(ref _moshPortTo, value);
}
#endregion Full UI properties
public bool MoshVisible => _useMosh && !_commandInput;
public ObservableCollection<string> CommandHistory { get; private set; }
#endregion Properties
#region Commands
@@ -167,66 +118,30 @@ namespace FluentTerminal.App.ViewModels.Profiles
#endregion Commands
#region Constructors
#region Constructor
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
IApplicationDataContainer historyContainer, SshProfile original = null) : base(settingsService, applicationView,
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
SshProfile original = null) : base(settingsService, applicationView, true,
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault,
RequestConPty = settingsService.GetApplicationSettings().UseConPty
})
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
_historyContainer = historyContainer;
if (string.IsNullOrEmpty(Model.Location))
{
_commandInput = settingsService.GetApplicationSettings().UseQuickSshConnectByDefault;
}
else if (string.IsNullOrEmpty(((SshProfile)Model).Host))
{
_commandInput = true;
}
else
{
_commandInput = false;
}
_commandInputOriginal = _commandInput;
FillCommandHistory();
Initialize((SshProfile)Model);
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFileAsync);
}
private SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
IApplicationDataContainer historyContainer, bool useCommandInput) : base(settingsService, applicationView,
new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
_historyContainer = historyContainer;
_commandInput = useCommandInput;
_commandInputOriginal = useCommandInput;
FillCommandHistory();
Initialize((SshProfile)Model);
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
}
#endregion Constructors
#endregion Constructor
#region Methods
// Fills the view model properties from the input sshProfile
private void Initialize(SshProfile sshProfile)
{
Command = string.IsNullOrEmpty(sshProfile.Location) ? string.Empty : $"{sshProfile.Location} {sshProfile.Arguments}";
Host = sshProfile.Host;
SshPort = sshProfile.SshPort;
@@ -234,13 +149,13 @@ namespace FluentTerminal.App.ViewModels.Profiles
if (string.IsNullOrEmpty(Username))
{
_trayProcessCommunicationService.GetUserName().ContinueWith(t =>
_trayProcessCommunicationService.GetUserNameAsync().ContinueWith(t =>
{
var username = t.Result;
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
{
ApplicationView.RunOnDispatcherThread(() => Username = username, false);
ApplicationView.ExecuteOnUiThreadAsync(() => Username = username);
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
}
@@ -251,9 +166,11 @@ namespace FluentTerminal.App.ViewModels.Profiles
MoshPortTo = sshProfile.MoshPortTo;
}
private async Task BrowseForIdentityFile()
// Requires UI thread
private async Task BrowseForIdentityFileAsync()
{
var file = await _fileSystemService.OpenFile(new[] { "*" });
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var file = await _fileSystemService.OpenFileAsync(new[] { "*" }).ConfigureAwait(true);
if (file != null)
{
IdentityFile = file.Path;
@@ -262,49 +179,43 @@ namespace FluentTerminal.App.ViewModels.Profiles
private string GetArgumentsString()
{
StringBuilder sb = new StringBuilder();
var sshArguments = GetSshArguments();
if (_sshPort != SshProfile.DefaultSshPort)
sb.Append($"-p {_sshPort:#####} ");
if (!_useMosh)
{
return string.IsNullOrWhiteSpace(sshArguments) ? $"{_username}@{_host}" : $"{sshArguments} {_username}@{_host}";
}
if (!string.IsNullOrEmpty(_identityFile))
sb.Append($"-i \"{_identityFile}\" ");
return string.IsNullOrEmpty(sshArguments)
? $"-p {_moshPortFrom}:{_moshPortTo} {_username}@{_host}"
: $"-p {_moshPortFrom}:{_moshPortTo} --ssh=\"ssh {sshArguments}\" {_username}@{_host}";
}
sb.Append($"{_username}@{_host}");
private string GetSshArguments()
{
if (string.IsNullOrEmpty(_identityFile))
{
return _sshPort == SshProfile.DefaultSshPort ? null : $"-p {_sshPort:#####}";
}
if (_useMosh)
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
return sb.ToString();
return _sshPort == SshProfile.DefaultSshPort
? $"-i \"{_identityFile}\""
: $"-p {_sshPort:#####} -i \"{_identityFile}\"";
}
protected override void LoadFromProfile(ShellProfile profile)
{
base.LoadFromProfile(profile);
CommandInput = _commandInputOriginal;
Initialize((SshProfile)profile);
}
protected override void CopyToProfile(ShellProfile profile)
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
base.CopyToProfile(profile);
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
_commandInputOriginal = _commandInput;
var sshProfile = (SshProfile) profile;
if (_commandInput)
{
CopyToProfileQuick((SshProfile)profile);
}
else
{
CopyToProfileFull((SshProfile)profile);
}
}
private void CopyToProfileFull(SshProfile sshProfile)
{
sshProfile.Location = _useMosh ? Constants.MoshCommandName : Constants.SshCommandName;
sshProfile.Arguments = GetArgumentsString();
@@ -319,55 +230,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
sshProfile.MoshPortTo = _moshPortTo;
}
private void CopyToProfileQuick(SshProfile sshProfile)
public override async Task<string> ValidateAsync()
{
var command = _command?.Trim();
if (string.IsNullOrEmpty(command))
{
sshProfile.Location = null;
sshProfile.Arguments = null;
return;
}
var match = CommandValidationRx.Match(command);
if (!match.Success)
{
// Should not happen ever because this method gets called only if validation succeeds.
throw new Exception("Invalid command.");
}
sshProfile.Location = match.Groups["cmd"].Value;
sshProfile.Arguments = match.Groups["args"].Success ? match.Groups["args"].Value.Trim() : null;
sshProfile.WorkingDirectory = null;
sshProfile.Host = null;
sshProfile.SshPort = 0;
sshProfile.Username = null;
sshProfile.IdentityFile = null;
sshProfile.UseMosh = false;
sshProfile.MoshPortFrom = 0;
sshProfile.MoshPortTo = 0;
}
public override Task<string> ValidateAsync()
{
return _commandInput ? ValidateQuickAsync() : ValidateFullAsync();
}
private async Task<string> ValidateFullAsync()
{
var error = await base.ValidateAsync();
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
return error;
}
var result = await GetSshInfoValidationResultAsync();
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
if (result == SshConnectionInfoValidationResult.Valid)
{
@@ -384,56 +256,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
return error;
}
private async Task<string> ValidateQuickAsync()
{
var error = await base.ValidateAsync();
if (!string.IsNullOrEmpty(error))
{
return error;
}
var match = CommandValidationRx.Match(_command?.Trim() ?? string.Empty);
if (!match.Success)
{
error = I18N.Translate("InvalidCommand");
return string.IsNullOrEmpty(error) ? "Invalid command." : error;
}
if (!AcceptableCommands.Any(c => c.Equals(match.Groups["cmd"].Value, StringComparison.OrdinalIgnoreCase)))
{
error = I18N.Translate("UnsupportedCommand");
return string.IsNullOrEmpty(error)
? $"Unsupported command: {match.Groups["cmd"]}."
: $"{error} {match.Groups["cmd"]}";
}
if (!match.Groups["args"].Success)
{
error = I18N.Translate("CommandArgumentsMandatory");
return string.IsNullOrEmpty(error) ? "Command arguments are missing." : error;
}
return null;
}
public override bool HasChanges()
{
// ReSharper disable once ArrangeRedundantParentheses
if (base.HasChanges() || (_commandInput != _commandInputOriginal))
{
return true;
}
if (_commandInput)
{
return !_command.NullableEqualTo($"{Model.Location} {Model.Arguments}");
}
var original = (SshProfile)Model;
return base.HasChanges() || !original.Host.NullableEqualTo(_host) || original.SshPort != _sshPort ||
@@ -442,7 +266,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
original.MoshPortFrom != _moshPortFrom || original.MoshPortTo != _moshPortTo;
}
public async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
private async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
{
var result = SshConnectionInfoValidationResult.Valid;
@@ -461,7 +285,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
}
if (!await CheckIdentityFileExistsAsync())
if (!await CheckIdentityFileExistsAsync().ConfigureAwait(false))
{
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
}
@@ -484,6 +308,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return result;
}
// Requires UI thread
private async Task<bool> CheckIdentityFileExistsAsync()
{
var identityFile = _identityFile;
@@ -503,7 +328,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
}
else
{
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync().ConfigureAwait(true);
if (string.IsNullOrEmpty(sshConfigDir))
{
@@ -513,7 +339,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
fullPath = Path.Combine(sshConfigDir, identityFile);
}
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath).ConfigureAwait(true))
{
_validatedIdentityFile = identityFile;
@@ -533,63 +360,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
#endregion Methods
#region Command history
private const string ExecutedCommandsKey = "ExecutedCommands";
private const int CommandHistoryLimit = 20;
private void FillCommandHistory()
{
var commands = _historyContainer.ReadValueFromJson<ExecutedCommandHistory>(ExecutedCommandsKey, null)
?.ExecutedCommands;
CommandHistory = commands == null
? new ObservableCollection<string>()
: new ObservableCollection<string>(commands.Select(c => $"{c.Command} {c.Args}"));
}
public void SaveCommand(string cmd, string args)
{
cmd = cmd?.Trim();
if (string.IsNullOrEmpty(cmd))
{
throw new ArgumentNullException(nameof(cmd));
}
var commandHistory =
_historyContainer.ReadValueFromJson<ExecutedCommandHistory>(ExecutedCommandsKey, null) ??
new ExecutedCommandHistory { ExecutedCommands = new List<ExecutedCommand>() };
var command = commandHistory.ExecutedCommands.FirstOrDefault(c =>
string.Equals(cmd, c.Command, StringComparison.Ordinal) && args.NullableEqualTo(c.Args));
if (command == null)
{
command = new ExecutedCommand
{ Command = cmd, Args = args, ExecutionCount = 1, LastExecution = DateTime.UtcNow };
}
else
{
command.ExecutionCount++;
command.LastExecution = DateTime.UtcNow;
commandHistory.ExecutedCommands.Remove(command);
}
commandHistory.ExecutedCommands.Insert(0, command);
while (commandHistory.ExecutedCommands.Count > CommandHistoryLimit)
{
commandHistory.ExecutedCommands.RemoveAt(CommandHistoryLimit);
}
_historyContainer.WriteValueAsJson(ExecutedCommandsKey, commandHistory);
}
#endregion Command history
#region Links/shortcuts related
// Resources:
@@ -599,11 +369,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
private const string SshUriScheme = "ssh";
private const string MoshUriScheme = "mosh";
private const string CustomSshUriScheme = "sshft";
private const string CustomUriHost = "fluent.terminal";
private const string CustomCommandQueryStringName = "cmd";
private const string CustomArgumentsQueryStringName = "args";
// Constant derived from https://man.openbsd.org/ssh
private const string IdentityFileOptionName = "IdentityFile";
@@ -612,41 +377,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
private static readonly Regex MoshRangeRx =
new Regex(@"^(?<from>\d{1,5})[:-](?<to>\d{1,5})$", RegexOptions.Compiled);
public static bool CheckScheme(Uri uri) => CheckSchemeStandard(uri) || CheckSchemeCustom(uri);
private static bool CheckSchemeStandard(Uri uri) =>
public static bool CheckScheme(Uri uri) =>
SshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase) ||
MoshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
private static bool CheckSchemeCustom(Uri uri) =>
CustomSshUriScheme.Equals(uri?.Scheme, StringComparison.OrdinalIgnoreCase);
public static SshConnectViewModel ParseUri(Uri uri, ISettingsService settingsService,
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
{
if (CheckSchemeStandard(uri))
{
return ParseUriStandard(uri, settingsService, applicationView, trayProcessCommunicationService,
fileSystemService, historyContainer);
}
if (CheckSchemeCustom(uri))
{
return ParseUriCustom(uri, settingsService, applicationView, trayProcessCommunicationService,
fileSystemService, historyContainer);
}
// Won't happen ever
throw new Exception($"Unsupported URI scheme: {uri.Scheme}");
}
private static SshConnectViewModel ParseUriStandard(Uri uri, ISettingsService settingsService,
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
{
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
fileSystemService, historyContainer, false)
fileSystemService)
{
Host = uri.Host,
UseMosh = MoshUriScheme.Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase)
@@ -708,58 +448,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
return vm;
}
private static SshConnectViewModel ParseUriCustom(Uri uri, ISettingsService settingsService,
IApplicationView applicationView, ITrayProcessCommunicationService trayProcessCommunicationService,
IFileSystemService fileSystemService, IApplicationDataContainer historyContainer)
public override async Task<Tuple<bool, string>> GetUrlAsync()
{
var vm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
fileSystemService, historyContainer, true);
// ReSharper disable once ConstantConditionalAccessQualifier
string queryString = uri.Query?.Trim();
if (string.IsNullOrEmpty(queryString))
{
return vm;
}
if (queryString.StartsWith("?", StringComparison.Ordinal))
{
queryString = queryString.Substring(1);
}
var queryStringParams = ParseParams(queryString, '&').ToList();
var cmdParam = queryStringParams.FirstOrDefault(t =>
CustomCommandQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
var argsParam = queryStringParams.FirstOrDefault(t =>
CustomArgumentsQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
vm._command =
$"{(string.IsNullOrEmpty(cmdParam?.Item2) ? Constants.SshCommandName : cmdParam.Item2)} {argsParam?.Item2}"
.Trim();
vm.LoadBaseFromQueryString(queryStringParams);
return vm;
}
public override Task<Tuple<bool, string>> GetUrlAsync()
{
return _commandInput ? GetUrlQuickAsync() : GetUrlFullAsync();
}
private async Task<Tuple<bool, string>> GetUrlFullAsync()
{
var error = await base.ValidateAsync();
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
return Tuple.Create(false, error);
}
var result = await GetSshInfoValidationResultAsync();
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
if (result != SshConnectionInfoValidationResult.Valid &&
// For links we can ignore missing username
@@ -776,7 +474,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return Tuple.Create(false, error);
}
StringBuilder sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
var sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
sb.Append("://");
@@ -833,28 +531,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
return Tuple.Create(true, sb.ToString());
}
private async Task<Tuple<bool, string>> GetUrlQuickAsync()
{
var error = await base.ValidateAsync();
if (!string.IsNullOrEmpty(error))
{
return Tuple.Create(false, error);
}
var match = CommandValidationRx.Match(_command);
if (!match.Success)
{
error = I18N.Translate("InvalidCommand");
return Tuple.Create(false, string.IsNullOrEmpty(error) ? "Invalid command." : error);
}
return Tuple.Create(true,
$"{CustomSshUriScheme}://{CustomUriHost}/?{CustomCommandQueryStringName}={Constants.SshCommandName}&{CustomArgumentsQueryStringName}={HttpUtility.UrlEncode(match.Groups["args"].Value.Trim())}&{GetBaseQueryString()}");
}
#endregion Links/shortcuts related
}
}
@@ -1,7 +1,6 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using GalaSoft.MvvmLight;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
@@ -9,18 +8,16 @@ namespace FluentTerminal.App.ViewModels.Settings
public class AboutPageViewModel : ViewModelBase
{
private const string BaseUrl = "https://github.com/felixse/FluentTerminal/releases/tag/";
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService;
private string _latestVersion;
private readonly IApplicationView _applicationView;
public AboutPageViewModel(ISettingsService settingsService, IUpdateService updateService, IApplicationView applicationView)
public AboutPageViewModel(IUpdateService updateService, IApplicationView applicationView)
{
_settingsService = settingsService;
_updateService = updateService;
_applicationView = applicationView;
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdate(true));
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdateAsync(true));
}
public IAsyncCommand CheckForUpdatesCommand { get; }
@@ -30,7 +27,7 @@ namespace FluentTerminal.App.ViewModels.Settings
get
{
var version = _updateService.GetCurrentVersion();
return ConvertVersionToString(version);
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
@@ -61,19 +58,16 @@ namespace FluentTerminal.App.ViewModels.Settings
public Task OnNavigatedTo()
{
return CheckForUpdate(false);
return CheckForUpdateAsync(false);
}
public async Task CheckForUpdate(bool notifyNoUpdate)
private async Task CheckForUpdateAsync(bool notifyNoUpdate)
{
var version = ConvertVersionToString(await _updateService.GetLatestVersionAsync());
await _applicationView.RunOnDispatcherThread(() => LatestVersion = version);
await _updateService.CheckForUpdate(notifyNoUpdate);
}
private string ConvertVersionToString(Version version)
{
return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
var version = await _updateService.GetLatestVersionAsync().ConfigureAwait(false);
await _applicationView.ExecuteOnUiThreadAsync(() =>
LatestVersion = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}")
.ConfigureAwait(false);
await _updateService.CheckForUpdateAsync(notifyNoUpdate).ConfigureAwait(false);
}
}
}
@@ -1,9 +1,9 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -23,9 +23,11 @@ namespace FluentTerminal.App.ViewModels.Settings
private bool _needsToRestart;
private readonly IApplicationLanguageService _applicationLanguageService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IFileSystemService _fileSystemService;
public GeneralPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService, ITrayProcessCommunicationService trayProcessCommunicationService)
IStartupTaskService startupTaskService, IApplicationLanguageService applicationLanguageService,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService)
{
_settingsService = settingsService;
_dialogService = dialogService;
@@ -33,10 +35,12 @@ namespace FluentTerminal.App.ViewModels.Settings
_startupTaskService = startupTaskService;
_applicationLanguageService = applicationLanguageService;
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
_applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectoryAsync);
}
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
@@ -57,26 +61,14 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public async Task OnNavigatedTo()
// Requires UI thread
public async Task OnNavigatedToAsync()
{
var startupTaskStatus = await _startupTaskService.GetStatus();
// ConfigureAwait(true) because we want to execute SetStartupTaskPropertiesForStatus from the calling (UI) thread.
var startupTaskStatus = await _startupTaskService.GetStatusAsync().ConfigureAwait(true);
SetStartupTaskPropertiesForStatus(startupTaskStatus);
}
public bool AlwaysShowTabs
{
get => _applicationSettings.AlwaysShowTabs;
set
{
if (_applicationSettings.AlwaysShowTabs != value)
{
_applicationSettings.AlwaysShowTabs = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool ShowCustomTitleInTitlebar
{
get => _applicationSettings.ShowCustomTitleInTitlebar;
@@ -156,20 +148,6 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool UseQuickSshConnectByDefault
{
get => _applicationSettings.UseQuickSshConnectByDefault;
set
{
if (_applicationSettings.UseQuickSshConnectByDefault != value)
{
_applicationSettings.UseQuickSshConnectByDefault = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool RTrimCopiedLines
{
get => _applicationSettings.RTrimCopiedLines;
@@ -195,7 +173,62 @@ namespace FluentTerminal.App.ViewModels.Settings
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
_trayProcessCommunicationService.MuteTerminal(value);
_trayProcessCommunicationService.MuteTerminalAsync(value);
}
}
}
public bool EnableLogging
{
get => _applicationSettings.EnableLogging;
set
{
if (_applicationSettings.EnableLogging != value)
{
_applicationSettings.EnableLogging = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool PrintableOutputOnly
{
get => _applicationSettings.PrintableOutputOnly;
set
{
if (_applicationSettings.PrintableOutputOnly != value)
{
_applicationSettings.PrintableOutputOnly = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public string LogDirectoryPath
{
get => _applicationSettings.LogDirectoryPath;
set
{
if (_applicationSettings.LogDirectoryPath != value)
{
_applicationSettings.LogDirectoryPath = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public bool UseConPty
{
get => _applicationSettings.UseConPty;
set
{
if (_applicationSettings.UseConPty != value)
{
_applicationSettings.UseConPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
@@ -250,22 +283,38 @@ namespace FluentTerminal.App.ViewModels.Settings
_applicationSettings.NewTerminalLocation = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(TabIsSelected));
}
}
}
public RelayCommand RestoreDefaultsCommand { get; }
public bool TabWindowCascadingAppMenu
{
get => _applicationSettings.TabWindowCascadingAppMenu;
set
{
if (_applicationSettings.TabWindowCascadingAppMenu != value)
{
_applicationSettings.TabWindowCascadingAppMenu = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
public IAsyncCommand RestoreDefaultsCommand { get; }
public IAsyncCommand BrowseLogDirectoryCommand { get; }
public bool StartupTaskEnabled
{
get => _startupTaskEnabled;
set
{
if (_startupTaskEnabled != value)
if (Set(ref _startupTaskEnabled, value))
{
_startupTaskEnabled = value;
RaisePropertyChanged(nameof(StartupTaskEnabled));
SetStartupTaskState(value);
// ReSharper disable once AssignmentIsFullyDiscarded
_ = SetStartupTaskStateAsync(value);
}
}
}
@@ -279,7 +328,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool TabIsSelected
{
get => NewTerminalLocation == NewTerminalLocation.Tab;
set { if (value) NewTerminalLocation = NewTerminalLocation.Tab; }
set => NewTerminalLocation = value ? NewTerminalLocation.Tab : NewTerminalLocation.Window;
}
public TabsPosition TabsPosition
@@ -342,15 +391,27 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool WindowIsSelected
public bool ShowTextCopied
{
get => NewTerminalLocation == NewTerminalLocation.Window;
set { if (value) NewTerminalLocation = NewTerminalLocation.Window; }
get => _applicationSettings.ShowTextCopied;
set
{
if (_applicationSettings.ShowTextCopied != value)
{
_applicationSettings.ShowTextCopied = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
private async Task RestoreDefaults()
// Requires UI thread
private async Task RestoreDefaultsAsync()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
// ConfigureAwait(true) because we're setting some view-model properties afterwards
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreGeneralSettings"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -360,7 +421,6 @@ namespace FluentTerminal.App.ViewModels.Settings
UnderlineSelectedTab = defaults.UnderlineSelectedTab;
InactiveTabColorMode = defaults.InactiveTabColorMode;
NewTerminalLocation = defaults.NewTerminalLocation;
AlwaysShowTabs = defaults.AlwaysShowTabs;
ShowNewOutputIndicator = defaults.ShowNewOutputIndicator;
EnableTrayIcon = defaults.EnableTrayIcon;
ShowCustomTitleInTitlebar = defaults.ShowCustomTitleInTitlebar;
@@ -368,9 +428,15 @@ namespace FluentTerminal.App.ViewModels.Settings
AutoFallbackToWindowsUsernameInLinks = defaults.AutoFallbackToWindowsUsernameInLinks;
RTrimCopiedLines = defaults.RTrimCopiedLines;
MuteTerminalBeeps = defaults.MuteTerminalBeeps;
EnableLogging = defaults.EnableLogging;
PrintableOutputOnly = defaults.PrintableOutputOnly;
LogDirectoryPath = defaults.LogDirectoryPath;
UseConPty = defaults.UseConPty;
ShowTextCopied = defaults.ShowTextCopied;
}
}
// Requires UI thread
private void SetStartupTaskPropertiesForStatus(StartupTaskStatus startupTaskStatus)
{
switch (startupTaskStatus)
@@ -398,22 +464,44 @@ namespace FluentTerminal.App.ViewModels.Settings
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy");
CanEnableStartupTask = false;
break;
case StartupTaskStatus.EnabledByPolicy:
StartupTaskEnabled = true;
StartupTaskErrorMessage = I18N.TranslateWithFallback("EnabledByPolicy", "Enabled by policy.");
CanEnableStartupTask = false;
break;
}
}
private async Task SetStartupTaskState(bool enabled)
// Requires UI thread
private async Task SetStartupTaskStateAsync(bool enabled)
{
StartupTaskStatus status;
if (enabled)
{
status = await _startupTaskService.EnableStartupTask();
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
status = await _startupTaskService.EnableStartupTaskAsync().ConfigureAwait(true);
}
else
{
await _startupTaskService.DisableStartupTask();
status = await _startupTaskService.GetStatus();
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
await _startupTaskService.DisableStartupTaskAsync().ConfigureAwait(true);
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
status = await _startupTaskService.GetStatusAsync().ConfigureAwait(true);
}
SetStartupTaskPropertiesForStatus(status);
}
// Requires UI thread
private async Task BrowseLogDirectoryAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var folder = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (folder != null)
{
LogDirectoryPath = folder;
}
}
}
}
@@ -17,10 +17,11 @@ namespace FluentTerminal.App.ViewModels.Settings
Model = keyBinding;
Parent = parent;
_dialogService = dialogService;
EditCommand = new AsyncCommand(Edit);
DeleteCommand = new AsyncCommand(Delete);
EditCommand = new AsyncCommand(EditAsync);
DeleteCommand = new AsyncCommand(DeleteAsync);
}
// Needs to be triggered from the UI thread
public event EventHandler Deleted;
public event EventHandler Edited;
@@ -97,8 +98,10 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public async Task<bool> Edit()
// Requires UI thread
public async Task<bool> EditAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var keyBinding = await _dialogService.ShowCreateKeyBindingDialog().ConfigureAwait(true);
if (keyBinding != null)
@@ -117,9 +120,12 @@ namespace FluentTerminal.App.ViewModels.Settings
return false;
}
private async Task Delete()
// Requires UI thread
private async Task DeleteAsync()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -14,20 +14,17 @@ namespace FluentTerminal.App.ViewModels.Settings
{
public class KeyBindingsPageViewModel : ViewModelBase
{
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private IDictionary<string, ICollection<KeyBinding>> _keyBindings;
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider, ITrayProcessCommunicationService trayProcessCommunicationService)
public KeyBindingsPageViewModel(ISettingsService settingsService, IDialogService dialogService, ITrayProcessCommunicationService trayProcessCommunicationService)
{
_settingsService = settingsService;
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_trayProcessCommunicationService = trayProcessCommunicationService;
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
AddCommand = new RelayCommand<string>(async command => await Add(command).ConfigureAwait(false));
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaultsAsync().ConfigureAwait(false));
AddCommand = new RelayCommand<string>(async command => await AddAsync(command).ConfigureAwait(false));
Initialize(_settingsService.GetCommandKeyBindings());
}
@@ -36,12 +33,12 @@ namespace FluentTerminal.App.ViewModels.Settings
public ObservableCollection<KeyBindingsViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingsViewModel>();
public RelayCommand RestoreDefaultsCommand { get; }
private async Task Add(string command)
private Task AddAsync(string command)
{
var keyBinding = KeyBindings.FirstOrDefault(k => k.Command == command);
await keyBinding?.ShowAddKeyBindingDialog();
return KeyBindings.First(k => k.Command == command).ShowAddKeyBindingDialogAsync();
}
// Requires UI thread
private void ClearKeyBindings()
{
foreach (var keyBinding in KeyBindings)
@@ -52,17 +49,16 @@ namespace FluentTerminal.App.ViewModels.Settings
KeyBindings.Clear();
}
// Requires UI thread
private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings)
{
_keyBindings = keyBindings;
ClearKeyBindings();
foreach (var value in Enum.GetValues(typeof(Command)))
{
var command = (Command)value;
var viewModel = new KeyBindingsViewModel(command.ToString(), _dialogService, I18N.Translate($"{nameof(Command)}.{command}"), true);
foreach (var keyBinding in _keyBindings[command.ToString()])
foreach (var keyBinding in keyBindings[command.ToString()])
{
viewModel.Add(keyBinding);
}
@@ -74,12 +70,15 @@ namespace FluentTerminal.App.ViewModels.Settings
private void OnEdited(string command, ICollection<KeyBinding> keyBindings)
{
_settingsService.SaveKeyBindings(command, keyBindings);
_trayProcessCommunicationService.UpdateToggleWindowKeyBindings();
_trayProcessCommunicationService.UpdateToggleWindowKeyBindingsAsync();
}
private async Task RestoreDefaults()
// Requires UI thread
private async Task RestoreDefaultsAsync()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
// ConfigureAwait(true) because we need to execute Initialize in the calling (UI) thread.
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreKeybindings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -42,21 +42,25 @@ namespace FluentTerminal.App.ViewModels.Settings
public ObservableCollection<KeyBindingViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingViewModel>();
public async Task ShowAddKeyBindingDialog()
// Requires UI thread
public async Task ShowAddKeyBindingDialogAsync()
{
var newKeyBinding = new KeyBindingViewModel(new KeyBinding { Command = Command }, _dialogService, this);
if (await newKeyBinding.Edit().ConfigureAwait(true))
// ConfigureAwait(true) because we need to execute Add method in the calling (UI) thread.
if (await newKeyBinding.EditAsync().ConfigureAwait(true))
{
Add(newKeyBinding.Model);
}
}
// Requires UI thread
public void Clear()
{
KeyBindings.Clear();
}
// Requires UI thread
public void Add(KeyBinding keyBinding)
{
var viewModel = new KeyBindingViewModel(keyBinding, _dialogService, this);
@@ -22,7 +22,7 @@ namespace FluentTerminal.App.ViewModels.Settings
_defaultValueProvider = defaultValueProvider;
_applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaultsAsync().ConfigureAwait(false));
}
public bool CopyOnSelect
@@ -71,6 +71,12 @@ namespace FluentTerminal.App.ViewModels.Settings
set { if (value) MouseRightClickAction = MouseAction.Paste; }
}
public bool MouseRightClickCopySelectionOrPasteIsSelected
{
get => MouseRightClickAction == MouseAction.CopySelectionOrPaste;
set { if (value) MouseRightClickAction = MouseAction.CopySelectionOrPaste; }
}
public MouseAction MouseMiddleClickAction
{
get => _applicationSettings.MouseMiddleClickAction;
@@ -103,11 +109,21 @@ namespace FluentTerminal.App.ViewModels.Settings
set { if (value) MouseMiddleClickAction = MouseAction.Paste; }
}
public bool MouseMiddleClickCopySelectionOrPasteIsSelected
{
get => MouseMiddleClickAction == MouseAction.CopySelectionOrPaste;
set { if (value) MouseMiddleClickAction = MouseAction.CopySelectionOrPaste; }
}
public RelayCommand RestoreDefaultsCommand { get; }
private async Task RestoreDefaults()
// Requires UI thread
private async Task RestoreDefaultsAsync()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -90,7 +90,8 @@ namespace FluentTerminal.App.ViewModels.Settings
Id = Guid.NewGuid(),
PreInstalled = false,
Name = "New profile",
KeyBindings = new List<KeyBinding>()
KeyBindings = new List<KeyBinding>(),
UseConPty = _settingsService.GetApplicationSettings().UseConPty
};
AddShellProfile(shellProfile);
@@ -103,6 +104,7 @@ namespace FluentTerminal.App.ViewModels.Settings
cloned.Id = Guid.NewGuid();
cloned.PreInstalled = false;
cloned.Name = $"Copy of {shellProfile.Name}";
cloned.KeyBindings = new List<KeyBinding>();
AddShellProfile(cloned);
}
@@ -52,14 +52,14 @@ namespace FluentTerminal.App.ViewModels.Settings
private void OnSshProfileDeleted(object sender, EventArgs e)
{
if (sender is SshProfileViewModel shellProfile)
if (sender is SshProfileViewModel sshProfile)
{
if (SelectedSshProfile == shellProfile)
if (SelectedSshProfile == sshProfile)
{
SelectedSshProfile = SshProfiles.First();
}
SshProfiles.Remove(shellProfile);
_settingsService.DeleteSshProfile(shellProfile.Id);
SshProfiles.Remove(sshProfile);
_settingsService.DeleteSshProfile(sshProfile.Id);
}
}
@@ -75,7 +75,8 @@ namespace FluentTerminal.App.ViewModels.Settings
PreInstalled = false,
Name = "New SSH profile",
KeyBindings = new List<KeyBinding>(),
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault
UseMosh = _settingsService.GetApplicationSettings().UseMoshByDefault,
RequestConPty = _settingsService.GetApplicationSettings().UseConPty
};
AddSshProfile(shellProfile);
@@ -88,6 +89,7 @@ namespace FluentTerminal.App.ViewModels.Settings
cloned.Id = Guid.NewGuid();
cloned.PreInstalled = false;
cloned.Name = $"Copy of {shellProfile.Name}";
cloned.KeyBindings = new List<KeyBinding>();
foreach (KeyBinding keyBinding in cloned.KeyBindings)
{
@@ -1,5 +1,6 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
@@ -67,6 +68,20 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool UseAcrylicBackground
{
get => _terminalOptions.UseAcrylicBackground;
set
{
if (_terminalOptions.UseAcrylicBackground != value)
{
_terminalOptions.UseAcrylicBackground = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public string FontFamily
{
get => _terminalOptions.FontFamily;
@@ -81,6 +96,20 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public int FontWeight
{
get => _terminalOptions.FontWeight;
set
{
if (_terminalOptions.FontWeight != value)
{
_terminalOptions.FontWeight = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public double BackgroundOpacity
{
get => _terminalOptions.BackgroundOpacity;
@@ -114,7 +143,6 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _terminalOptions.ScrollBackLimit.ToString();
set
{
var oldValue = _terminalOptions.ScrollBackLimit;
if (uint.TryParse(value, out uint intValue))
{
if (intValue == uint.MinValue)
@@ -135,28 +163,15 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool BoldText
public string WordSeparator
{
get => _terminalOptions.BoldText;
get => WebViewSpecialCharEncoder.Decode(_terminalOptions.WordSeparator);
set
{
if (_terminalOptions.BoldText != value)
value = WebViewSpecialCharEncoder.Encode(value);
if (_terminalOptions.WordSeparator != value)
{
_terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public bool ShowTextCopied
{
get => _terminalOptions.ShowTextCopied;
set
{
if (_terminalOptions.ShowTextCopied != value)
{
_terminalOptions.ShowTextCopied = value;
_terminalOptions.WordSeparator = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
@@ -211,9 +226,13 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
private async Task RestoreDefaults()
// Requires UI thread
private async Task RestoreDefaultsAsync()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreTerminalOptions"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK)
{
@@ -223,10 +242,10 @@ namespace FluentTerminal.App.ViewModels.Settings
ScrollBarStyle = defaults.ScrollBarStyle;
FontFamily = defaults.FontFamily;
FontSize = defaults.FontSize;
BoldText = defaults.BoldText;
FontWeight = defaults.FontWeight;
BackgroundOpacity = defaults.BackgroundOpacity;
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
ShowTextCopied = defaults.ShowTextCopied;
WordSeparator = defaults.WordSeparator;
}
}
@@ -236,10 +255,10 @@ namespace FluentTerminal.App.ViewModels.Settings
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaultsAsync().ConfigureAwait(false));
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
Sizes = Enumerable.Range(1, 72);
Sizes = Enumerable.Range(2, 72);
_terminalOptions = _settingsService.GetTerminalOptions();
}
@@ -1,11 +1,14 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.Models.Messages;
namespace FluentTerminal.App.ViewModels.Settings
{
@@ -18,30 +21,38 @@ namespace FluentTerminal.App.ViewModels.Settings
private double _backgroundOpacity;
private readonly IThemeParserFactory _themeParserFactory;
private readonly IFileSystemService _fileSystemService;
private readonly IImageFileSystemService _imageFileSystemService;
public event EventHandler<string> SelectedThemeBackgroundColorChanged;
public event EventHandler<ImageFile> SelectedThemeBackgroundImageChanged;
public event EventHandler<ThemeViewModel> SelectedThemeChanged;
public ThemesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
IThemeParserFactory themeParserFactory, IFileSystemService fileSystemService)
public ThemesPageViewModel(ISettingsService settingsService,
IDialogService dialogService,
IDefaultValueProvider defaultValueProvider,
IThemeParserFactory themeParserFactory,
IFileSystemService fileSystemService,
IImageFileSystemService imageFileSystemService)
{
_settingsService = settingsService;
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_themeParserFactory = themeParserFactory;
_fileSystemService = fileSystemService;
_imageFileSystemService = imageFileSystemService;
CreateThemeCommand = new RelayCommand(CreateTheme);
ImportThemeCommand = new RelayCommand(ImportTheme);
ImportThemeCommand = new AsyncCommand(ImportThemeAsync);
CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme);
_settingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
BackgroundOpacity = _settingsService.GetTerminalOptions().BackgroundOpacity;
var activeThemeId = _settingsService.GetCurrentThemeId();
foreach (var theme in _settingsService.GetThemes())
{
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, fileSystemService, false);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, false);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
@@ -56,7 +67,7 @@ namespace FluentTerminal.App.ViewModels.Settings
}
public RelayCommand CreateThemeCommand { get; }
public RelayCommand ImportThemeCommand { get; }
public IAsyncCommand ImportThemeCommand { get; }
public RelayCommand<ThemeViewModel> CloneCommand { get; set; }
public double BackgroundOpacity
@@ -73,11 +84,17 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_selectedTheme != null)
{
_selectedTheme.BackgroundChanged -= OnSelectedThemeBackgroundChanged;
_selectedTheme.BackgroundImageChanged -= OnSelectedThemeBackgroundImageChanged;
}
Set(ref _selectedTheme, value);
SelectedThemeChanged?.Invoke(this, _selectedTheme);
if (value != null)
{
_selectedTheme.BackgroundOpacity = BackgroundOpacity;
value.BackgroundChanged += OnSelectedThemeBackgroundChanged;
value.BackgroundImageChanged += OnSelectedThemeBackgroundImageChanged;
}
}
}
@@ -110,28 +127,49 @@ namespace FluentTerminal.App.ViewModels.Settings
AddTheme(theme);
}
private async void ImportTheme()
// Requires UI thread
private async Task ImportThemeAsync()
{
var file = await _fileSystemService.OpenFile(_themeParserFactory.SupportedFileTypes).ConfigureAwait(true);
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
var file = await _fileSystemService.OpenFileAsync(_themeParserFactory.SupportedFileTypes)
.ConfigureAwait(true);
if (file != null)
{
var parser = _themeParserFactory.GetParser(file.FileType);
if (parser == null)
{
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
await _dialogService.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"),
I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
return;
}
try
{
var theme = await parser.Parse(file.Name, file.Content).ConfigureAwait(true);
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
var exportedTheme = await parser.Import(file.Name, file.Content).ConfigureAwait(true);
AddTheme(theme);
if (!string.IsNullOrWhiteSpace(exportedTheme.EncodedImage))
{
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
var importedImage = await _imageFileSystemService
.ImportThemeImageAsync(exportedTheme.BackgroundImage, exportedTheme.EncodedImage)
.ConfigureAwait(true);
exportedTheme.BackgroundImage = importedImage;
}
var terminalTheme = new TerminalTheme(exportedTheme);
AddTheme(terminalTheme);
}
catch (Exception exception)
{
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK).ConfigureAwait(false);
await _dialogService
.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK)
.ConfigureAwait(false);
}
}
}
@@ -140,7 +178,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_settingsService.SaveTheme(theme, true);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, true);
var viewModel = new ThemeViewModel(theme, _settingsService, _dialogService, _fileSystemService, _imageFileSystemService, true);
viewModel.EditCommand.Execute(null);
viewModel.Activated += OnThemeActivated;
viewModel.Deleted += OnThemeDeleted;
@@ -180,14 +218,19 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
private void OnTerminalOptionsChanged(object sender, TerminalOptions e)
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
{
BackgroundOpacity = e.BackgroundOpacity;
BackgroundOpacity = message.TerminalOptions.BackgroundOpacity;
}
private void OnSelectedThemeBackgroundChanged(object sender, string e)
{
SelectedThemeBackgroundColorChanged?.Invoke(this, e);
}
private void OnSelectedThemeBackgroundImageChanged(object sender, ImageFile e)
{
SelectedThemeBackgroundImageChanged?.Invoke(this, e);
}
}
}
@@ -9,22 +9,21 @@ 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,
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers)
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IApplicationView applicationView,
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers,
IImageFileSystemService imageFileSystemService)
{
About = new AboutPageViewModel(settingsService, updateService, applicationView);
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, defaultValueProvider, trayProcessCommunicationService);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService);
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, trayProcessCommunicationService);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService, fileSystemService);
Profiles = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService);
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService);
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService, imageFileSystemService);
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService,
applicationView, trayProcessCommunicationService, containers.HistoryContainer);
}
public event EventHandler Closed;
public event EventHandler AboutPageRequested;
public GeneralPageViewModel General { get; }
public KeyBindingsPageViewModel KeyBindings { get; }
@@ -32,14 +31,8 @@ namespace FluentTerminal.App.ViewModels
public TerminalPageViewModel Terminal { get; }
public ThemesPageViewModel Themes { get; }
public MousePageViewModel Mouse { get; }
public AboutPageViewModel About { get; }
public SshProfilesPageViewModel SshProfiles { get; }
public void NavigateToAboutPage()
{
AboutPageRequested?.Invoke(this, EventArgs.Empty);
}
public void Close()
{
Closed?.Invoke(this, EventArgs.Empty);
@@ -11,9 +11,9 @@ using FluentTerminal.App.ViewModels.Profiles;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Extends <see cref="ShellProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
/// Extends <see cref="ProfileViewModelBase{T}"/>, and implements only "Set as default" logic.
/// </summary>
public class ShellProfileViewModel : ShellProfileViewModelBase<CommonProfileProviderViewModel>
public class ShellProfileViewModel : ProfileViewModelBase<CommonProfileProviderViewModel>
{
#region Fields
@@ -60,21 +60,23 @@ namespace FluentTerminal.App.ViewModels
shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
}
#endregion Constrcutor
#region Methods
private async Task RestoreDefaults()
// Requires UI thread
private async Task RestoreDefaultsAsync()
{
if (InEditMode || !ProfileVm.PreInstalled)
{
throw new InvalidOperationException();
}
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
// ConfigureAwait(true) because we need to execute Initialize method in the calling (UI) thread.
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
@@ -5,10 +5,10 @@ using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Extends <see cref="ShellProfileViewModelBase{T}"/>, and doesn't implement any additional logic because in
/// Extends <see cref="ProfileViewModelBase{T}"/>, and doesn't implement any additional logic because in
/// case of SSH profiles no additional logic is needed.
/// </summary>
public class SshProfileViewModel : ShellProfileViewModelBase<SshConnectViewModel>
public class SshProfileViewModel : ProfileViewModelBase<SshConnectViewModel>
{
#region Constructor
@@ -19,7 +19,7 @@ namespace FluentTerminal.App.ViewModels
isNew)
{
ProfileVm = new SshConnectViewModel(settingsService, applicationView, trayProcessCommunicationService,
fileSystemService, historyContainer, sshProfile);
fileSystemService, sshProfile);
}
#endregion Constructor
@@ -0,0 +1,31 @@
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels
{
public class TabThemeViewModel : ViewModelBase
{
private bool _isSelected;
public TabThemeViewModel(TabTheme theme, TerminalViewModel terminal)
{
Theme = theme;
Terminal = terminal;
}
public bool IsSelected
{
get => _isSelected;
set
{
if (Set(ref _isSelected, value) && value)
{
Terminal.TabTheme = this;
}
}
}
public TabTheme Theme { get; }
public TerminalViewModel Terminal { get; }
}
}
+555 -110
View File
@@ -5,12 +5,19 @@ using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Newtonsoft.Json;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.UI.Core;
using FluentTerminal.Models.Messages;
using System.Windows.Input;
using FluentTerminal.App.ViewModels.Menu;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml;
using System.Collections.Generic;
namespace FluentTerminal.App.ViewModels
{
@@ -22,28 +29,108 @@ namespace FluentTerminal.App.ViewModels
#endregion Static
#region Serialize terminal state
private class TerminalState
{
public bool HasCustomTitle { get; set; }
public string ShellTitle { get; set; }
public string TabTitle { get; set; }
public TerminalTheme TerminalTheme { get; set; }
public int TabThemeId { get; set; }
public bool ShowSearchPanel { get; set; }
public string SearchText { get; set; }
public bool SearchMatchCase { get; set; }
public bool SearchWholeWord { get; set; }
public bool SearchWithRegex { get; set; }
public string XtermBufferState { get; set; }
public byte TerminalId { get; set; }
public ShellProfile ShellProfile { get; set; }
public int FontSize { get; set; }
}
public async Task<string> SerializeAsync()
{
TerminalState state = new TerminalState
{
HasCustomTitle = _hasCustomTitle,
ShellTitle = ShellTitle,
TabTitle = TabTitle,
TerminalTheme = TerminalTheme,
TabThemeId = TabTheme.Theme.Id,
ShowSearchPanel = ShowSearchPanel,
SearchText = SearchText,
SearchMatchCase = SearchMatchCase,
SearchWholeWord = SearchWholeWord,
SearchWithRegex = SearchWithRegex,
XtermBufferState = await SerializeXtermStateAsync().ConfigureAwait(false),
TerminalId = Terminal.Id,
ShellProfile = ShellProfile,
FontSize = FontSize
};
return JsonConvert.SerializeObject(state);
}
public void Restore(string data)
{
TerminalState state = JsonConvert.DeserializeObject<TerminalState>(data);
if (state != null)
{
_hasCustomTitle = state.HasCustomTitle;
ShellTitle = state.ShellTitle;
TabTitle = state.TabTitle;
TerminalTheme = state.TerminalTheme;
TabTheme = TabThemes.FirstOrDefault(t => t.Theme.Id == state.TabThemeId);
ShowSearchPanel = state.ShowSearchPanel;
SearchText = state.SearchText;
SearchMatchCase = state.SearchMatchCase;
SearchWholeWord = state.SearchWholeWord;
SearchWithRegex = state.SearchWithRegex;
XtermBufferState = state.XtermBufferState;
_terminalId = state.TerminalId;
ShellProfile = state.ShellProfile;
FontSize = state.FontSize;
TabTheme.IsSelected = true;
}
}
#endregion Serialize terminal state
private readonly IKeyboardCommandService _keyboardCommandService;
private bool _isSelected;
private bool _isHovered;
private bool _hasNewOutput;
private bool _hasExitedWithError;
private string _searchText;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private bool _searchMatchCase;
private bool _searchWholeWord;
private bool _searchWithRegex;
private TabThemeViewModel _tabTheme;
private TerminalTheme _terminalTheme;
private TerminalOptions _terminalOptions;
private string _tabTitle;
private string _shellTitle;
private bool _hasCustomTitle;
private byte? _terminalId;
private bool _hasSelection;
private string _hoveredUri;
private MenuViewModel _contextMenu;
private MenuViewModel _tabContextMenu;
private readonly TabThemeViewModel _transparentTabThemeViewModel;
private int _fontSize;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService)
IApplicationView applicationView, IClipboardService clipboardService, string terminalState = null)
{
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
MessengerInstance.Register<KeyBindingsChangedMessage>(this, OnKeyBindingsChanged);
SettingsService = settingsService;
SettingsService.CurrentThemeChanged += OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged += OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged += OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged += OnKeyBindingsChanged;
_terminalOptions = SettingsService.GetTerminalOptions();
@@ -58,17 +145,37 @@ namespace FluentTerminal.App.ViewModels
ShellProfile = shellProfile;
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? SettingsService.GetCurrentTheme() : SettingsService.GetTheme(shellProfile.TerminalThemeId);
TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
TabThemes = new ObservableCollection<TabThemeViewModel>(SettingsService.GetTabThemes().Select(theme => new TabThemeViewModel(theme, this)));
CloseCommand = new AsyncCommand(TryCloseAsync, CanExecuteCommand);
CloseLeftTabsCommand = new RelayCommand(CloseLeftTabs, CanExecuteCommand);
CloseRightTabsCommand = new RelayCommand(CloseRightTabs, CanExecuteCommand);
CloseOtherTabsCommand = new RelayCommand(CloseOtherTabs, CanExecuteCommand);
FindNextCommand = new RelayCommand(FindNext, CanExecuteCommand);
FindPreviousCommand = new RelayCommand(FindPrevious, CanExecuteCommand);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel, CanExecuteCommand);
EditTitleCommand = new AsyncCommand(EditTitleAsync, CanExecuteCommand);
DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand);
ReconnectTabCommand = new AsyncCommand(ReconnectTabAsync, () => CanExecuteCommand() && HasExitedWithError);
CopyCommand = new AsyncCommand(Copy, () => HasSelection);
PasteCommand = new AsyncCommand(Paste);
CopyLinkCommand = new AsyncCommand(() => CopyTextAsync(HoveredUri), () => !string.IsNullOrWhiteSpace(HoveredUri));
ShowSearchPanelCommand = new RelayCommand(() => ShowSearchPanel = true, () => !ShowSearchPanel);
CloseCommand = new RelayCommand(async () => await TryClose().ConfigureAwait(false));
FindNextCommand = new RelayCommand(FindNext);
FindPreviousCommand = new RelayCommand(FindPrevious);
CloseSearchPanelCommand = new RelayCommand(CloseSearchPanel);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme);
EditTitleCommand = new AsyncCommand(EditTitle);
FontSize = _terminalOptions.FontSize;
Terminal = new Terminal(TrayProcessCommunicationService);
if (!string.IsNullOrEmpty(terminalState))
{
Restore(terminalState);
}
else
{
var defaultTabTheme = TabThemes.FirstOrDefault(t => t.Theme.Id == ShellProfile.TabThemeId);
defaultTabTheme.IsSelected = true;
}
_transparentTabThemeViewModel = TabThemes.FirstOrDefault(t => t.Theme.Id == 0);
Terminal = new Terminal(TrayProcessCommunicationService, _terminalId);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
Terminal.OutputReceived += Terminal_OutputReceived;
Terminal.SizeChanged += Terminal_SizeChanged;
@@ -76,37 +183,70 @@ namespace FluentTerminal.App.ViewModels
Terminal.Exited += Terminal_Exited;
Terminal.Closed += Terminal_Closed;
Overlay = new OverlayViewModel(dispatcherTimer);
ContextMenu = BuidContextMenu();
TabContextMenu = BuildTabContextMenu();
}
public MenuViewModel ContextMenu
{
get => _contextMenu;
set => Set(ref _contextMenu, value);
}
public MenuViewModel TabContextMenu
{
get => _tabContextMenu;
set => Set(ref _tabContextMenu, value);
}
private bool _disposalRequested;
public void DisposalPrepare()
{
_disposalRequested = true;
CloseCommand = null;
TerminalView.DisposalPrepare();
TerminalView = null;
Terminal = null;
Overlay = null;
}
public event EventHandler Activated;
public event EventHandler Closed;
public event EventHandler<string> FindNextRequested;
public event EventHandler<string> FindPreviousRequested;
public event EventHandler KeyBindingsChanged;
public event EventHandler<TerminalOptions> OptionsChanged;
public event EventHandler<SearchRequest> FindNextRequested;
public event EventHandler<SearchRequest> FindPreviousRequested;
public event EventHandler SearchStarted;
public event EventHandler<TerminalTheme> ThemeChanged;
public event EventHandler<string> ShellTitleChanged;
public event EventHandler<string> CustomTitleChanged;
public event EventHandler CloseLeftTabsRequested;
public event EventHandler CloseRightTabsRequested;
public event EventHandler CloseOtherTabsRequested;
public event EventHandler DuplicateTabRequested;
public event EventHandler<int> FontSizeChanged;
public ApplicationSettings ApplicationSettings { get; private set; }
public IApplicationView ApplicationView { get; }
public TabTheme BackgroundTabTheme
public TabThemeViewModel BackgroundTabTheme
{
// The effective background theme depends on whether it is selected (use the theme), or if it is inactive
// (if we're set to underline inactive tabs, use the null theme).
get => IsSelected || (!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Background) ?
_tabTheme :
SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
TabTheme :
_transparentTabThemeViewModel;
}
public IClipboardService ClipboardService { get; }
public RelayCommand CloseCommand { get; }
public string XtermBufferState { get; private set; }
public AsyncCommand CloseCommand { get; private set; }
public RelayCommand CloseRightTabsCommand { get; }
public RelayCommand CloseLeftTabsCommand { get; }
public RelayCommand CloseOtherTabsCommand { get; }
public RelayCommand CloseSearchPanelCommand { get; }
@@ -118,6 +258,20 @@ namespace FluentTerminal.App.ViewModels
public RelayCommand FindPreviousCommand { get; }
public RelayCommand DuplicateTabCommand { get; }
public AsyncCommand ReconnectTabCommand { get; }
public ICommand CopyLinkCommand { get; private set; }
public ICommand CopyCommand { get; private set; }
public ICommand PasteCommand { get; private set; }
public ICommand ShowSearchPanelCommand { get; private set; }
public double BackgroundOpacity => _terminalOptions?.BackgroundOpacity ?? 1.0;
public bool IsSelected
{
get => _isSelected;
@@ -127,16 +281,57 @@ namespace FluentTerminal.App.ViewModels
{
if (IsSelected)
{
_keyboardCommandService.RegisterCommandHandler(nameof(Command.Search), () => ShowSearchPanel = !ShowSearchPanel);
HasNewOutput = false;
}
else
{
_keyboardCommandService.DeregisterCommandHandler(nameof(Command.Search));
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
RaisePropertyChanged(nameof(ShowCloseButton));
}
}
}
public bool HasSelection
{
get => _hasSelection;
set => Set(ref _hasSelection, value);
}
public string HoveredUri
{
get => _hoveredUri;
set
{
if (Set(ref _hoveredUri, value))
{
ContextMenu = BuidContextMenu();
}
}
}
public bool IsHovered
{
get => _isHovered;
set
{
if (Set(ref _isHovered, value))
{
RaisePropertyChanged(nameof(ShowCloseButton));
}
}
}
public bool ShowCloseButton
{
get => IsHovered || IsSelected;
}
public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) ||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null);
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Theme.Color != null);
public bool HasNewOutput
{
@@ -153,6 +348,7 @@ namespace FluentTerminal.App.ViewModels
{
HasNewOutput = false;
}
ReconnectTabCommand.RaiseCanExecuteChanged();
}
}
@@ -162,11 +358,27 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _searchText, value);
}
public RelayCommand<string> SelectTabThemeCommand { get; }
public bool SearchMatchCase
{
get => _searchMatchCase;
set => Set(ref _searchMatchCase, value);
}
public bool SearchWholeWord
{
get => _searchWholeWord;
set => Set(ref _searchWholeWord, value);
}
public bool SearchWithRegex
{
get => _searchWithRegex;
set => Set(ref _searchWithRegex, value);
}
public ISettingsService SettingsService { get; }
public ShellProfile ShellProfile { get; }
public ShellProfile ShellProfile { get; private set; }
public bool ShowSearchPanel
{
@@ -174,7 +386,9 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _showSearchPanel, value);
}
public TabTheme TabTheme
public bool SearchHasFocus { get; set; }
public TabThemeViewModel TabTheme
{
get => _tabTheme;
set
@@ -182,14 +396,17 @@ namespace FluentTerminal.App.ViewModels
Set(ref _tabTheme, value);
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
// necessary to update this constantly so that duplicated tabs will also carry over the color
ShellProfile.TabThemeId = value.Theme.Id;
}
}
public ObservableCollection<TabTheme> TabThemes { get; }
public ObservableCollection<TabThemeViewModel> TabThemes { get; }
public Terminal Terminal { get; private set; }
public OverlayViewModel Overlay { get; private set; }
public OverlayViewModel Overlay { get; set; }
public TerminalTheme TerminalTheme
{
@@ -197,6 +414,8 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _terminalTheme, value);
}
public bool Initialized { get; set; }
public string TabTitle
{
get => _tabTitle;
@@ -223,10 +442,8 @@ namespace FluentTerminal.App.ViewModels
}
}
if (Set(ref _tabTitle, title))
{
CustomTitleChanged?.Invoke(this, title);
}
Set(ref _tabTitle, title);
ShellProfile.Name = title;
}
}
@@ -242,25 +459,43 @@ namespace FluentTerminal.App.ViewModels
}
}
public int FontSize
{
get => _fontSize;
set
{
_fontSize = value > 0 ? value : 1;
}
}
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task Close()
public Task CloseAsync()
{
SettingsService.CurrentThemeChanged -= OnCurrentThemeChanged;
SettingsService.TerminalOptionsChanged -= OnTerminalOptionsChanged;
SettingsService.ApplicationSettingsChanged -= OnApplicationSettingsChanged;
SettingsService.KeyBindingsChanged -= OnKeyBindingsChanged;
return Terminal.Close();
MessengerInstance.Unregister(this);
return Terminal.CloseAsync();
}
public void CopyText(string text)
public Task CopyTextAsync(string text)
{
ClipboardService.SetText(text);
return ApplicationView.ExecuteOnUiThreadAsync(() =>
{
ClipboardService.SetText(text);
if (ApplicationSettings.ShowTextCopied)
{
Overlay.Show(I18N.Translate("TextCopied"));
}
});
}
public async Task EditTitle()
// Requires UI thread
public async Task EditTitleAsync()
{
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"));
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"))
.ConfigureAwait(true);
if (result != null)
{
if (string.IsNullOrWhiteSpace(result))
@@ -288,64 +523,129 @@ namespace FluentTerminal.App.ViewModels
FocusTerminal();
}
private bool CanExecuteCommand<T>(T a)
{
return CanExecuteCommand();
}
private bool CanExecuteCommand()
{
return Initialized && !_disposalRequested;
}
private void CloseLeftTabs()
{
CloseLeftTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void CloseRightTabs()
{
CloseRightTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void CloseOtherTabs()
{
CloseOtherTabsRequested?.Invoke(this, EventArgs.Empty);
}
private void FindNext()
{
FindNextRequested?.Invoke(this, SearchText);
FindNextRequested?.Invoke(this, new SearchRequest { MatchCase = SearchMatchCase, Regex = SearchWithRegex, Term = SearchText, WholeWord = SearchWholeWord });
}
public ITerminalView TerminalView { get; set; }
private Task<string> SerializeXtermStateAsync()
{
return TerminalView?.SerializeXtermStateAsync() ?? Task.FromResult(string.Empty);
}
private void FindPrevious()
{
FindPreviousRequested?.Invoke(this, SearchText);
FindPreviousRequested?.Invoke(this, new SearchRequest { MatchCase = SearchMatchCase, Regex = SearchWithRegex, Term = SearchText, WholeWord = SearchWholeWord });
}
private async void OnApplicationSettingsChanged(object sender, ApplicationSettings e)
private void DuplicateTab()
{
await ApplicationView.RunOnDispatcherThread(() =>
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
}
public async Task ReconnectTabAsync()
{
HasExitedWithError = false;
await TerminalView?.ReconnectAsync();
}
private void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
{
ApplicationSettings = message.ApplicationSettings;
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
ApplicationSettings = e;
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
});
}
private async void OnCurrentThemeChanged(object sender, Guid e)
private void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
{
await ApplicationView.RunOnDispatcherThread(() =>
// only change theme if not overwritten by profile
if (ShellProfile.TerminalThemeId == Guid.Empty)
{
// only change theme if not overwritten by profile
if (ShellProfile.TerminalThemeId == Guid.Empty)
var currentTheme = SettingsService.GetTheme(message.ThemeId);
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
var currentTheme = SettingsService.GetTheme(e);
TerminalTheme = currentTheme;
ThemeChanged?.Invoke(this, currentTheme);
});
}
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
{
_terminalOptions = message.TerminalOptions;
ApplicationView.ExecuteOnUiThreadAsync(() => RaisePropertyChanged(nameof(BackgroundOpacity)), CoreDispatcherPriority.Low);
}
private void OnKeyBindingsChanged(KeyBindingsChangedMessage message)
{
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
var contextMenu = BuidContextMenu();
if (!contextMenu.EquivalentTo(ContextMenu))
{
ContextMenu = contextMenu;
}
});
}
private async void OnKeyBindingsChanged(object sender, EventArgs e)
{
await ApplicationView.RunOnDispatcherThread(() => KeyBindingsChanged?.Invoke(this, EventArgs.Empty));
}
var tabContextMenu = BuildTabContextMenu();
if (!tabContextMenu.EquivalentTo(TabContextMenu))
{
TabContextMenu = tabContextMenu;
}
private async void OnTerminalOptionsChanged(object sender, TerminalOptions e)
{
_terminalOptions = e;
await ApplicationView.RunOnDispatcherThread(() => OptionsChanged?.Invoke(this, e));
}
private void SelectTabTheme(string id)
{
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
}, CoreDispatcherPriority.Low, true);
}
private void Terminal_Exited(object sender, int exitCode)
{
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
if (ShellProfile?.Tag is ISessionSuccessTracker tracker && exitCode != 0)
{
tracker.SetInvalid();
}
ApplicationView.ExecuteOnUiThreadAsync(() => HasExitedWithError = exitCode > 0);
}
private void Terminal_Closed(object sender, EventArgs e)
{
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
Closed?.Invoke(this, EventArgs.Empty);
Terminal.KeyboardCommandReceived -= Terminal_KeyboardCommandReceived;
Terminal.OutputReceived -= Terminal_OutputReceived;
Terminal.SizeChanged -= Terminal_SizeChanged;
Terminal.TitleChanged -= Terminal_TitleChanged;
Terminal.Exited -= Terminal_Exited;
Terminal.Closed -= Terminal_Closed;
}
private async void Terminal_KeyboardCommandReceived(object sender, string e)
@@ -354,83 +654,228 @@ namespace FluentTerminal.App.ViewModels
{
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
ClipboardService.SetText(selection);
if(_terminalOptions.ShowTextCopied)
{
Overlay.Show(I18N.Translate("TextCopied"));
}
break;
await Copy().ConfigureAwait(false);
return;
}
case nameof(Command.Paste):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
await Paste().ConfigureAwait(false);
return;
}
case nameof(Command.PasteWithoutNewlines):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
string content = await ClipboardService.GetTextAsync().ConfigureAwait(false);
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
TerminalView.Paste(content);
}
break;
return;
}
case nameof(Command.Search):
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
await ApplicationView.ExecuteOnUiThreadAsync(() =>
{
SearchStarted?.Invoke(this, EventArgs.Empty);
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
{
SearchStarted?.Invoke(this, EventArgs.Empty);
}
}).ConfigureAwait(false);
return;
}
case nameof(Command.IncreaseFontSize):
{
FontSize++;
FontSizeChanged?.Invoke(this, FontSize);
return;
}
case nameof(Command.DecreaseFontSize):
{
if (FontSize > 2)
{
FontSize--;
FontSizeChanged?.Invoke(this, FontSize);
}
break;
return;
}
case nameof(Command.ResetFontSize):
{
FontSize = _terminalOptions.FontSize;
FontSizeChanged?.Invoke(this, FontSize);
return;
}
default:
{
_keyboardCommandService.SendCommand(e);
break;
await ApplicationView.ExecuteOnUiThreadAsync(() => _keyboardCommandService.SendCommand(e))
.ConfigureAwait(false);
return;
}
}
}
private void Terminal_OutputReceived(object sender, byte[] e)
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
tracker.SetOutputReceived();
}
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
ApplicationView.ExecuteOnUiThreadAsync(() => HasNewOutput = true);
}
}
private void Terminal_SizeChanged(object sender, TerminalSize e)
{
Overlay.Show($"{e.Columns} x {e.Rows}");
ApplicationView.ExecuteOnUiThreadAsync(() => Overlay.Show($"{e.Columns} x {e.Rows}"));
}
private void Terminal_TitleChanged(object sender, string e)
{
ShellTitle = e;
ShellTitleChanged?.Invoke(this, e);
if (e == Terminal.Profile.Location) return;
ApplicationView.ExecuteOnUiThreadAsync(() => ShellTitle = e);
}
private async Task TryClose()
private async Task TryCloseAsync()
{
if (ApplicationSettings.ConfirmClosingTabs)
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmCloseTab"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.Cancel)
{
return;
}
tracker.SetInvalid();
}
await Close().ConfigureAwait(true);
if (!ApplicationSettings.ConfirmClosingTabs || await DialogService
.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
string.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK,
DialogButton.Cancel).ConfigureAwait(false) == DialogButton.OK)
{
await CloseAsync().ConfigureAwait(false);
}
}
private async Task Copy()
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(false);
await CopyTextAsync(selection).ConfigureAwait(false);
}
private async Task Paste()
{
// prevent from pasting something into the terminal window when the actual command is executed
// while being in the search box
if (!SearchHasFocus)
{
var content = await ClipboardService.GetTextAsync().ConfigureAwait(false);
if (content != null)
{
TerminalView.Paste(content);
}
}
}
private MenuViewModel BuidContextMenu()
{
var commandKeyBindings = SettingsService.GetCommandKeyBindings();
var contextMenu = new MenuViewModel();
if (!string.IsNullOrWhiteSpace(HoveredUri))
{
var copyLink = new MenuItemViewModel(I18N.Translate("CopyLink"), CopyLinkCommand, icon: Mdl2Icon.Link());
contextMenu.Items.Add(copyLink);
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
}
var copy = new MenuItemViewModel(I18N.Translate("Command.Copy"), CopyCommand, icon: Mdl2Icon.Copy());
AddKeyBindings(copy, Command.Copy, commandKeyBindings);
contextMenu.Items.Add(copy);
var paste = new MenuItemViewModel(I18N.Translate("Command.Paste"), PasteCommand, icon: Mdl2Icon.Paste());
AddKeyBindings(paste, Command.Paste, commandKeyBindings);
contextMenu.Items.Add(paste);
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
var editTitle = new MenuItemViewModel(I18N.Translate("EditTitle.Text"), EditTitleCommand, icon: Mdl2Icon.Edit());
AddKeyBindings(editTitle, Command.ChangeTabTitle, commandKeyBindings);
contextMenu.Items.Add(editTitle);
var search = new MenuItemViewModel(I18N.Translate("Search.Text"), ShowSearchPanelCommand, icon: Mdl2Icon.Search());
AddKeyBindings(search, Command.Search, commandKeyBindings);
contextMenu.Items.Add(search);
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
var duplicate = new MenuItemViewModel(I18N.Translate("Command.DuplicateTab"), DuplicateTabCommand);
AddKeyBindings(duplicate, Command.DuplicateTab, commandKeyBindings);
contextMenu.Items.Add(duplicate);
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
var close = new MenuItemViewModel(I18N.Translate("Close"), CloseCommand, icon: Mdl2Icon.Cancel());
AddKeyBindings(close, Command.CloseTab, commandKeyBindings);
contextMenu.Items.Add(close);
return contextMenu;
}
private MenuViewModel BuildTabContextMenu()
{
var commandKeyBindings = SettingsService.GetCommandKeyBindings();
var contextMenu = new MenuViewModel();
RadioMenuItemViewModel createTabColorItem(TabThemeViewModel tabTheme)
{
//todo localise name
var icon = tabTheme.Theme.Id == 0 ? Mdl2Icon.PaginationDotOutline10() : Mdl2Icon.PaginationDotSolid10(tabTheme.Theme.Color);
return new RadioMenuItemViewModel(I18N.Translate($"{tabTheme.Theme.Name}"), $"{Terminal.Id}_tabTheme", tabTheme, bindingPath: nameof(TabThemeViewModel.IsSelected) , icon: icon);
}
var tabColor = new ExpandableMenuItemViewModel(I18N.Translate("Color.Text"));
foreach (var tabTheme in TabThemes)
{
tabColor.SubItems.Add(createTabColorItem(tabTheme));
}
contextMenu.Items.Add(tabColor);
var editTitle = new MenuItemViewModel(I18N.Translate("EditTitle.Text"), EditTitleCommand, icon: Mdl2Icon.Edit());
AddKeyBindings(editTitle, Command.ChangeTabTitle, commandKeyBindings);
contextMenu.Items.Add(editTitle);
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
var duplicate = new MenuItemViewModel(I18N.Translate("Command.DuplicateTab"), DuplicateTabCommand);
AddKeyBindings(duplicate, Command.DuplicateTab, commandKeyBindings);
contextMenu.Items.Add(duplicate);
contextMenu.Items.Add(new SeparatorMenuItemViewModel());
var closeLeft = new MenuItemViewModel(I18N.Translate("CloseLeft.Text"), CloseLeftTabsCommand);
contextMenu.Items.Add(closeLeft);
var closeRight = new MenuItemViewModel(I18N.Translate("CloseRight.Text"), CloseRightTabsCommand);
contextMenu.Items.Add(closeRight);
var closeOther = new MenuItemViewModel(I18N.Translate("CloseOther.Text"), CloseOtherTabsCommand);
contextMenu.Items.Add(closeOther);
var close = new MenuItemViewModel(I18N.Translate("Close"), CloseCommand, icon: Mdl2Icon.Cancel());
AddKeyBindings(close, Command.CloseTab, commandKeyBindings);
contextMenu.Items.Add(close);
return contextMenu;
}
private void AddKeyBindings(MenuItemViewModel menuItem, Command command, IDictionary<string, ICollection<KeyBinding>> commandKeyBindings)
{
var keyBindings = commandKeyBindings[command.ToString()];
if (keyBindings?.Any() == true)
{
menuItem.KeyBinding = new MenuItemKeyBindingViewModel(keyBindings.First());
}
}
}
}
+216 -90
View File
@@ -1,11 +1,13 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels
{
@@ -41,16 +43,25 @@ namespace FluentTerminal.App.ViewModels
private string _selection;
private string _white;
private string _yellow;
private ImageFile _backgroundThemeFile;
private readonly IFileSystemService _fileSystemService;
private readonly IImageFileSystemService _imageFileSystemService;
public event EventHandler<string> BackgroundChanged;
public event EventHandler<ImageFile> BackgroundImageChanged;
public ThemeViewModel(TerminalTheme theme, ISettingsService settingsService, IDialogService dialogService, IFileSystemService fileSystemService, bool isNew)
public ThemeViewModel(TerminalTheme theme,
ISettingsService settingsService,
IDialogService dialogService,
IFileSystemService fileSystemService,
IImageFileSystemService imageFileSystemService,
bool isNew)
{
Model = theme;
_settingsService = settingsService;
_dialogService = dialogService;
_fileSystemService = fileSystemService;
_imageFileSystemService = imageFileSystemService;
_isNew = isNew;
Name = Model.Name;
@@ -81,12 +92,16 @@ namespace FluentTerminal.App.ViewModels
CursorAccent = Model.Colors.CursorAccent;
Selection = Model.Colors.Selection;
BackgroundThemeFile = Model.BackgroundImage;
SetActiveCommand = new RelayCommand(SetActive);
DeleteCommand = new RelayCommand(async () => await Delete().ConfigureAwait(false), NotPreInstalled);
DeleteCommand = new AsyncCommand(DeleteAsync, NotPreInstalled);
EditCommand = new RelayCommand(Edit, NotPreInstalled);
CancelEditCommand = new RelayCommand(async () => await CancelEdit().ConfigureAwait(false));
SaveChangesCommand = new RelayCommand(SaveChanges);
ExportCommand = new RelayCommand(async () => await Export().ConfigureAwait(false), NotPreInstalled);
CancelEditCommand = new AsyncCommand(CancelEditAsync);
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
ExportCommand = new AsyncCommand(Export, NotPreInstalled);
ChooseBackgroundImageCommand = new AsyncCommand(ChooseBackgroundImageAsync, NotPreInstalled);
DeleteBackgroundImageCommand = new AsyncCommand(DeleteBackgroundImageAsync, NotPreInstalled);
}
public event EventHandler Activated;
@@ -177,7 +192,7 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _brightYellow, value);
}
public RelayCommand CancelEditCommand { get; }
public ICommand CancelEditCommand { get; }
public string Cursor
{
@@ -197,9 +212,9 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _cyan, value);
}
public RelayCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public RelayCommand ExportCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand EditCommand { get; }
public ICommand ExportCommand { get; }
public string Foreground
{
@@ -245,7 +260,7 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _red, value);
}
public RelayCommand SaveChangesCommand { get; }
public ICommand SaveChangesCommand { get; }
public string Selection
{
@@ -253,7 +268,7 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _selection, value);
}
public RelayCommand SetActiveCommand { get; }
public ICommand SetActiveCommand { get; }
public string White
{
@@ -267,7 +282,22 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _yellow, value);
}
public void SaveChanges()
public ImageFile BackgroundThemeFile
{
get => _backgroundThemeFile;
set
{
Set(ref _backgroundThemeFile, value);
BackgroundImageChanged?.Invoke(this, value);
}
}
public ICommand ChooseBackgroundImageCommand { get; }
public ICommand DeleteBackgroundImageCommand { get; }
// Requires UI thread
private async Task SaveChangesAsync()
{
Model.Name = Name;
Model.Author = Author;
@@ -296,92 +326,114 @@ namespace FluentTerminal.App.ViewModels
Model.Colors.CursorAccent = CursorAccent;
Model.Colors.Selection = Selection;
if (Model.BackgroundImage != null &&
BackgroundThemeFile != Model?.BackgroundImage)
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
await _imageFileSystemService
.RemoveImportedImageAsync($"{Model.BackgroundImage?.Name}{Model.BackgroundImage?.FileType}")
.ConfigureAwait(true);
}
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
BackgroundThemeFile = await SaveBackgroundImageAsync().ConfigureAwait(true);
Model.BackgroundImage = BackgroundThemeFile;
_settingsService.SaveTheme(Model);
InEditMode = false;
_isNew = false;
}
private async Task CancelEdit()
// Requires UI thread
private async Task CancelEditAsync()
{
if (_isNew)
{
await Delete();
await DeleteAsync().ConfigureAwait(false);
return;
}
TerminalTheme changedTheme = new TerminalTheme()
{
Name = Name,
Author = Author,
BackgroundImage = BackgroundThemeFile,
Colors = new TerminalColors()
{
Black = Black,
Red = Red,
Green = Green,
Yellow = Yellow,
Blue = Blue,
Magenta = Magenta,
Cyan = Cyan,
White = White,
BrightBlack = BrightBlack,
BrightRed = BrightRed,
BrightGreen = BrightGreen,
BrightYellow = BrightYellow,
BrightBlue = BrightBlue,
BrightMagenta = BrightMagenta,
BrightCyan = BrightCyan,
BrightWhite = BrightWhite,
Background = Background,
Foreground = Foreground,
Cursor = Cursor,
CursorAccent = CursorAccent,
Selection = Selection
}
};
if (!_fallbackTheme.Equals(changedTheme))
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Black = _fallbackTheme.Colors.Black;
Red = _fallbackTheme.Colors.Red;
Green = _fallbackTheme.Colors.Green;
Yellow = _fallbackTheme.Colors.Yellow;
Blue = _fallbackTheme.Colors.Blue;
Magenta = _fallbackTheme.Colors.Magenta;
Cyan = _fallbackTheme.Colors.Cyan;
White = _fallbackTheme.Colors.White;
BrightBlack = _fallbackTheme.Colors.BrightBlack;
BrightRed = _fallbackTheme.Colors.BrightRed;
BrightGreen = _fallbackTheme.Colors.BrightGreen;
BrightYellow = _fallbackTheme.Colors.BrightYellow;
BrightBlue = _fallbackTheme.Colors.BrightBlue;
BrightMagenta = _fallbackTheme.Colors.BrightMagenta;
BrightCyan = _fallbackTheme.Colors.BrightCyan;
BrightWhite = _fallbackTheme.Colors.BrightWhite;
Background = _fallbackTheme.Colors.Background;
Foreground = _fallbackTheme.Colors.Foreground;
Cursor = _fallbackTheme.Colors.Cursor;
CursorAccent = _fallbackTheme.Colors.CursorAccent;
Selection = _fallbackTheme.Colors.Selection;
Name = _fallbackTheme.Name;
Author = _fallbackTheme.Author;
BackgroundThemeFile = _fallbackTheme.BackgroundImage;
InEditMode = false;
await _imageFileSystemService.RemoveTemporaryBackgroundThemeImageAsync().ConfigureAwait(false);
}
}
else
{
TerminalTheme changedTheme = new TerminalTheme()
{
Name = Name,
Author = Author,
Colors = new TerminalColors()
{
Black = Black,
Red = Red,
Green = Green,
Yellow = Yellow,
Blue = Blue,
Magenta = Magenta,
Cyan = Cyan,
White = White,
BrightBlack = BrightBlack,
BrightRed = BrightRed,
BrightGreen = BrightGreen,
BrightYellow = BrightYellow,
BrightBlue = BrightBlue,
BrightMagenta = BrightMagenta,
BrightCyan = BrightCyan,
BrightWhite = BrightWhite,
Background = Background,
Foreground = Foreground,
Cursor = Cursor,
CursorAccent = CursorAccent,
Selection = Selection
}
};
if (!_fallbackTheme.Equals(changedTheme))
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Black = _fallbackTheme.Colors.Black;
Red = _fallbackTheme.Colors.Red;
Green = _fallbackTheme.Colors.Green;
Yellow = _fallbackTheme.Colors.Yellow;
Blue = _fallbackTheme.Colors.Blue;
Magenta = _fallbackTheme.Colors.Magenta;
Cyan = _fallbackTheme.Colors.Cyan;
White = _fallbackTheme.Colors.White;
BrightBlack = _fallbackTheme.Colors.BrightBlack;
BrightRed = _fallbackTheme.Colors.BrightRed;
BrightGreen = _fallbackTheme.Colors.BrightGreen;
BrightYellow = _fallbackTheme.Colors.BrightYellow;
BrightBlue = _fallbackTheme.Colors.BrightBlue;
BrightMagenta = _fallbackTheme.Colors.BrightMagenta;
BrightCyan = _fallbackTheme.Colors.BrightCyan;
BrightWhite = _fallbackTheme.Colors.BrightWhite;
Background = _fallbackTheme.Colors.Background;
Foreground = _fallbackTheme.Colors.Foreground;
Cursor = _fallbackTheme.Colors.Cursor;
CursorAccent = _fallbackTheme.Colors.CursorAccent;
Selection = _fallbackTheme.Colors.Selection;
Name = _fallbackTheme.Name;
Author = _fallbackTheme.Author;
InEditMode = false;
}
}
else
{
InEditMode = false;
}
InEditMode = false;
}
}
@@ -390,16 +442,38 @@ namespace FluentTerminal.App.ViewModels
return !Model.PreInstalled;
}
private async Task Delete()
// Requires UI thread
private async Task DeleteAsync()
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmDeleteTheme"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteTheme"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
await DeleteBackgroundImageIfExistsAsync().ConfigureAwait(true);
// ConfigureAwait(true) because we need to trigger Deleted event in the calling (UI) thread.
await _imageFileSystemService.RemoveTemporaryBackgroundThemeImageAsync().ConfigureAwait(true);
Deleted?.Invoke(this, EventArgs.Empty);
}
}
// Requires UI thread
private async Task DeleteBackgroundImageAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteBackgroundImage"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK)
{
BackgroundThemeFile = null;
}
}
private void Edit()
{
_fallbackTheme = new TerminalTheme(Model);
@@ -413,8 +487,60 @@ namespace FluentTerminal.App.ViewModels
private Task Export()
{
var content = JsonConvert.SerializeObject(Model, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
return _fileSystemService.SaveTextFile(Name, "Fluent Terminal Theme", ".flutecolors", content);
var encodedImage = _imageFileSystemService.EncodeImage(BackgroundThemeFile);
var exportedTheme = new ExportedTerminalTheme(Model, encodedImage);
var content = JsonConvert.SerializeObject(exportedTheme, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new TerminalThemeContractResolver() });
return _fileSystemService.SaveTextFileAsync(Name, "Fluent Terminal Theme", ".flutecolors", content);
}
private async Task<ImageFile> SaveBackgroundImageAsync()
{
if (BackgroundThemeFile == null)
{
return BackgroundThemeFile;
}
if (BackgroundThemeFile == Model.BackgroundImage)
{
return BackgroundThemeFile;
}
var importedBackgroundThemeFile =
await _fileSystemService.SaveImageInRoamingAsync(BackgroundThemeFile).ConfigureAwait(false);
await _imageFileSystemService.RemoveTemporaryBackgroundThemeImageAsync().ConfigureAwait(false);
return importedBackgroundThemeFile;
}
// Requires UI thread
private async Task ChooseBackgroundImageAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var chosenImage = await _imageFileSystemService
.ImportTemporaryImageFileAsync(new[] {".jpeg", ".png", ".jpg"}).ConfigureAwait(true);
if(chosenImage != null)
{
BackgroundThemeFile = chosenImage;
}
}
// Requires UI thread
private async Task DeleteBackgroundImageIfExistsAsync()
{
if (BackgroundThemeFile != null)
{
var imageFile = Model.BackgroundImage;
BackgroundThemeFile = null;
if (imageFile != null)
{
await _imageFileSystemService.RemoveImportedImageAsync($"{imageFile.Name}{imageFile.FileType}")
.ConfigureAwait(false);
}
}
}
}
}
@@ -0,0 +1,15 @@
namespace FluentTerminal.App.ViewModels.Utilities
{
public static class WebViewSpecialCharEncoder
{
public static string Encode(string value)
{
return value.Replace("\"", "&quot;").Replace("'", "&squo;").Replace("\\", "&bsol;");
}
public static string Decode(string value)
{
return value.Replace("&quot;", "\"").Replace("&squo;", "'").Replace("&bsol;", "\\");
}
}
}
+39
View File
@@ -0,0 +1,39 @@
using Microsoft.Xaml.Interactivity;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Actions
{
public class FocusAction : DependencyObject, IAction
{
public object Execute(object sender, object parameter)
{
var control = TargetObject ?? sender as Control;
if (control != null)
{
if (!control.IsLoaded)
control.Loaded += Control_Loaded;
else
control.Focus(FocusState.Programmatic);
}
return null;
}
private void Control_Loaded(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
control.Focus(FocusState.Programmatic);
control.Loaded -= Control_Loaded; // won't be needed anymore. Remove reference just in case
}
public Control TargetObject
{
get { return (Control)GetValue(TargetObjectProperty); }
set { SetValue(TargetObjectProperty, value); }
}
public static readonly DependencyProperty TargetObjectProperty =
DependencyProperty.Register(nameof(TargetObject), typeof(Control), typeof(FocusAction), new PropertyMetadata(null));
}
}
@@ -1,7 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
@@ -27,7 +25,7 @@ namespace FluentTerminal.App.Adapters
public async Task<ValueSet> SendMessageAsync(ValueSet message)
{
var response = await _appServiceConnection.SendMessageAsync(message).AsTask();
var response = await _appServiceConnection.SendMessageAsync(message);
if (response.Status == AppServiceResponseStatus.Success)
{
@@ -7,6 +7,7 @@ using Windows.Foundation.Metadata;
using Windows.UI.Core;
using Windows.UI.Core.Preview;
using Windows.UI.ViewManagement;
using FluentTerminal.App.Utilities;
namespace FluentTerminal.App.Adapters
{
@@ -24,13 +25,14 @@ namespace FluentTerminal.App.Adapters
_applicationView = ApplicationView.GetForCurrentView();
_applicationView.Consolidated += _applicationView_Consolidated;
_dispatcher = CoreApplication.GetCurrentView().Dispatcher;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequest;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequested;
Logger.Instance.Debug("Created ApplicationViewAdapter for ApplicationView with Id: {Id}", _applicationView.Id);
}
private void _applicationView_Consolidated(ApplicationView sender, ApplicationViewConsolidatedEventArgs args)
{
_applicationView.Consolidated -= _applicationView_Consolidated;
Closed?.Invoke(this, EventArgs.Empty);
}
@@ -47,27 +49,21 @@ namespace FluentTerminal.App.Adapters
return ApiInformation.IsApiContractPresent(api, version);
}
public Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true)
{
if (!enforceNewSchedule && _dispatcher.HasThreadAccess)
{
action();
public Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false) => _dispatcher.ExecuteAsync(action, priority, enforceNewSchedule);
return Task.CompletedTask;
}
public Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false) => _dispatcher.ExecuteAsync(func, priority, enforceNewSchedule);
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
}
public async Task<bool> TryClose()
public async Task<bool> TryCloseAsync()
{
if (_closed)
{
Logger.Instance.Debug("ApplicationViewAdapter.TryClose was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
Logger.Instance.Debug("ApplicationViewAdapter.TryCloseAsync 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();
Logger.Instance.Debug("TryCloseAsync ApplicationView with Id: {Id}", _applicationView.Id);
_closed = await _applicationView.TryConsolidateAsync();
return _closed;
}
@@ -78,28 +74,27 @@ namespace FluentTerminal.App.Adapters
_applicationView.ExitFullScreenMode();
return true;
}
else
{
return _applicationView.TryEnterFullScreenMode();
}
return _applicationView.TryEnterFullScreenMode();
}
private async void OnCloseRequest(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
private async void OnCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
{
var deferral = e.GetDeferral();
var args = new CancelableEventArgs();
if (CloseRequested != null)
if (CloseRequested is { } closeRequestedHandler)
{
await CloseRequested.Invoke(this, args);
// ConfigureAwait(true) because SystemNavigationManagerPreview.GetForCurrentView().CloseRequested requires the calling (UI) thread
await closeRequestedHandler(this, args).ConfigureAwait(true);
}
e.Handled = args.Cancelled;
if (!e.Handled)
{
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequested;
}
deferral.Complete();
@@ -1,42 +0,0 @@
using FluentTerminal.App.Services;
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.Adapters
{
public class DispatcherTimerAdapter : IDispatcherTimer
{
private readonly DispatcherTimer _dispatcherTimer;
public DispatcherTimerAdapter()
{
_dispatcherTimer = new DispatcherTimer();
_dispatcherTimer.Tick += OnDispatcherTimerTick;
}
private void OnDispatcherTimerTick(object sender, object e)
{
Tick?.Invoke(this, null);
}
public TimeSpan Interval
{
get => _dispatcherTimer.Interval;
set => _dispatcherTimer.Interval = value;
}
public bool IsEnabled => _dispatcherTimer.IsEnabled;
public event EventHandler<object> Tick;
public void Start()
{
_dispatcherTimer.Start();
}
public void Stop()
{
_dispatcherTimer.Stop();
}
}
}
Loaded 100 of 515 files, more files were not shown because too many files have changed in this diff. Show more