Compare commits

...
210 Commits
Author SHA1 Message Date
Felix Seidl 35c1f2d305 fix flute run not respecting UseConPty setting
fixes #941
2021-07-28 22:22:47 +02:00
Felix Seidl 4c5e13607f add missing messager registrations 2021-07-24 14:29:50 +02:00
Felix Seidl ae4a82ab5d fix command bar backgrounds 2021-07-24 14:29:14 +02:00
Felix Seidl 76312e042b update dependencies 2021-07-24 13:14:48 +02:00
Mertcan Davulcu 2bd2ec94b3 Update README.md (#928) 2021-06-20 13:24:04 +02:00
dependabot[bot] b0f651dccb Bump lodash from 4.17.20 to 4.17.21 in /FluentTerminal.Client (#917)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-10 07:42:13 +02:00
dependabot[bot] c63a6bbc53 Bump browserslist from 4.14.5 to 4.16.6 in /FluentTerminal.Client (#922)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.14.5 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.14.5...4.16.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-30 11:56:46 +02:00
Felix Seidl e7df4a0743 Revert WinUI (Settigs Crash) 2021-03-28 10:48:02 +02:00
Felix Seidl 823205fa69 Migrate to WCT MVVM 2021-03-27 18:08:51 +01:00
Felix Seidl 5fd7b00778 update nuget dependencies 2021-03-27 16:29:20 +01:00
Felix Seidl 3fb321a0d8 added nuget.config 2021-03-27 15:54:27 +01:00
Felix Seidl a815ccb64a Update main.yml 2021-03-27 15:50:32 +01:00
J04n5mith ebccdc0329 added quotes in the icon copy line (first line) (#897)
The icon does not copy when you have a path with blanks.
2021-03-06 12:58:53 +01:00
Mertcan Davulcu f9860ae8dd Clear Turkish Support (#892)
* Update Resources.resw

* Update Resources.resw
2021-02-07 22:03:35 +01:00
Felix Seidl 1a8a77bf66 Update main.yml 2021-02-07 21:25:16 +01:00
nckrkllkc 0d02b83279 Update Resources.resw (#891)
Spelling errors in Turkish language are fixed.
2021-02-07 21:12:16 +01:00
Felix Seidl c263489f40 Increase version 2021-01-19 18:41:37 +01:00
Felix Seidl b8610f6ccb Fix problems with adding/deleting profiles 2021-01-19 17:51:45 +01:00
Felix Seidl 9a1783c221 Update translations 2021-01-18 22:28:45 +01:00
akanealwandFelix Seidl 6a123ed953 Made changes to install.bat and added install_with_icon.bat to fix issues #867 and #719 respectively (#884)
* Create README.md

* Update README.md

* Add files via upload

* Update README.md

* Add files via upload

* Update README.md

* Update Install.bat

Added entry for drive context menu item.

* Update Install_with_icon.bat

* Update Uninstall.bat

Added extra uninstall command for added drive context menu item

* Update Install_with_icon.bat

Added another entry for drive context menu integration

* Update Install.bat

* Update README.md

* Update Install_with_icon.bat

* Update README.md

* Update Install_with_icon.bat

* remove icon file on uninstall

Co-authored-by: Felix Seidl <f.seidl@me.com>
2021-01-18 22:13:24 +01:00
Faris Cakaric 3fd4625d9e Added Bosnian (#880) 2021-01-17 12:29:30 +01:00
Faris CakaricandFelix Seidl 783d03848f Added Bosnian (#879)
* Added Bosnian

* include in project

Co-authored-by: Felix Seidl <f.seidl@me.com>
2021-01-17 12:29:16 +01:00
Felix Seidl 74e63f9abf downgrade WinUI to prevent crash when settings is opened the second time 2021-01-17 12:00:14 +01:00
Felix Seidl 92edcd224d fix sdk reference 2021-01-16 17:57:56 +01:00
Felix Seidl a1c9350818 increase version 2020-11-16 17:18:30 +01:00
Felix Seidl f6040e8c56 Update translations 2020-11-15 12:29:05 +01:00
Felix Seidl 44c7a0a2ae Update WinUI to fix crash in color picker
fixes #860
2020-11-15 12:27:50 +01:00
Felix Seidl 7491618109 fix esc key 2020-11-15 11:21:34 +01:00
RhavoX 78361ee160 Adjusted the behavior of search so that the previous text is selected once search is invoked when being open already (#869) 2020-11-15 11:03:32 +01:00
Felix Seidl 774665151e Increase version 2020-11-07 12:32:08 +01:00
Felix Seidl 8d2b696658 list all profiles in the new tab/window menu 2020-10-30 13:14:38 +01:00
Felix Seidl 6c7ea3bed6 Update translations 2020-10-30 12:09:25 +01:00
Felix Seidl 3e22d56468 update node dependencies 2020-10-30 11:46:24 +01:00
Felix Seidl b60fd38d51 update nuget dependencies 2020-10-30 11:46:14 +01:00
RhavoX 6e3cfa634e Improved the way the search flyout is behaving. (#826)
The search command will now show the flyout and focus on the textbox. If the textbox has no focus and search command is used again it will focus the textbox.
A new command has been added to close the search flyout (escape by default) which can now be used to close it.
2020-09-27 22:44:22 +02:00
Xherdi Lika 6d57cdfdab ✨ Updated Readme to include link to vscode extension (#842) 2020-09-03 19:11:53 +02:00
Felix Seidl 198853d37b Merge branch 'master' of https://github.com/felixse/FluentTerminal 2020-08-07 14:56:37 +02:00
Felix Seidl 68208ff0ec increase version 2020-08-07 14:56:35 +02:00
RhavoX e3f079701f Added missing Command.ReconnectTab localisation entries. (#827) 2020-08-06 14:59:22 +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
353 changed files with 33917 additions and 9646 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: microsoft/setup-msbuild@v1.0.2
- name: MSBuild
run: msbuild FluentTerminal.sln -t:build -p:Configuration=Debug -p:Platform=x64 -m -restore
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -1,3 +1,4 @@
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%1\"" /f
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
@@ -0,0 +1,9 @@
copy /y FluentTerminal.ico "%LOCALAPPDATA%\Microsoft\WindowsApps"
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
reg add "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here" /v icon /t REG_SZ /d "%LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico" /f
@@ -0,0 +1,2 @@
If you want a an icon for the context menu entries, download the .ico file and put in the same directory as the Install_with_icon.bat file then run it.
The same Uninstall.bat works for both Install.bat files.
@@ -1,3 +1,5 @@
reg delete "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\Drive\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here" /f
del %LOCALAPPDATA%\Microsoft\WindowsApps\FluentTerminal.ico
@@ -23,7 +23,7 @@ namespace FluentTerminal.App.Services.Test
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
{
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<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<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
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<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
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<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
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<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
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<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
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<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
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.11.0" />
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="Moq" Version="4.13.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@@ -1,7 +1,6 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Moq;
@@ -11,8 +10,8 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
using Xunit;
using Microsoft.Toolkit.Mvvm.Messaging;
namespace FluentTerminal.App.Services.Test
{
@@ -38,7 +37,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);
@@ -60,6 +60,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>(),
@@ -88,6 +89,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);
@@ -113,6 +115,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);
@@ -136,6 +139,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);
@@ -157,12 +161,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);
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
WeakReferenceMessenger.Default.Register<CurrentThemeChangedMessage>(this,
(r, m) => currentThemeChangedEventInvoked = true);
settingsService.SaveCurrentThemeId(currentThemeId);
@@ -181,7 +186,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);
@@ -206,11 +212,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);
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
WeakReferenceMessenger.Default.Register<CurrentThemeChangedMessage>(this,
(r, m) => currentThemeChangedEventInvoked = true);
settingsService.SaveTheme(theme);
@@ -229,7 +236,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);
@@ -251,7 +259,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);
@@ -274,7 +283,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);
@@ -297,7 +307,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);
@@ -320,7 +331,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);
@@ -342,7 +354,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);
@@ -364,11 +377,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);
Messenger.Default.Register<TerminalOptionsChangedMessage>(this,
message => terminalOptionsChangedEventInvoked = true);
WeakReferenceMessenger.Default.Register<TerminalOptionsChangedMessage>(this,
(r, m) => terminalOptionsChangedEventInvoked = true);
settingsService.SaveTerminalOptions(terminalOptions);
@@ -388,7 +402,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);
@@ -410,7 +425,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);
@@ -432,11 +448,11 @@ 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);
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this,
a => applicationSettingsChangedEventInvoked = true);
WeakReferenceMessenger.Default.Register<ApplicationSettingsChangedMessage>(this, (r, m) => applicationSettingsChangedEventInvoked = true);
settingsService.SaveApplicationSettings(applicationSettings);
@@ -459,7 +475,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);
@@ -483,7 +500,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);
@@ -508,7 +526,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);
@@ -531,11 +550,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);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
WeakReferenceMessenger.Default.Register<KeyBindingsChangedMessage>(this,
(r, m) => keyBindingsChangedEventInvoked = true);
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
@@ -553,7 +573,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);
@@ -578,11 +599,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);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
WeakReferenceMessenger.Default.Register<KeyBindingsChangedMessage>(this,
(r, m) => keyBindingsChangedEventInvoked = true);
settingsService.ResetKeyBindings();
@@ -602,6 +624,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);
@@ -627,6 +650,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);
@@ -648,6 +672,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>(),
@@ -670,6 +695,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>(),
@@ -691,6 +717,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>(),
@@ -715,6 +742,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,
@@ -743,6 +771,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,7 +68,7 @@ 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] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
}
@@ -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,7 +107,7 @@ 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] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
}
@@ -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
{
@@ -56,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,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();
}
}
@@ -1,5 +1,4 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml;
namespace FluentTerminal.App.Services.EventArgs
{
@@ -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)
{
}
}
}
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluentTerminal.App.Services.Exceptions
{
@@ -6,13 +6,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.6.0" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="PListNet" Version="3.0.0" />
<PackageReference Include="RestSharp" Version="106.6.10" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Toolkit.Mvvm" Version="7.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="PListNet" Version="3.2.0" />
<PackageReference Include="RestSharp" Version="106.12.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
</ItemGroup>
@@ -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,25 +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<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null);
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
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);
}
}
@@ -13,12 +13,14 @@ 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);
@@ -28,7 +30,6 @@ namespace FluentTerminal.App.Services
void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id);
void SaveDefaultSshProfileId(Guid id);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
@@ -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,37 +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<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause);
Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause);
Task ResizeTerminal(byte id, TerminalSize size);
Task ResizeTerminalAsync(byte id, TerminalSize size);
Task UpdateToggleWindowKeyBindings();
Task UpdateToggleWindowKeyBindingsAsync();
Task Write(byte terminalId, byte[] data);
Task WriteAsync(byte terminalId, byte[] data);
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
void UnsubscribeFromTerminalOutput(byte terminalId);
Task CloseTerminal(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,5 +1,4 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
@@ -27,6 +26,7 @@ namespace FluentTerminal.App.Services.Implementation
UnderlineSelectedTab = false,
InactiveTabColorMode = InactiveTabColorMode.Background,
NewTerminalLocation = NewTerminalLocation.Tab,
TabWindowCascadingAppMenu = true,
TabsPosition = TabsPosition.Top,
CopyOnSelect = false,
MouseMiddleClickAction = MouseAction.None,
@@ -40,7 +40,9 @@ namespace FluentTerminal.App.Services.Implementation
MuteTerminalBeeps = true,
EnableLogging = false,
PrintableOutputOnly = true,
LogDirectoryPath = logDirectoryPath
LogDirectoryPath = logDirectoryPath,
UseConPty = true,
ShowTextCopied = false
};
}
@@ -112,6 +114,18 @@ namespace FluentTerminal.App.Services.Implementation
}
};
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>
{
@@ -124,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>
{
@@ -167,8 +169,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NewCustomCommandTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Q
Alt = true,
Key = (int)ExtendedVirtualKey.T
}
};
@@ -180,35 +182,10 @@ namespace FluentTerminal.App.Services.Implementation
Command = nameof(Command.NewCustomCommandWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.Q
Key = (int)ExtendedVirtualKey.N
}
};
case Command.SavedSshNewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.SavedSshNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
@@ -245,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>
{
@@ -318,6 +283,16 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.CloseSearch:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.CloseSearch),
Key = (int)ExtendedVirtualKey.Escape
}
};
case Command.ToggleFullScreen:
return new List<KeyBinding>
{
@@ -357,7 +332,36 @@ 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");
}
@@ -375,7 +379,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,
@@ -386,37 +390,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"
}
};
@@ -432,11 +436,12 @@ namespace FluentTerminal.App.Services.Implementation
ScrollBarStyle = ScrollBarStyle.Hidden,
FontFamily = "Consolas",
FontSize = 13,
BoldText = false,
FontWeight = 400,
BackgroundOpacity = 0.8,
Padding = 12,
ScrollBackLimit = 1000,
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;"
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;",
UseAcrylicBackground = true
};
}
@@ -458,8 +463,8 @@ namespace FluentTerminal.App.Services.Implementation
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"
@@ -486,8 +491,8 @@ namespace FluentTerminal.App.Services.Implementation
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"
@@ -514,8 +519,8 @@ namespace FluentTerminal.App.Services.Implementation
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"
@@ -7,29 +7,23 @@ 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<ICustomCommandDialog> _customCommandDialogFactory;
private readonly Func<ISshProfileSelectionDialog> _sshProfileSelectionDialogFactory;
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<ICustomCommandDialog> customCommandDialogFactory,
Func<ISshProfileSelectionDialog> sshProfileSelectionDialogFactory,
Func<IAboutDialog> aboutDialogFactory)
{
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
_messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory;
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
_customCommandDialogFactory = customCommandDialogFactory;
_sshProfileSelectionDialogFactory = sshProfileSelectionDialogFactory;
_aboutDialogFactory = aboutDialogFactory;
}
@@ -40,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))
{
@@ -77,26 +71,12 @@ 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<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
_customCommandDialogFactory().GetCustomCommandAsync(input);
public Task<SshProfile> ShowSshProfileSelectionDialogAsync()
{
var dialog = _sshProfileSelectionDialogFactory();
return dialog.SelectProfile();
}
public Task ShowAboutDialogAsync()
{
var dialog = _aboutDialogFactory();
@@ -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";
@@ -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));
}
}
}
@@ -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,8 +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;
using Microsoft.Toolkit.Mvvm.Messaging;
namespace FluentTerminal.App.Services.Implementation
{
@@ -62,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))
@@ -70,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);
}
@@ -80,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)
@@ -92,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);
@@ -116,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);
@@ -140,25 +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 void DeleteShellProfile(Guid id)
{
_shellProfiles.Delete(id.ToString());
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
WeakReferenceMessenger.Default.Send(new ShellProfileDeletedMessage(id));
}
public void DeleteSshProfile(Guid id)
{
_sshProfiles.Delete(id.ToString());
Messenger.Default.Send(new SshProfileDeletedMessage(id));
Messenger.Default.Send(new KeyBindingsChangedMessage());
WeakReferenceMessenger.Default.Send(new ShellProfileDeletedMessage(id));
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage());
}
@@ -175,7 +193,7 @@ namespace FluentTerminal.App.Services.Implementation
}
}
Messenger.Default.Send(new ThemeDeletedMessage(id));
WeakReferenceMessenger.Default.Send(new ThemeDeletedMessage(id));
}
public ApplicationSettings GetApplicationSettings()
@@ -217,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)
{
@@ -242,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()
{
@@ -264,20 +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 _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))
{
return new List<SshProfile>();
// 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()
@@ -307,34 +333,32 @@ namespace FluentTerminal.App.Services.Implementation
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
}
Messenger.Default.Send(new KeyBindingsChangedMessage());
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage());
}
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
{
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
WeakReferenceMessenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
{
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
WeakReferenceMessenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void SaveCurrentThemeId(Guid id)
{
_roamingSettings.SetValue(CurrentThemeKey, id);
Messenger.Default.Send(new CurrentThemeChangedMessage(id));
WeakReferenceMessenger.Default.Send(new CurrentThemeChangedMessage(id));
}
public void SaveDefaultShellProfileId(Guid id)
{
_localSettings.SetValue(DefaultShellProfileKey, id);
}
public void SaveDefaultSshProfileId(Guid id)
{
_localSettings.SetValue(DefaultSshProfileKey, id);
WeakReferenceMessenger.Default.Send(new DefaultShellProfileChangedMessage(id));
}
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
@@ -345,7 +369,7 @@ namespace FluentTerminal.App.Services.Implementation
}
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
Messenger.Default.Send(new KeyBindingsChangedMessage());
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage());
}
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
@@ -353,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.
Messenger.Default.Send(new KeyBindingsChangedMessage());
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
Messenger.Default.Send(new ShellProfileAddedMessage(shellProfile));
WeakReferenceMessenger.Default.Send(new ShellProfileAddedMessage(shellProfile));
}
else
{
WeakReferenceMessenger.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.
Messenger.Default.Send(new KeyBindingsChangedMessage());
WeakReferenceMessenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
Messenger.Default.Send(new SshProfileAddedMessage(sshProfile));
WeakReferenceMessenger.Default.Send(new ShellProfileAddedMessage(sshProfile));
}
else
{
WeakReferenceMessenger.Default.Send(new ShellProfileChangedMessage(sshProfile));
}
}
public void SaveTerminalOptions(TerminalOptions terminalOptions)
{
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
Messenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions));
WeakReferenceMessenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions));
}
public void SaveTheme(TerminalTheme theme, bool newTheme = false)
@@ -385,12 +418,12 @@ namespace FluentTerminal.App.Services.Implementation
if (theme.Id == GetCurrentThemeId())
{
Messenger.Default.Send(new CurrentThemeChangedMessage(theme.Id));
WeakReferenceMessenger.Default.Send(new CurrentThemeChangedMessage(theme.Id));
}
if (newTheme)
{
Messenger.Default.Send(new ThemeAddedMessage(theme));
WeakReferenceMessenger.Default.Send(new ThemeAddedMessage(theme));
}
}
}
@@ -8,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;
@@ -31,9 +52,7 @@ namespace FluentTerminal.App.Services.Implementation
return _nextTerminalId++;
}
private string _userName;
public async Task<string> GetUserName()
public async Task<string> GetUserNameAsync()
{
if (!string.IsNullOrEmpty(_userName))
{
@@ -41,52 +60,56 @@ namespace FluentTerminal.App.Services.Implementation
return _userName;
}
StringValueResponse 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<StringValueResponse>((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.Value;
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 SaveTextFileException(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;
@@ -94,42 +117,31 @@ 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 void UpdateSettings(ApplicationSettings settings)
@@ -137,7 +149,8 @@ namespace FluentTerminal.App.Services.Implementation
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
}
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
public async Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType)
{
var request = new CreateTerminalRequest
{
@@ -149,24 +162,17 @@ 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 async Task<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause)
public Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause)
{
var request = new PauseTerminalOutputRequest
{
Id = id,
Pause = pause
};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
return JsonConvert.DeserializeObject<PauseTerminalOutputResponse>((string)responseMessage[MessageKeys.Content]);
return GetResponseAsync<PauseTerminalOutputResponse>(
new PauseTerminalOutputRequest {Id = id, Pause = pause});
}
public void Initialize(IAppServiceConnection appServiceConnection)
@@ -206,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)
@@ -230,19 +231,15 @@ namespace FluentTerminal.App.Services.Implementation
}
}
public Task UpdateToggleWindowKeyBindings()
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
{
@@ -254,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);
@@ -263,10 +260,6 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
public async Task<string> GetCommandPathAsync(string command)
{
if (string.IsNullOrWhiteSpace(command))
@@ -288,11 +281,8 @@ namespace FluentTerminal.App.Services.Implementation
throw new Exception(error);
}
var request = new GetCommandPathRequest {Command = command};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response =
JsonConvert.DeserializeObject<StringValueResponse>((string) responseMessage[MessageKeys.Content]);
var response = await GetResponseAsync<StringValueResponse>(new GetCommandPathRequest {Command = command})
.ConfigureAwait(false);
if (response.Success)
{
@@ -306,13 +296,17 @@ namespace FluentTerminal.App.Services.Implementation
throw new Exception(response.Error);
}
private ValueSet CreateMessage(IMessage content)
public Task QuitApplicationAsync()
{
return new ValueSet
{
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
};
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);
+28 -21
View File
@@ -11,9 +11,10 @@ namespace FluentTerminal.App.Services
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
private bool _closingFromUI;
private bool _closingFromUi;
private bool _exited;
private readonly bool _requireShellProcessStart;
private string _fallbackTitle;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
{
@@ -23,6 +24,12 @@ namespace FluentTerminal.App.Services
_requireShellProcessStart = !terminalId.HasValue;
}
public void Reconnect()
{
_exited = false;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
}
private void OnTerminalExited(object sender, TerminalExitStatus status)
{
if (status.TerminalId != Id)
@@ -33,7 +40,7 @@ namespace FluentTerminal.App.Services
_exited = true;
Exited?.Invoke(this, status.ExitCode);
if (_closingFromUI || status.ExitCode <= 0)
if (_closingFromUi || status.ExitCode <= 0)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
@@ -68,23 +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;
_closingFromUi = true;
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
return _trayProcessCommunicationService.CloseTerminalAsync(Id);
}
/// <summary>
@@ -115,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);
}
@@ -128,19 +135,18 @@ 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<TerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
public async Task<TerminalResponse> StartShellProcessAsync(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
{
Profile = shellProfile;
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
{
OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState));
@@ -150,24 +156,25 @@ namespace FluentTerminal.App.Services
if (_requireShellProcessStart)
{
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
var response = await _trayProcessCommunicationService
.CreateTerminalAsync(Id, size, shellProfile, sessionType).ConfigureAwait(false);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
_fallbackTitle = response.Name;
SetTitle(_fallbackTitle);
}
return response;
}
else
{
return await _trayProcessCommunicationService.PauseTerminalOutput(Id, false);
return await _trayProcessCommunicationService.PauseTerminalOutputAsync(Id, false);
}
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.Write(Id, data);
return _trayProcessCommunicationService.WriteAsync(Id, data);
}
/// <summary>
@@ -8,10 +8,11 @@ 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)
@@ -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;
}
}
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
@@ -7,7 +8,7 @@ namespace FluentTerminal.App.ViewModels
{
private const int DelayMilliseconds = 5000;
private readonly CommandProfileProviderViewModel _vm;
private readonly Action _saveAction;
private readonly object _lock = new object();
@@ -18,9 +19,9 @@ namespace FluentTerminal.App.ViewModels
private bool _toSave = true;
internal DelayedHistorySaver(CommandProfileProviderViewModel vm)
internal DelayedHistorySaver(Action saveAction)
{
_vm = vm;
_saveAction = saveAction;
}
public void SetSuccessfulSessionStart()
@@ -86,7 +87,7 @@ namespace FluentTerminal.App.ViewModels
{
if (_toSave)
{
_vm.SaveToHistory();
_saveAction.Invoke();
}
_toSave = false;
@@ -1,9 +1,9 @@
using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
namespace FluentTerminal.App.ViewModels
{
public class EnvironmentVariableViewModel : ViewModelBase
public class EnvironmentVariableViewModel : ObservableObject
{
private string _name;
private string _value;
@@ -13,13 +13,13 @@ namespace FluentTerminal.App.ViewModels
public string Name
{
get => _name;
set => Set(ref _name, value);
set => SetProperty(ref _name, value);
}
public string Value
{
get => _value;
set => Set(ref _value, value);
set => SetProperty(ref _value, value);
}
}
}
@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
<PackageReference Include="Microsoft.Toolkit.Mvvm" Version="7.0.2" />
</ItemGroup>
<ItemGroup>
@@ -13,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>
+14 -10
View File
@@ -1,18 +1,22 @@
using FluentTerminal.Models;
using System;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public interface ITerminalView
public interface ITerminalView : IDisposable
{
Task ChangeTheme(TerminalTheme theme);
Task ChangeKeyBindings();
Task ChangeOptions(TerminalOptions options);
Task Initialize(TerminalViewModel viewModel);
Task ChangeFontSize(int fontSize);
Task ChangeThemeAsync(TerminalTheme theme);
Task ChangeKeyBindingsAsync();
Task ChangeOptionsAsync(TerminalOptions options);
Task InitializeAsync(TerminalViewModel viewModel);
Task ReconnectAsync();
void DisposalPrepare();
Task FindNext(string searchText);
Task FindPrevious(string searchText);
Task FocusTerminal();
Task<string> SerializeXtermState();
Task FindNextAsync(SearchRequest request);
Task FindPreviousAsync(SearchRequest request);
Task FocusTerminalAsync();
Task<string> SerializeXtermStateAsync();
void Paste(string text);
}
}
@@ -1,124 +0,0 @@
using FluentTerminal.App.ViewModels.Utilities;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Infrastructure
{
public class AsyncCommand : IAsyncCommand
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(
Func<Task> execute,
Func<bool> canExecute = null,
IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute()
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async Task ExecuteAsync()
{
if (CanExecute())
{
try
{
_isExecuting = true;
await _execute();
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute();
}
void ICommand.Execute(object parameter)
{
ExecuteAsync().FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
public class AsyncCommand<T> : IAsyncCommand<T>
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<T, Task> _execute;
private readonly Func<T, bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(Func<T, Task> execute, Func<T, bool> canExecute = null, IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute(T parameter)
{
return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
}
public async Task ExecuteAsync(T parameter)
{
if (CanExecute(parameter))
{
try
{
_isExecuting = true;
await _execute(parameter);
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute((T)parameter);
}
void ICommand.Execute(object parameter)
{
ExecuteAsync((T)parameter).FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
}
@@ -1,20 +0,0 @@
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Infrastructure
{
/// <summary>
/// based on: https://github.com/johnthiriet/AsyncVoid
/// </summary>
public interface IAsyncCommand : ICommand
{
Task ExecuteAsync();
bool CanExecute();
}
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,46 @@
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 Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using Windows.System;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemKeyBindingViewModel : ObservableObject
{
private int _key;
public int Key
{
get => _key;
set => SetProperty(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 => SetProperty(ref _keyModifiers, value);
}
private bool _ctrl;
public bool Ctrl
{
get => _ctrl;
set
{
if (SetProperty(ref _ctrl, value))
{
SetKeyModifiers();
}
}
}
private bool _alt;
public bool Alt
{
get => _alt;
set
{
if (SetProperty(ref _alt, value))
{
SetKeyModifiers();
}
}
}
private bool _shift;
public bool Shift
{
get => _shift;
set
{
if (SetProperty(ref _shift, value))
{
SetKeyModifiers();
}
}
}
private bool _windows;
public bool Windows
{
get => _windows;
set
{
if (SetProperty(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 => SetProperty(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 Microsoft.Toolkit.Mvvm.ComponentModel;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemViewModelBase : ObservableObject
{
#region Properties
private string _text;
public string Text
{
get => _text;
set => SetProperty(ref _text, value);
}
private string _description;
public string Description
{
get => _description;
set => SetProperty(ref _description, value);
}
private Mdl2Icon _icon;
public Mdl2Icon Icon
{
get => _icon;
set => SetProperty(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 Microsoft.Toolkit.Mvvm.ComponentModel;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuViewModel : ObservableObject
{
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,31 +1,31 @@
using FluentTerminal.App.Services;
using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.ViewModels
{
public class OverlayViewModel : ViewModelBase
public class OverlayViewModel : ObservableObject
{
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 = new DispatcherTimer {Interval = new TimeSpan(0, 0, 2)};
}
public bool ShowOverlay
{
get => _showOverlay;
set => Set(ref _showOverlay, value);
set => SetProperty(ref _showOverlay, value);
}
public string OverlayContent
{
get => _overlayContent;
set => Set(ref _overlayContent, value);
set => SetProperty(ref _overlayContent, value);
}
public void Show(string message)
@@ -3,22 +3,21 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class ProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
public abstract class ProfileViewModelBase<T> : ObservableObject where T : ProfileProviderViewModelBase
{
#region Fields
@@ -42,7 +41,7 @@ namespace FluentTerminal.App.ViewModels
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
set => SetProperty(ref _inEditMode, value);
}
private string _name;
@@ -50,7 +49,7 @@ namespace FluentTerminal.App.ViewModels
public string Name
{
get => _name;
set => Set(ref _name, value);
set => SetProperty(ref _name, value);
}
public KeyBindingsViewModel KeyBindings { get; }
@@ -63,21 +62,22 @@ namespace FluentTerminal.App.ViewModels
#region Events
// Needs to be triggered from UI thread
public event EventHandler Deleted;
#endregion Events
#region Commands
public IAsyncCommand CancelEditCommand { get; }
public ICommand CancelEditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public ICommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public ICommand EditCommand { get; }
public IAsyncCommand SaveChangesCommand { get; }
public ICommand SaveChangesCommand { get; }
public IAsyncCommand AddKeyboardShortcutCommand { get; }
public ICommand AddKeyboardShortcutCommand { get; }
#endregion Commands
@@ -102,17 +102,18 @@ namespace FluentTerminal.App.ViewModels
Initialize(shellProfile);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
DeleteCommand = new AsyncRelayCommand(DeleteAsync, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
CancelEditCommand = new AsyncRelayCommand(CancelEditAsync);
SaveChangesCommand = new AsyncRelayCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncRelayCommand(AddKeyboardShortcutAsync);
}
#endregion Constructor
#region Methods
// Requires UI thread
// Loads view model properties from the input shellProfile
protected void Initialize(ShellProfile shellProfile)
{
@@ -134,9 +135,11 @@ namespace FluentTerminal.App.ViewModels
|| !(_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)
@@ -162,58 +165,64 @@ namespace FluentTerminal.App.ViewModels
InEditMode = true;
}
private async Task CancelEdit()
// Requires UI thread
private async Task CancelEditAsync()
{
if (IsNew)
{
await Delete();
}
else
{
if (HasChanges())
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK)
{
// Cancelled, so rollback
ProfileVm.RejectChanges();
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
}
}
public virtual async Task SaveChangesAsync()
{
var error = await ProfileVm.AcceptChangesAsync();
if (!string.IsNullOrEmpty(error))
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), error, DialogButton.OK);
await DeleteAsync().ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Any(x => string.IsNullOrWhiteSpace(x)))
if (!HasChanges())
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK);
KeyBindings.Editable = false;
InEditMode = false;
return;
}
// 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;
}
}
// Requires UI thread
private async Task SaveChangesAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var error = await ProfileVm.AcceptChangesAsync().ConfigureAwait(true);
if (!string.IsNullOrEmpty(error))
{
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.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK);
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK).ConfigureAwait(false);
return;
}
@@ -239,9 +248,9 @@ namespace FluentTerminal.App.ViewModels
IsNew = false;
}
public Task AddKeyboardShortcut()
private Task AddKeyboardShortcutAsync()
{
return KeyBindings.ShowAddKeyBindingDialog();
return KeyBindings.ShowAddKeyBindingDialogAsync();
}
#endregion Methods
@@ -1,8 +1,9 @@
using System.Threading.Tasks;
using System.Windows.Input;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels.Profiles
{
@@ -26,7 +27,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public string Location
{
get => _location;
set => Set(ref _location, value);
set => SetProperty(ref _location, value);
}
private string _workingDirectory;
@@ -34,7 +35,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public string WorkingDirectory
{
get => _workingDirectory;
set => Set(ref _workingDirectory, value);
set => SetProperty(ref _workingDirectory, value);
}
private string _arguments;
@@ -42,16 +43,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
public string Arguments
{
get => _arguments;
set => Set(ref _arguments, value);
set => SetProperty(ref _arguments, value);
}
#endregion Properties
#region Commands
public IAsyncCommand BrowseForCustomShellCommand { get; }
public ICommand BrowseForCustomShellCommand { get; }
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
public ICommand BrowseForWorkingDirectoryCommand { get; }
#endregion Commands
@@ -63,8 +64,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
{
_fileSystemService = fileSystemService;
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
BrowseForCustomShellCommand = new AsyncRelayCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncRelayCommand(BrowseForWorkingDirectory);
Initialize(Model);
}
@@ -90,7 +91,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile);
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
profile.Location = _location;
profile.Arguments = _arguments;
@@ -99,7 +100,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 +124,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;
@@ -1,18 +0,0 @@
using System;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels.Profiles
{
public class ExecutedCommand
{
public string Value { get; set; }
public bool IsProfile { get; set; }
public DateTime LastExecution { get; set; }
public int ExecutionCount { get; set; }
public ShellProfile ShellProfile { get; set; }
}
}
@@ -4,11 +4,12 @@ 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;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Messaging;
namespace FluentTerminal.App.ViewModels.Profiles
{
@@ -16,15 +17,10 @@ namespace FluentTerminal.App.ViewModels.Profiles
/// Base class for all profile view model classes. It contains properties shared by all other view models
/// (theme-related properties, line-ending translation, and WinPTY/ConPTY selection).
/// </summary>
public abstract class ProfileProviderViewModelBase : ViewModelBase
public abstract class ProfileProviderViewModelBase : ObservableObject,
IRecipient<ThemeAddedMessage>,
IRecipient<ThemeDeletedMessage>
{
#region Static
private static readonly LineEndingStyle[] LineEndingStylesArray =
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
#endregion Static
#region Fields
protected readonly ISettingsService SettingsService;
@@ -67,17 +63,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
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
@@ -96,7 +81,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
? value
: TabThemes.FirstOrDefault(t => t.Id == value.Id) ?? TabThemes.First();
if (Set(ref _selectedTabTheme, theme))
if (SetProperty(ref _selectedTabTheme, theme))
{
_tabThemeId = theme.Id;
}
@@ -110,7 +95,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
get => _tabThemeId;
set
{
if (Set(ref _tabThemeId, value))
if (SetProperty(ref _tabThemeId, value))
{
SelectedTabTheme = TabThemes.FirstOrDefault(t => t.Id.Equals(value));
}
@@ -129,13 +114,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))
{
@@ -143,7 +122,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(theme.Id)) ?? TerminalThemes.First();
}
if (Set(ref _selectedTerminalTheme, theme))
if (SetProperty(ref _selectedTerminalTheme, theme))
{
_terminalThemeId = theme.Id;
}
@@ -157,7 +136,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
get => _terminalThemeId;
set
{
if (Set(ref _terminalThemeId, value))
if (SetProperty(ref _terminalThemeId, value))
{
SelectedTerminalTheme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(value));
}
@@ -169,7 +148,15 @@ namespace FluentTerminal.App.ViewModels.Profiles
public bool UseConPty
{
get => _useConPty;
set => Set(ref _useConPty, value);
set => SetProperty(ref _useConPty, value);
}
private bool _useBuffer;
public bool UseBuffer
{
get => _useBuffer;
set => SetProperty(ref _useBuffer, value);
}
#endregion Properties
@@ -195,17 +182,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
SelectedTerminalTheme = TerminalThemes.First();
MessengerInstance.Register<ThemeAddedMessage>(this, OnThemeAdded);
MessengerInstance.Register<ThemeDeletedMessage>(this, OnThemeDeleted);
Initialize(Model);
WeakReferenceMessenger.Default.RegisterAll(this);
}
#endregion Constructor
#region Methods
private void OnThemeDeleted(ThemeDeletedMessage message)
public void Receive(ThemeDeletedMessage message)
{
var theme = TerminalThemes.FirstOrDefault(t => t.Id.Equals(message.ThemeId));
@@ -214,7 +200,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return;
}
ApplicationView.RunOnDispatcherThread(() =>
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
TerminalThemes.Remove(theme);
@@ -222,18 +208,18 @@ namespace FluentTerminal.App.ViewModels.Profiles
{
SelectedTerminalTheme = TerminalThemes.First();
}
}, false);
}, CoreDispatcherPriority.Low, true);
}
private void OnThemeAdded(ThemeAddedMessage message)
public void Receive(ThemeAddedMessage message)
{
ApplicationView.RunOnDispatcherThread(() => TerminalThemes.Add(message.Theme), 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;
}
@@ -245,8 +231,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
protected virtual Task CopyToProfileAsync(ShellProfile profile)
{
profile.LineEndingTranslation = _lineEndingTranslation;
profile.UseConPty = _useConPty;
profile.UseBuffer = _useBuffer;
profile.TerminalThemeId = _terminalThemeId;
profile.TabThemeId = _tabThemeId;
return Task.CompletedTask;
@@ -263,8 +249,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;
}
@@ -280,19 +266,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))
{
await CopyToProfileAsync(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
@@ -300,7 +286,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";
@@ -340,8 +326,7 @@ URL={0}
public string GetBaseQueryString()
{
var queryString =
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
var queryString = $"{UseConPtyQueryStringName}={_useConPty}&{UseBufferQueryStringName}={_useBuffer}";
if (_tabThemeId != TabThemes.First().Id)
{
@@ -372,11 +357,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 =>
@@ -6,10 +6,11 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Windows.Input;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels.Profiles
{
@@ -59,7 +60,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public string Host
{
get => _host;
set => Set(ref _host, value);
set => SetProperty(ref _host, value);
}
private ushort _sshPort;
@@ -67,7 +68,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public ushort SshPort
{
get => _sshPort;
set => Set(ref _sshPort, value);
set => SetProperty(ref _sshPort, value);
}
private string _username;
@@ -75,7 +76,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public string Username
{
get => _username;
set => Set(ref _username, value);
set => SetProperty(ref _username, value);
}
private string _identityFile;
@@ -83,7 +84,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public string IdentityFile
{
get => _identityFile;
set => Set(ref _identityFile, value);
set => SetProperty(ref _identityFile, value);
}
private bool _useMosh;
@@ -91,7 +92,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public bool UseMosh
{
get => _useMosh;
set => Set(ref _useMosh, value);
set => SetProperty(ref _useMosh, value);
}
private ushort _moshPortFrom;
@@ -99,7 +100,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public ushort MoshPortFrom
{
get => _moshPortFrom;
set => Set(ref _moshPortFrom, value);
set => SetProperty(ref _moshPortFrom, value);
}
private ushort _moshPortTo;
@@ -107,14 +108,14 @@ namespace FluentTerminal.App.ViewModels.Profiles
public ushort MoshPortTo
{
get => _moshPortTo;
set => Set(ref _moshPortTo, value);
set => SetProperty(ref _moshPortTo, value);
}
#endregion Properties
#region Commands
public IAsyncCommand BrowseForIdentityFileCommand { get; }
public ICommand BrowseForIdentityFileCommand { get; }
#endregion Commands
@@ -123,14 +124,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
SshProfile original = null) : base(settingsService, applicationView, true,
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault,
RequestConPty = settingsService.GetApplicationSettings().UseConPty
})
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
Initialize((SshProfile)Model);
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
BrowseForIdentityFileCommand = new AsyncRelayCommand(BrowseForIdentityFileAsync);
}
#endregion Constructor
@@ -147,13 +150,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);
}
@@ -164,9 +167,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;
@@ -175,20 +180,28 @@ 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)
@@ -200,9 +213,9 @@ namespace FluentTerminal.App.ViewModels.Profiles
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile);
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
SshProfile sshProfile = (SshProfile) profile;
var sshProfile = (SshProfile) profile;
sshProfile.Location = _useMosh ? Constants.MoshCommandName : Constants.SshCommandName;
sshProfile.Arguments = GetArgumentsString();
@@ -220,14 +233,14 @@ 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))
{
return error;
}
var result = await GetSshInfoValidationResultAsync();
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
if (result == SshConnectionInfoValidationResult.Valid)
{
@@ -254,7 +267,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;
@@ -273,7 +286,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
}
if (!await CheckIdentityFileExistsAsync())
if (!await CheckIdentityFileExistsAsync().ConfigureAwait(false))
{
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
}
@@ -296,6 +309,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return result;
}
// Requires UI thread
private async Task<bool> CheckIdentityFileExistsAsync()
{
var identityFile = _identityFile;
@@ -315,7 +329,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))
{
@@ -325,7 +340,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;
@@ -435,14 +451,14 @@ namespace FluentTerminal.App.ViewModels.Profiles
public override async Task<Tuple<bool, string>> GetUrlAsync()
{
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
@@ -459,7 +475,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("://");
@@ -1,12 +1,12 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Infrastructure;
using GalaSoft.MvvmLight;
using System;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class AboutPageViewModel : ViewModelBase
public class AboutPageViewModel : ObservableObject
{
private const string BaseUrl = "https://github.com/felixse/FluentTerminal/releases/tag/";
private readonly IUpdateService _updateService;
@@ -18,17 +18,17 @@ namespace FluentTerminal.App.ViewModels.Settings
_updateService = updateService;
_applicationView = applicationView;
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdate(true));
CheckForUpdatesCommand = new AsyncRelayCommand(() => CheckForUpdateAsync(true));
}
public IAsyncCommand CheckForUpdatesCommand { get; }
public ICommand CheckForUpdatesCommand { get; }
public string CurrentVersion
{
get
{
var version = _updateService.GetCurrentVersion();
return ConvertVersionToString(version);
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
@@ -39,12 +39,12 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _latestVersion;
set
{
if (Set(ref _latestVersion, value))
if (SetProperty(ref _latestVersion, value))
{
RaisePropertyChanged(nameof(LatestVersionFound));
RaisePropertyChanged(nameof(LatestVersionLoading));
RaisePropertyChanged(nameof(LatestVersionNotFound));
RaisePropertyChanged(nameof(LatestVersionReleaseNotesURL));
OnPropertyChanged(nameof(LatestVersionFound));
OnPropertyChanged(nameof(LatestVersionLoading));
OnPropertyChanged(nameof(LatestVersionNotFound));
OnPropertyChanged(nameof(LatestVersionReleaseNotesURL));
}
}
}
@@ -59,19 +59,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,16 +1,16 @@
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 Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class GeneralPageViewModel : ViewModelBase
public class GeneralPageViewModel : ObservableObject
{
private readonly ApplicationSettings _applicationSettings;
private readonly IDefaultValueProvider _defaultValueProvider;
@@ -40,8 +40,8 @@ namespace FluentTerminal.App.ViewModels.Settings
_applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectory);
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync);
BrowseLogDirectoryCommand = new AsyncRelayCommand(BrowseLogDirectoryAsync);
}
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
@@ -49,7 +49,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool NeedsToRestart
{
get => _needsToRestart;
set => Set(ref _needsToRestart, value);
set => SetProperty(ref _needsToRestart, value);
}
public string SelectedLanguage
@@ -62,9 +62,11 @@ 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);
}
@@ -77,7 +79,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.ShowCustomTitleInTitlebar = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -91,7 +93,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.EnableTrayIcon = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
// Toggle message telling user to restart the program
ShouldRestartForTrayMessage = !ShouldRestartForTrayMessage;
@@ -102,7 +104,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool ShouldRestartForTrayMessage
{
get => _shouldRestartForTrayMessage;
set => Set(ref _shouldRestartForTrayMessage, value);
set => SetProperty(ref _shouldRestartForTrayMessage, value);
}
public bool ShowNewOutputIndicator
@@ -114,7 +116,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.ShowNewOutputIndicator = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -128,7 +130,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.UseMoshByDefault = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -142,7 +144,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.AutoFallbackToWindowsUsernameInLinks = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -156,7 +158,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.RTrimCopiedLines = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -170,9 +172,9 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.MuteTerminalBeeps = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
_trayProcessCommunicationService.MuteTerminal(value);
_trayProcessCommunicationService.MuteTerminalAsync(value);
}
}
}
@@ -185,7 +187,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.EnableLogging = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -199,7 +201,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.PrintableOutputOnly = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -213,7 +215,21 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.LogDirectoryPath = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
public bool UseConPty
{
get => _applicationSettings.UseConPty;
set
{
if (_applicationSettings.UseConPty != value)
{
_applicationSettings.UseConPty = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
@@ -227,7 +243,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool CanEnableStartupTask
{
get => _canEnableStartupTask;
set => Set(ref _canEnableStartupTask, value);
set => SetProperty(ref _canEnableStartupTask, value);
}
public bool ConfirmClosingTabs
@@ -239,7 +255,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.ConfirmClosingTabs = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -253,7 +269,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.ConfirmClosingWindows = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -267,25 +283,39 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.NewTerminalLocation = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
OnPropertyChanged(nameof(TabIsSelected));
}
}
}
public IAsyncCommand RestoreDefaultsCommand { get; }
public bool TabWindowCascadingAppMenu
{
get => _applicationSettings.TabWindowCascadingAppMenu;
set
{
if (_applicationSettings.TabWindowCascadingAppMenu != value)
{
_applicationSettings.TabWindowCascadingAppMenu = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
OnPropertyChanged();
}
}
}
public IAsyncCommand BrowseLogDirectoryCommand { get; }
public ICommand RestoreDefaultsCommand { get; }
public ICommand BrowseLogDirectoryCommand { get; }
public bool StartupTaskEnabled
{
get => _startupTaskEnabled;
set
{
if (_startupTaskEnabled != value)
if (SetProperty(ref _startupTaskEnabled, value))
{
_startupTaskEnabled = value;
RaisePropertyChanged(nameof(StartupTaskEnabled));
SetStartupTaskState(value);
// ReSharper disable once AssignmentIsFullyDiscarded
_ = SetStartupTaskStateAsync(value);
}
}
}
@@ -293,13 +323,13 @@ namespace FluentTerminal.App.ViewModels.Settings
public string StartupTaskErrorMessage
{
get => _startupTaskErrorMessage;
set => Set(ref _startupTaskErrorMessage, value);
set => SetProperty(ref _startupTaskErrorMessage, value);
}
public bool TabIsSelected
{
get => NewTerminalLocation == NewTerminalLocation.Tab;
set { if (value) NewTerminalLocation = NewTerminalLocation.Tab; }
set => NewTerminalLocation = value ? NewTerminalLocation.Tab : NewTerminalLocation.Window;
}
public TabsPosition TabsPosition
@@ -311,7 +341,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.TabsPosition = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -331,7 +361,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.UnderlineSelectedTab = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -357,20 +387,32 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.InactiveTabColorMode = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
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);
OnPropertyChanged();
}
}
}
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)
{
@@ -390,9 +432,12 @@ namespace FluentTerminal.App.ViewModels.Settings
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)
@@ -420,27 +465,39 @@ 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);
}
private async Task BrowseLogDirectory()
// Requires UI thread
private async Task BrowseLogDirectoryAsync()
{
var folder = await _fileSystemService.BrowseForDirectory();
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var folder = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (folder != null)
{
@@ -1,14 +1,15 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class KeyBindingViewModel : ViewModelBase
public class KeyBindingViewModel : ObservableObject
{
private readonly IDialogService _dialogService;
@@ -17,10 +18,11 @@ namespace FluentTerminal.App.ViewModels.Settings
Model = keyBinding;
Parent = parent;
_dialogService = dialogService;
EditCommand = new AsyncCommand(Edit);
DeleteCommand = new AsyncCommand(Delete);
EditCommand = new AsyncRelayCommand(EditAsync);
DeleteCommand = new AsyncRelayCommand(DeleteAsync);
}
// Needs to be triggered from the UI thread
public event EventHandler Deleted;
public event EventHandler Edited;
@@ -35,7 +37,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Meta != value)
{
Model.Meta = value;
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -48,7 +50,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Alt != value)
{
Model.Alt = value;
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -61,13 +63,13 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Ctrl != value)
{
Model.Ctrl = value;
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
public IAsyncCommand DeleteCommand { get; }
public IAsyncCommand EditCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand EditCommand { get; }
public int Key
{
@@ -77,7 +79,7 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Key != value)
{
Model.Key = value;
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -92,13 +94,15 @@ namespace FluentTerminal.App.ViewModels.Settings
if (Model.Shift != value)
{
Model.Shift = value;
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
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 +121,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)
{
@@ -2,17 +2,18 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class KeyBindingsPageViewModel : ViewModelBase
public class KeyBindingsPageViewModel : ObservableObject
{
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
@@ -23,22 +24,22 @@ namespace FluentTerminal.App.ViewModels.Settings
_settingsService = settingsService;
_dialogService = dialogService;
_trayProcessCommunicationService = trayProcessCommunicationService;
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
AddCommand = new RelayCommand<string>(async command => await Add(command).ConfigureAwait(false));
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync);
AddCommand = new AsyncRelayCommand<string>(AddAsync);
Initialize(_settingsService.GetCommandKeyBindings());
}
public RelayCommand<string> AddCommand { get; }
public ICommand AddCommand { get; }
public ObservableCollection<KeyBindingsViewModel> KeyBindings { get; } = new ObservableCollection<KeyBindingsViewModel>();
public RelayCommand RestoreDefaultsCommand { get; }
public ICommand 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)
@@ -49,6 +50,7 @@ namespace FluentTerminal.App.ViewModels.Settings
KeyBindings.Clear();
}
// Requires UI thread
private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings)
{
ClearKeyBindings();
@@ -69,12 +71,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)
{
@@ -1,6 +1,6 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels.Settings
{
public class KeyBindingsViewModel : ViewModelBase
public class KeyBindingsViewModel : ObservableObject
{
private readonly IDialogService _dialogService;
private bool _editable;
@@ -37,26 +37,30 @@ namespace FluentTerminal.App.ViewModels.Settings
public bool Editable
{
get => _editable;
set => Set(ref _editable, value);
set => SetProperty(ref _editable, value);
}
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);
@@ -2,13 +2,14 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class MousePageViewModel : ViewModelBase
public class MousePageViewModel : ObservableObject
{
private readonly ISettingsService _settingsService;
private readonly IDialogService _dialogService;
@@ -22,7 +23,7 @@ namespace FluentTerminal.App.ViewModels.Settings
_defaultValueProvider = defaultValueProvider;
_applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync);
}
public bool CopyOnSelect
@@ -34,7 +35,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.CopyOnSelect = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -48,7 +49,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.MouseRightClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -71,6 +72,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;
@@ -80,7 +87,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_applicationSettings.MouseMiddleClickAction = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -103,11 +110,21 @@ namespace FluentTerminal.App.ViewModels.Settings
set { if (value) MouseMiddleClickAction = MouseAction.Paste; }
}
public RelayCommand RestoreDefaultsCommand { get; }
private async Task RestoreDefaults()
public bool MouseMiddleClickCopySelectionOrPasteIsSelected
{
var result = await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), I18N.Translate("ConfirmRestoreMouseSettings"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
get => MouseMiddleClickAction == MouseAction.CopySelectionOrPaste;
set { if (value) MouseMiddleClickAction = MouseAction.CopySelectionOrPaste; }
}
public ICommand RestoreDefaultsCommand { get; }
// Requires UI thread
private async Task RestoreDefaultsAsync()
{
// 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)
{
@@ -1,15 +1,16 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class ProfilesPageViewModel : ViewModelBase
public class ProfilesPageViewModel : ObservableObject
{
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IDialogService _dialogService;
@@ -79,9 +80,9 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public RelayCommand CreateShellProfileCommand { get; }
public ICommand CreateShellProfileCommand { get; }
public RelayCommand<ShellProfileViewModel> CloneCommand { get; }
public ICommand CloneCommand { get; }
private void CreateShellProfile()
{
@@ -90,7 +91,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);
@@ -121,7 +123,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public ShellProfileViewModel SelectedShellProfile
{
get => _selectedShellProfile;
set => Set(ref _selectedShellProfile, value);
set => SetProperty(ref _selectedShellProfile, value);
}
public ObservableCollection<ShellProfileViewModel> ShellProfiles { get; } = new ObservableCollection<ShellProfileViewModel>();
@@ -1,15 +1,16 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class SshProfilesPageViewModel: ViewModelBase
public class SshProfilesPageViewModel: ObservableObject
{
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
@@ -52,20 +53,20 @@ 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);
}
}
public RelayCommand CreateSshProfileCommand { get; }
public ICommand CreateSshProfileCommand { get; }
public RelayCommand<SshProfileViewModel> CloneCommand { get; }
public ICommand CloneCommand { get; }
public void CreateSshProfile()
{
@@ -75,7 +76,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);
@@ -111,7 +113,7 @@ namespace FluentTerminal.App.ViewModels.Settings
public SshProfileViewModel SelectedSshProfile
{
get => _selectedShellProfile;
set => Set(ref _selectedShellProfile, value);
set => SetProperty(ref _selectedShellProfile, value);
}
public ObservableCollection<SshProfileViewModel> SshProfiles { get; } = new ObservableCollection<SshProfileViewModel>();
@@ -3,15 +3,16 @@ using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class TerminalPageViewModel : ViewModelBase
public class TerminalPageViewModel : ObservableObject
{
private readonly TerminalOptions _terminalOptions;
private readonly ISettingsService _settingsService;
@@ -63,7 +64,21 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.CursorBlink = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
public bool UseAcrylicBackground
{
get => _terminalOptions.UseAcrylicBackground;
set
{
if (_terminalOptions.UseAcrylicBackground != value)
{
_terminalOptions.UseAcrylicBackground = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged();
}
}
}
@@ -77,7 +92,21 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontFamily = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
public int FontWeight
{
get => _terminalOptions.FontWeight;
set
{
if (_terminalOptions.FontWeight != value)
{
_terminalOptions.FontWeight = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
OnPropertyChanged();
}
}
}
@@ -91,7 +120,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.BackgroundOpacity = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -105,7 +134,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.Padding = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -125,12 +154,12 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.ScrollBackLimit = intValue;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
else
{
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -145,35 +174,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.WordSeparator = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public bool BoldText
{
get => _terminalOptions.BoldText;
set
{
if (_terminalOptions.BoldText != value)
{
_terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public bool ShowTextCopied
{
get => _terminalOptions.ShowTextCopied;
set
{
if (_terminalOptions.ShowTextCopied != value)
{
_terminalOptions.ShowTextCopied = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -189,14 +190,14 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.FontSize = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
public IEnumerable<int> Sizes { get; }
public RelayCommand RestoreDefaultsCommand { get; }
public ICommand RestoreDefaultsCommand { get; }
private CursorStyle CursorStyle
{
@@ -207,7 +208,7 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.CursorStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
@@ -221,14 +222,18 @@ namespace FluentTerminal.App.ViewModels.Settings
{
_terminalOptions.ScrollBarStyle = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
OnPropertyChanged();
}
}
}
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)
{
@@ -238,10 +243,9 @@ 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;
}
}
@@ -252,7 +256,7 @@ namespace FluentTerminal.App.ViewModels.Settings
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
RestoreDefaultsCommand = new AsyncRelayCommand(RestoreDefaultsAsync);
Fonts = systemFontService.GetSystemFontFamilies().OrderBy(s => s.Name);
Sizes = Enumerable.Range(2, 72);
@@ -1,18 +1,20 @@
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;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.Toolkit.Mvvm.Messaging;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Settings
{
public class ThemesPageViewModel : ViewModelBase
public class ThemesPageViewModel : ObservableObject,
IRecipient<TerminalOptionsChangedMessage>
{
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IDialogService _dialogService;
@@ -21,30 +23,36 @@ 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 AsyncCommand(ImportTheme);
ImportThemeCommand = new AsyncRelayCommand(ImportThemeAsync);
CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme);
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,16 +64,18 @@ namespace FluentTerminal.App.ViewModels.Settings
}
SelectedTheme = Themes.First(t => t.IsActive);
WeakReferenceMessenger.Default.RegisterAll(this);
}
public RelayCommand CreateThemeCommand { get; }
public IAsyncCommand ImportThemeCommand { get; }
public RelayCommand<ThemeViewModel> CloneCommand { get; set; }
public ICommand CreateThemeCommand { get; }
public ICommand ImportThemeCommand { get; }
public ICommand CloneCommand { get; set; }
public double BackgroundOpacity
{
get => _backgroundOpacity;
set => Set(ref _backgroundOpacity, value);
set => SetProperty(ref _backgroundOpacity, value);
}
public ThemeViewModel SelectedTheme
@@ -76,11 +86,17 @@ namespace FluentTerminal.App.ViewModels.Settings
if (_selectedTheme != null)
{
_selectedTheme.BackgroundChanged -= OnSelectedThemeBackgroundChanged;
_selectedTheme.BackgroundImageChanged -= OnSelectedThemeBackgroundImageChanged;
}
Set(ref _selectedTheme, value);
SetProperty(ref _selectedTheme, value);
SelectedThemeChanged?.Invoke(this, _selectedTheme);
if (value != null)
{
_selectedTheme.BackgroundOpacity = BackgroundOpacity;
value.BackgroundChanged += OnSelectedThemeBackgroundChanged;
value.BackgroundImageChanged += OnSelectedThemeBackgroundImageChanged;
}
}
}
@@ -113,28 +129,49 @@ namespace FluentTerminal.App.ViewModels.Settings
AddTheme(theme);
}
private async Task 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);
}
}
}
@@ -143,7 +180,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;
@@ -183,7 +220,7 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
public void Receive(TerminalOptionsChangedMessage message)
{
BackgroundOpacity = message.TerminalOptions.BackgroundOpacity;
}
@@ -192,5 +229,10 @@ namespace FluentTerminal.App.ViewModels.Settings
{
SelectedThemeBackgroundColorChanged?.Invoke(this, e);
}
private void OnSelectedThemeBackgroundImageChanged(object sender, ImageFile e)
{
SelectedThemeBackgroundImageChanged?.Invoke(this, e);
}
}
}
@@ -1,22 +1,23 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.ViewModels.Settings;
using GalaSoft.MvvmLight;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using System;
namespace FluentTerminal.App.ViewModels
{
public class SettingsViewModel : ViewModelBase
public class SettingsViewModel : ObservableObject
{
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IApplicationView applicationView,
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers)
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers,
IImageFileSystemService imageFileSystemService)
{
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);
@@ -1,12 +1,12 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using GalaSoft.MvvmLight.Command;
using System;
using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.App.ViewModels.Profiles;
using System.Windows.Input;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels
{
@@ -28,7 +28,7 @@ namespace FluentTerminal.App.ViewModels
public bool IsDefault
{
get => _isDefault;
set => Set(ref _isDefault, value);
set => SetProperty(ref _isDefault, value);
}
#endregion Properties
@@ -41,9 +41,9 @@ namespace FluentTerminal.App.ViewModels
#region Commands
public RelayCommand SetDefaultCommand { get; }
public ICommand SetDefaultCommand { get; }
public IAsyncCommand RestoreDefaultsCommand { get; }
public ICommand RestoreDefaultsCommand { get; }
#endregion Commands
@@ -60,21 +60,23 @@ namespace FluentTerminal.App.ViewModels
shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
RestoreDefaultsCommand = new AsyncRelayCommand(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)
@@ -0,0 +1,31 @@
using FluentTerminal.Models;
using Microsoft.Toolkit.Mvvm.ComponentModel;
namespace FluentTerminal.App.ViewModels
{
public class TabThemeViewModel : ObservableObject
{
private bool _isSelected;
public TabThemeViewModel(TabTheme theme, TerminalViewModel terminal)
{
Theme = theme;
Terminal = terminal;
}
public bool IsSelected
{
get => _isSelected;
set
{
if (SetProperty(ref _isSelected, value) && value)
{
Terminal.TabTheme = this;
}
}
}
public TabTheme Theme { get; }
public TerminalViewModel Terminal { get; }
}
}
+419 -135
View File
@@ -1,22 +1,29 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Newtonsoft.Json;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.UI.Core;
using FluentTerminal.Models.Messages;
using System.Windows.Input;
using FluentTerminal.App.ViewModels.Menu;
using System.Collections.Generic;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Mvvm.Input;
namespace FluentTerminal.App.ViewModels
{
public class TerminalViewModel : ViewModelBase
public class TerminalViewModel : ObservableObject,
IRecipient<ApplicationSettingsChangedMessage>,
IRecipient<CurrentThemeChangedMessage>,
IRecipient<TerminalOptionsChangedMessage>,
IRecipient<KeyBindingsChangedMessage>
{
#region Static
@@ -25,21 +32,26 @@ 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 TabTheme TabTheme { 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> Serialize()
public async Task<string> SerializeAsync()
{
TerminalState state = new TerminalState
{
@@ -47,12 +59,16 @@ namespace FluentTerminal.App.ViewModels
ShellTitle = ShellTitle,
TabTitle = TabTitle,
TerminalTheme = TerminalTheme,
TabTheme = TabTheme,
TabThemeId = TabTheme.Theme.Id,
ShowSearchPanel = ShowSearchPanel,
SearchText = SearchText,
XtermBufferState = await SerializeXtermState(),
SearchMatchCase = SearchMatchCase,
SearchWholeWord = SearchWholeWord,
SearchWithRegex = SearchWithRegex,
XtermBufferState = await SerializeXtermStateAsync().ConfigureAwait(false),
TerminalId = Terminal.Id,
ShellProfile = ShellProfile
ShellProfile = ShellProfile,
FontSize = FontSize
};
return JsonConvert.SerializeObject(state);
@@ -67,12 +83,17 @@ namespace FluentTerminal.App.ViewModels
ShellTitle = state.ShellTitle;
TabTitle = state.TabTitle;
TerminalTheme = state.TerminalTheme;
TabTheme = state.TabTheme;
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;
}
}
@@ -85,22 +106,27 @@ namespace FluentTerminal.App.ViewModels
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, string terminalState = null)
IApplicationView applicationView, IClipboardService clipboardService, string terminalState = null)
{
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
SettingsService = settingsService;
_terminalOptions = SettingsService.GetTerminalOptions();
@@ -116,24 +142,35 @@ 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);
CloseCommand = new AsyncCommand(CloseTab, CanExecuteCommand);
TabThemes = new ObservableCollection<TabThemeViewModel>(SettingsService.GetTabThemes().Select(theme => new TabThemeViewModel(theme, this)));
CloseCommand = new AsyncRelayCommand(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);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme, CanExecuteCommand);
EditTitleCommand = new AsyncCommand(EditTitle, CanExecuteCommand);
EditTitleCommand = new AsyncRelayCommand(EditTitleAsync, CanExecuteCommand);
DuplicateTabCommand = new RelayCommand(DuplicateTab, CanExecuteCommand);
ReconnectTabCommand = new AsyncRelayCommand(ReconnectTabAsync, () => CanExecuteCommand() && HasExitedWithError);
CopyCommand = new AsyncRelayCommand(Copy, () => HasSelection);
PasteCommand = new AsyncRelayCommand(Paste);
CopyLinkCommand = new AsyncRelayCommand(() => CopyTextAsync(HoveredUri), () => !string.IsNullOrWhiteSpace(HoveredUri));
ShowSearchPanelCommand = new RelayCommand(() => ShowSearchPanel = true, () => !ShowSearchPanel);
if (!String.IsNullOrEmpty(terminalState))
FontSize = _terminalOptions.FontSize;
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;
@@ -143,8 +180,22 @@ namespace FluentTerminal.App.ViewModels
Terminal.Exited += Terminal_Exited;
Terminal.Closed += Terminal_Closed;
Overlay = new OverlayViewModel(dispatcherTimer);
ContextMenu = BuidContextMenu();
TabContextMenu = BuildTabContextMenu();
WeakReferenceMessenger.Default.RegisterAll(this);
}
public MenuViewModel ContextMenu
{
get => _contextMenu;
set => SetProperty(ref _contextMenu, value);
}
public MenuViewModel TabContextMenu
{
get => _tabContextMenu;
set => SetProperty(ref _tabContextMenu, value);
}
private bool _disposalRequested;
@@ -161,68 +212,104 @@ namespace FluentTerminal.App.ViewModels
public event EventHandler Activated;
public event EventHandler Closed;
public event EventHandler<string> FindNextRequested;
public event EventHandler<string> FindPreviousRequested;
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 string XtermBufferState { get; private set; }
public AsyncCommand CloseCommand { get; private set; }
public ICommand CloseCommand { get; private set; }
public RelayCommand CloseRightTabsCommand { get; }
public ICommand CloseRightTabsCommand { get; }
public RelayCommand CloseLeftTabsCommand { get; }
public ICommand CloseLeftTabsCommand { get; }
public RelayCommand CloseOtherTabsCommand { get; }
public ICommand CloseOtherTabsCommand { get; }
public RelayCommand CloseSearchPanelCommand { get; }
public ICommand CloseSearchPanelCommand { get; }
public IDialogService DialogService { get; }
public IAsyncCommand EditTitleCommand { get; }
public ICommand EditTitleCommand { get; }
public RelayCommand FindNextCommand { get; }
public ICommand FindNextCommand { get; }
public RelayCommand FindPreviousCommand { get; }
public ICommand FindPreviousCommand { get; }
public RelayCommand DuplicateTabCommand { get; }
public ICommand DuplicateTabCommand { get; }
public AsyncRelayCommand 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;
set
{
if (Set(ref _isSelected, value))
if (SetProperty(ref _isSelected, value))
{
if (IsSelected)
{
_keyboardCommandService.RegisterCommandHandler(nameof(Command.Search), () => ShowSearchPanel = true);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseSearch), () => CloseSearchPanel());
HasNewOutput = false;
}
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
RaisePropertyChanged(nameof(ShowCloseButton));
else
{
_keyboardCommandService.DeregisterCommandHandler(nameof(Command.Search));
_keyboardCommandService.DeregisterCommandHandler(nameof(Command.CloseSearch));
}
OnPropertyChanged(nameof(IsUnderlined));
OnPropertyChanged(nameof(BackgroundTabTheme));
OnPropertyChanged(nameof(ShowCloseButton));
}
}
}
public bool HasSelection
{
get => _hasSelection;
set => SetProperty(ref _hasSelection, value);
}
public string HoveredUri
{
get => _hoveredUri;
set
{
if (SetProperty(ref _hoveredUri, value))
{
ContextMenu = BuidContextMenu();
}
}
}
@@ -232,9 +319,9 @@ namespace FluentTerminal.App.ViewModels
get => _isHovered;
set
{
if (Set(ref _isHovered, value))
if (SetProperty(ref _isHovered, value))
{
RaisePropertyChanged(nameof(ShowCloseButton));
OnPropertyChanged(nameof(ShowCloseButton));
}
}
}
@@ -245,12 +332,12 @@ namespace FluentTerminal.App.ViewModels
}
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
{
get => _hasNewOutput;
set => Set(ref _hasNewOutput, value);
set => SetProperty(ref _hasNewOutput, value);
}
public bool HasExitedWithError
@@ -258,20 +345,37 @@ namespace FluentTerminal.App.ViewModels
get => _hasExitedWithError;
set
{
if (Set(ref _hasExitedWithError, value) && value)
if (SetProperty(ref _hasExitedWithError, value) && value)
{
HasNewOutput = false;
}
ReconnectTabCommand.NotifyCanExecuteChanged();
}
}
public string SearchText
{
get => _searchText;
set => Set(ref _searchText, value);
set => SetProperty(ref _searchText, value);
}
public RelayCommand<string> SelectTabThemeCommand { get; }
public bool SearchMatchCase
{
get => _searchMatchCase;
set => SetProperty(ref _searchMatchCase, value);
}
public bool SearchWholeWord
{
get => _searchWholeWord;
set => SetProperty(ref _searchWholeWord, value);
}
public bool SearchWithRegex
{
get => _searchWithRegex;
set => SetProperty(ref _searchWithRegex, value);
}
public ISettingsService SettingsService { get; }
@@ -280,30 +384,35 @@ namespace FluentTerminal.App.ViewModels
public bool ShowSearchPanel
{
get => _showSearchPanel;
set => Set(ref _showSearchPanel, value);
set => SetProperty(ref _showSearchPanel, value);
}
public TabTheme TabTheme
public bool SearchHasFocus { get; set; }
public TabThemeViewModel TabTheme
{
get => _tabTheme;
set
{
Set(ref _tabTheme, value);
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
SetProperty(ref _tabTheme, value);
OnPropertyChanged(nameof(IsUnderlined));
OnPropertyChanged(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
{
get => _terminalTheme;
set => Set(ref _terminalTheme, value);
set => SetProperty(ref _terminalTheme, value);
}
public bool Initialized { get; set; }
@@ -334,10 +443,8 @@ namespace FluentTerminal.App.ViewModels
}
}
if (Set(ref _tabTitle, title))
{
CustomTitleChanged?.Invoke(this, title);
}
SetProperty(ref _tabTitle, title);
ShellProfile.Name = title;
}
}
@@ -346,34 +453,50 @@ namespace FluentTerminal.App.ViewModels
get => _shellTitle;
set
{
if (Set(ref _shellTitle, value) && !_hasCustomTitle)
if (SetProperty(ref _shellTitle, value) && !_hasCustomTitle)
{
TabTitle = value;
}
}
}
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task Close()
public int FontSize
{
MessengerInstance.Unregister(this);
return Terminal.Close();
}
public void CopyText(string text)
{
ClipboardService.SetText(text);
if (_terminalOptions.ShowTextCopied)
get => _fontSize;
set
{
Overlay.Show(I18N.Translate("TextCopied"));
_fontSize = value > 0 ? value : 1;
}
}
public async Task EditTitle()
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task CloseAsync()
{
var result = await DialogService.ShowInputDialogAsync(I18N.Translate("EditTitleString"));
//MessengerInstance.Unregister(this); // todo necessary?
return Terminal.CloseAsync();
}
public Task CopyTextAsync(string text)
{
return ApplicationView.ExecuteOnUiThreadAsync(() =>
{
ClipboardService.SetText(text);
if (ApplicationSettings.ShowTextCopied)
{
Overlay.Show(I18N.Translate("TextCopied"));
}
});
}
// Requires UI thread
public async Task EditTitleAsync()
{
// 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))
@@ -411,11 +534,6 @@ namespace FluentTerminal.App.ViewModels
return Initialized && !_disposalRequested;
}
private async Task CloseTab()
{
await TryClose().ConfigureAwait(false);
}
private void CloseLeftTabs()
{
CloseLeftTabsRequested?.Invoke(this, EventArgs.Empty);
@@ -433,19 +551,19 @@ namespace FluentTerminal.App.ViewModels
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; }
public async Task<string> SerializeXtermState()
private Task<string> SerializeXtermStateAsync()
{
return await TerminalView?.SerializeXtermState();
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 void DuplicateTab()
@@ -453,38 +571,61 @@ namespace FluentTerminal.App.ViewModels
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
}
private async void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
public async Task ReconnectTabAsync()
{
await ApplicationView.RunOnDispatcherThread(() =>
HasExitedWithError = false;
await TerminalView?.ReconnectAsync();
}
public void Receive(ApplicationSettingsChangedMessage message)
{
ApplicationSettings = message.ApplicationSettings;
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
ApplicationSettings = message.ApplicationSettings;
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
OnPropertyChanged(nameof(IsUnderlined));
OnPropertyChanged(nameof(BackgroundTabTheme));
});
}
private async void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
public void Receive(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(message.ThemeId);
TerminalTheme = currentTheme;
ThemeChanged?.Invoke(this, currentTheme);
}
});
});
}
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
public void Receive(TerminalOptionsChangedMessage message)
{
_terminalOptions = message.TerminalOptions;
ApplicationView.ExecuteOnUiThreadAsync(() => OnPropertyChanged(nameof(BackgroundOpacity)), CoreDispatcherPriority.Low);
}
private void SelectTabTheme(string id)
public void Receive(KeyBindingsChangedMessage message)
{
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
var contextMenu = BuidContextMenu();
if (!contextMenu.EquivalentTo(ContextMenu))
{
ContextMenu = contextMenu;
}
var tabContextMenu = BuildTabContextMenu();
if (!tabContextMenu.EquivalentTo(TabContextMenu))
{
TabContextMenu = tabContextMenu;
}
}, CoreDispatcherPriority.Low, true);
}
private void Terminal_Exited(object sender, int exitCode)
@@ -494,13 +635,12 @@ namespace FluentTerminal.App.ViewModels
tracker.SetInvalid();
}
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
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;
@@ -515,43 +655,68 @@ namespace FluentTerminal.App.ViewModels
{
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
CopyText(selection);
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(() =>
{
ShowSearchPanel = true;
SearchStarted?.Invoke(this, EventArgs.Empty);
}).ConfigureAwait(false);
return;
}
case nameof(Command.CloseSearch):
{
await ApplicationView.ExecuteOnUiThreadAsync(() =>
{
CloseSearchPanel();
}).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;
}
}
}
@@ -565,39 +730,158 @@ namespace FluentTerminal.App.ViewModels
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 (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
tracker.SetInvalid();
}
if (ApplicationSettings.ConfirmClosingTabs)
if (!ApplicationSettings.ConfirmClosingTabs || await DialogService
.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
string.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK,
DialogButton.Cancel).ConfigureAwait(false) == DialogButton.OK)
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), String.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
await CloseAsync().ConfigureAwait(false);
}
}
if (result == DialogButton.Cancel)
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)
{
return;
TerminalView.Paste(content);
}
}
}
await Close().ConfigureAwait(true);
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());
}
}
}
}
+244 -119
View File
@@ -1,15 +1,16 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels
{
public class ThemeViewModel : ViewModelBase
public class ThemeViewModel : ObservableObject
{
private readonly IDialogService _dialogService;
private readonly ISettingsService _settingsService;
@@ -41,16 +42,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 +91,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 AsyncRelayCommand(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 AsyncRelayCommand(CancelEditAsync);
SaveChangesCommand = new AsyncRelayCommand(SaveChangesAsync);
ExportCommand = new AsyncRelayCommand(Export, NotPreInstalled);
ChooseBackgroundImageCommand = new AsyncRelayCommand(ChooseBackgroundImageAsync, NotPreInstalled);
DeleteBackgroundImageCommand = new AsyncRelayCommand(DeleteBackgroundImageAsync, NotPreInstalled);
}
public event EventHandler Activated;
@@ -98,7 +112,7 @@ namespace FluentTerminal.App.ViewModels
public string Author
{
get => _author;
set => Set(ref _author, value);
set => SetProperty(ref _author, value);
}
public string Background
@@ -106,7 +120,7 @@ namespace FluentTerminal.App.ViewModels
get => _background;
set
{
Set(ref _background, value);
SetProperty(ref _background, value);
BackgroundChanged?.Invoke(this, value);
}
}
@@ -114,103 +128,103 @@ namespace FluentTerminal.App.ViewModels
public double BackgroundOpacity
{
get => _backgroundOpacity;
set => Set(ref _backgroundOpacity, value);
set => SetProperty(ref _backgroundOpacity, value);
}
public string Black
{
get => _black;
set => Set(ref _black, value);
set => SetProperty(ref _black, value);
}
public string Blue
{
get => _blue;
set => Set(ref _blue, value);
set => SetProperty(ref _blue, value);
}
public string BrightBlack
{
get => _brightBlack;
set => Set(ref _brightBlack, value);
set => SetProperty(ref _brightBlack, value);
}
public string BrightBlue
{
get => _brightBlue;
set => Set(ref _brightBlue, value);
set => SetProperty(ref _brightBlue, value);
}
public string BrightCyan
{
get => _brightCyan;
set => Set(ref _brightCyan, value);
set => SetProperty(ref _brightCyan, value);
}
public string BrightGreen
{
get => _brightGreen;
set => Set(ref _brightGreen, value);
set => SetProperty(ref _brightGreen, value);
}
public string BrightMagenta
{
get => _brightMagenta;
set => Set(ref _brightMagenta, value);
set => SetProperty(ref _brightMagenta, value);
}
public string BrightRed
{
get => _brightRed;
set => Set(ref _brightRed, value);
set => SetProperty(ref _brightRed, value);
}
public string BrightWhite
{
get => _brightWhite;
set => Set(ref _brightWhite, value);
set => SetProperty(ref _brightWhite, value);
}
public string BrightYellow
{
get => _brightYellow;
set => Set(ref _brightYellow, value);
set => SetProperty(ref _brightYellow, value);
}
public RelayCommand CancelEditCommand { get; }
public ICommand CancelEditCommand { get; }
public string Cursor
{
get => _cursor;
set => Set(ref _cursor, value);
set => SetProperty(ref _cursor, value);
}
public string CursorAccent
{
get => _cursorAccent;
set => Set(ref _cursorAccent, value);
set => SetProperty(ref _cursorAccent, value);
}
public string Cyan
{
get => _cyan;
set => Set(ref _cyan, value);
set => SetProperty(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
{
get => _foreground;
set => Set(ref _foreground, value);
set => SetProperty(ref _foreground, value);
}
public string Green
{
get => _green;
set => Set(ref _green, value);
set => SetProperty(ref _green, value);
}
public Guid Id { get; }
@@ -218,56 +232,71 @@ namespace FluentTerminal.App.ViewModels
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
set => SetProperty(ref _inEditMode, value);
}
public bool IsActive
{
get => _isActive;
set => Set(ref _isActive, value);
set => SetProperty(ref _isActive, value);
}
public string Magenta
{
get => _magenta;
set => Set(ref _magenta, value);
set => SetProperty(ref _magenta, value);
}
public string Name
{
get => _name;
set => Set(ref _name, value);
set => SetProperty(ref _name, value);
}
public string Red
{
get => _red;
set => Set(ref _red, value);
set => SetProperty(ref _red, value);
}
public RelayCommand SaveChangesCommand { get; }
public ICommand SaveChangesCommand { get; }
public string Selection
{
get => _selection;
set => Set(ref _selection, value);
set => SetProperty(ref _selection, value);
}
public RelayCommand SetActiveCommand { get; }
public ICommand SetActiveCommand { get; }
public string White
{
get => _white;
set => Set(ref _white, value);
set => SetProperty(ref _white, value);
}
public string Yellow
{
get => _yellow;
set => Set(ref _yellow, value);
set => SetProperty(ref _yellow, value);
}
public void SaveChanges()
public ImageFile BackgroundThemeFile
{
get => _backgroundThemeFile;
set
{
SetProperty(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 +325,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 +441,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 +486,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);
}
}
}
}
}
+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,7 +25,7 @@ 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);
}
@@ -48,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;
}
@@ -79,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,43 +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();
}
private void OnDispatcherTimerTick(object sender, object e)
{
Tick?.Invoke(this, EventArgs.Empty);
}
public TimeSpan Interval
{
get => _dispatcherTimer.Interval;
set => _dispatcherTimer.Interval = value;
}
public bool IsEnabled => _dispatcherTimer.IsEnabled;
public event EventHandler<object> Tick;
public void Start()
{
_dispatcherTimer.Tick += OnDispatcherTimerTick;
_dispatcherTimer.Start();
}
public void Stop()
{
_dispatcherTimer.Stop();
_dispatcherTimer.Tick -= OnDispatcherTimerTick;
}
}
}
@@ -38,11 +38,10 @@ namespace FluentTerminal.App.Adapters
_messageDialog.Commands.Add(command);
}
public async Task<DialogButton> ShowAsync()
public Task<DialogButton> ShowAsync()
{
var result = await _messageDialog.ShowAsync();
return (DialogButton)result.Id;
return _messageDialog.ShowAsync().AsTask().ContinueWith(t => (DialogButton) t.Result.Id,
TaskContinuationOptions.OnlyOnRanToCompletion);
}
}
}
+2
View File
@@ -41,6 +41,8 @@
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
<converters:I18NConverter x:Key="I18NConverter" />
<converters:NullToCollapsedConverter x:Key="NullToCollapsedConverter" />
<converters:MenuViewModelToFlyoutMenuConverter x:Key="MenuViewModelToFlyoutMenuConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
+270 -267
View File
@@ -27,7 +27,6 @@ using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.ApplicationModel.Core;
using Windows.Storage;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
@@ -37,7 +36,7 @@ using IContainer = Autofac.IContainer;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
using Microsoft.Toolkit.Mvvm.Messaging;
namespace FluentTerminal.App
{
@@ -48,6 +47,7 @@ namespace FluentTerminal.App
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IDialogService _dialogService;
private readonly INotificationService _notificationService;
private bool _alreadyLaunched;
private bool _isLaunching;
private ApplicationSettings _applicationSettings;
@@ -68,6 +68,9 @@ namespace FluentTerminal.App
UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += (sender, e) =>
Logger.Instance.Error(e.Exception, "Unobserved Task Exception");
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
@@ -78,8 +81,11 @@ namespace FluentTerminal.App
SshProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.SshProfilesContainerName, ApplicationDataCreateDisposition.Always)),
HistoryContainer = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ExecutedCommandsContainerName, ApplicationDataCreateDisposition.Always))
};
var builder = new ContainerBuilder();
builder.RegisterInstance(applicationDataContainers);
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
builder.RegisterType<CommandHistoryService>().As<ICommandHistoryService>().SingleInstance();
builder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>().SingleInstance();
builder.RegisterType<TrayProcessCommunicationService>().As<ITrayProcessCommunicationService>().SingleInstance();
builder.RegisterType<DialogService>().As<IDialogService>().SingleInstance();
@@ -93,9 +99,8 @@ namespace FluentTerminal.App
builder.RegisterType<FluentTerminalThemeParser>().As<IThemeParser>().SingleInstance();
builder.RegisterType<ClipboardService>().As<IClipboardService>().SingleInstance();
builder.RegisterType<FileSystemService>().As<IFileSystemService>().SingleInstance();
builder.RegisterType<ImageFileSystemService>().As<IImageFileSystemService>().SingleInstance();
builder.RegisterType<SystemFontService>().As<ISystemFontService>().SingleInstance();
builder.RegisterType<ShellProfileSelectionDialog>().As<IShellProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<SshProfileSelectionDialog>().As<ISshProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<CreateKeyBindingDialog>().As<ICreateKeyBindingDialog>().InstancePerDependency();
builder.RegisterType<InputDialog>().As<IInputDialog>().InstancePerDependency();
builder.RegisterType<AboutDialog>().As<IAboutDialog>().InstancePerDependency();
@@ -103,17 +108,16 @@ namespace FluentTerminal.App
builder.RegisterType<SshInfoDialog>().As<ISshConnectionInfoDialog>().InstancePerDependency();
builder.RegisterType<CustomCommandDialog>().As<ICustomCommandDialog>().InstancePerDependency();
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
builder.RegisterType<ApplicationLanguageService>().As<IApplicationLanguageService>().SingleInstance();
builder.RegisterType<ShellProfileMigrationService>().As<IShellProfileMigrationService>().SingleInstance();
builder.RegisterInstance(applicationDataContainers);
_container = builder.Build();
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
WeakReferenceMessenger.Default.Register<App, ApplicationSettingsChangedMessage>(this, (r, m) => r.OnApplicationSettingsChanged(m));
_settingsService = _container.Resolve<ISettingsService>();
_notificationService = _container.Resolve<INotificationService>();
var shellProfileMigrationService = _container.Resolve<IShellProfileMigrationService>();
foreach (var profile in _settingsService.GetShellProfiles())
@@ -183,11 +187,194 @@ namespace FluentTerminal.App
protected override async void OnActivated(IActivatedEventArgs args)
{
if (args is ProtocolActivatedEventArgs protocolActivated)
await OnLaunchOrActivate(args);
base.OnActivated(args);
}
private async Task ParseCommandLineArgumentsAsync(object verb, CommandLineActivatedEventArgs commandLineActivated)
{
using var deferral = commandLineActivated.Operation.GetDeferral();
if (verb is SettingsVerb settingsVerb)
{
if (string.IsNullOrWhiteSpace(settingsVerb.Import) && !settingsVerb.Export)
{
await ShowSettingsAsync();
}
else if (settingsVerb.Export)
{
var settings = _settingsService.ExportSettings();
var path = Path.Combine(commandLineActivated.Operation.CurrentDirectoryPath, "config.json");
try
{
await StartSystemTray().ConfigureAwait(false);
await _trayProcessCommunicationService.SaveTextFileAsync(path, settings).ConfigureAwait(false);
_notificationService.ShowNotification("Export settings", $"Settings were exported to {path}.");
}
catch (Exception e)
{
_notificationService.ShowNotification("Export settings", $"Failed with error: {e.Message}");
commandLineActivated.Operation.ExitCode = 1;
}
if (!_isLaunching && !_alreadyLaunched)
{
Exit();
}
}
else if (!string.IsNullOrWhiteSpace(settingsVerb.Import))
{
var path = Path.Combine(commandLineActivated.Operation.CurrentDirectoryPath, settingsVerb.Import.Replace(".\\", string.Empty));
try
{
await StartSystemTray().ConfigureAwait(false);
var content = await _trayProcessCommunicationService.ReadTextFileAsync(path).ConfigureAwait(false);
_settingsService.ImportSettings(content);
_notificationService.ShowNotification("Import settings", $"Successfully imported settings from {path}.");
}
catch (Exception e)
{
_notificationService.ShowNotification("Import settings", $"Failed with error: {e.Message}");
commandLineActivated.Operation.ExitCode = 1;
}
if (!_isLaunching && !_alreadyLaunched)
{
Exit();
}
}
}
else if (verb is NewVerb newVerb)
{
var profile = default(ShellProfile);
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
{
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
}
if (profile == null)
{
profile = _settingsService.GetDefaultShellProfile();
}
if (!string.IsNullOrWhiteSpace(newVerb.Directory) && newVerb.Directory != ".")
{
profile.WorkingDirectory = newVerb.Directory;
}
else
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminalAsync(profile, location);
}
else if (verb is RunVerb runVerb)
{
var profile = new ShellProfile
{
Id = Guid.Empty,
Location = null,
Arguments = runVerb.Command,
WorkingDirectory = runVerb.Directory,
UseConPty = _settingsService.GetApplicationSettings().UseConPty
};
if (runVerb.SmartBuffer.HasValue)
{
profile.UseBuffer = runVerb.SmartBuffer.Value;
}
if (!string.IsNullOrWhiteSpace(runVerb.Theme))
{
var theme = _settingsService.GetThemes().FirstOrDefault(x => x.Name.Equals(runVerb.Theme, StringComparison.CurrentCultureIgnoreCase));
if (theme != null)
{
profile.TerminalThemeId = theme.Id;
}
}
if (string.IsNullOrWhiteSpace(profile.WorkingDirectory))
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = runVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: runVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminalAsync(profile, location);
}
}
private Task ShowOrCreateWindowAsync(ActivationViewSwitcher viewSwitcher)
{
var viewModel = _mainViewModels.Find(o => o.ApplicationView.Id == _activeWindowId) ??
_mainViewModels.LastOrDefault();
return viewModel == null
? CreateTerminalAsync(_settingsService.GetDefaultShellProfile(), NewTerminalLocation.Tab, viewSwitcher)
: ShowAsStandaloneAsync(viewModel, viewSwitcher);
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
await OnLaunchOrActivate(args);
}
public async Task OnLaunchOrActivate(IActivatedEventArgs args)
{
if (args is LaunchActivatedEventArgs launchActivated)
{
if (_isLaunching)
{
return;
}
_isLaunching = true;
if (!_alreadyLaunched)
{
await InitializeLoggerAsync();
// ReSharper disable once AssignmentIsFullyDiscarded
_ = JumpListHelper.UpdateAsync(_settingsService);
var viewModel = _container.Resolve<MainViewModel>();
if (launchActivated.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
await viewModel.AddProfileByGuidAsync(Guid.Parse(launchActivated.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
}
else
{
await viewModel.AddDefaultProfileAsync(NewTerminalLocation.Tab);
}
await CreateMainViewAsync(typeof(MainPage), viewModel, true);
Window.Current.Activate();
}
else if (launchActivated.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
var location = _applicationSettings.NewTerminalLocation;
var profile = _settingsService.GetShellProfile(Guid.Parse(launchActivated.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
await CreateTerminalAsync(profile, location, launchActivated.ViewSwitcher);
}
else
{
var viewModel = await CreateNewTerminalWindowAsync();
await viewModel.AddDefaultProfileAsync(NewTerminalLocation.Tab);
await ShowAsStandaloneAsync(viewModel, launchActivated.ViewSwitcher);
}
_isLaunching = false;
}
else if (args is ProtocolActivatedEventArgs protocolActivated)
{
if (protocolActivated.Uri == new Uri("ftcmd://fluent.terminal?focus"))
{
await ShowOrCreateWindow(protocolActivated.ViewSwitcher);
await ShowOrCreateWindowAsync(protocolActivated.ViewSwitcher);
return;
}
@@ -207,7 +394,7 @@ namespace FluentTerminal.App
// in order to be able to communicate with user.
mainViewModel = _container.Resolve<MainViewModel>();
await CreateMainView(typeof(MainPage), mainViewModel, true);
await CreateMainViewAsync(typeof(MainPage), mainViewModel, true);
applicationView = mainViewModel.ApplicationView;
}
@@ -225,7 +412,7 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
@@ -247,19 +434,19 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks && string.IsNullOrEmpty(vm.Username))
{
vm.Username = await _trayProcessCommunicationService.GetUserName();
vm.Username = await _trayProcessCommunicationService.GetUserNameAsync();
}
var error = await vm.AcceptChangesAsync(true);
SshProfile profile = (SshProfile) vm.Model;
var profile = (SshProfile)vm.Model;
if (!string.IsNullOrEmpty(error))
{
@@ -269,16 +456,16 @@ namespace FluentTerminal.App
if (profile == null)
{
// User clicked "Cancel" in the dialog.
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
}
if (mainViewModel == null)
await CreateTerminal(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
await CreateTerminalAsync(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
else
await mainViewModel.AddTerminalAsync(profile);
await mainViewModel.AddTabAsync(profile);
return;
}
@@ -291,7 +478,7 @@ namespace FluentTerminal.App
{
vm = CommandProfileProviderViewModel.ParseUri(protocolActivated.Uri, _settingsService,
applicationView, _trayProcessCommunicationService,
_container.Resolve<ApplicationDataContainers>().HistoryContainer);
_container.Resolve<ICommandHistoryService>());
}
catch (Exception ex)
{
@@ -300,7 +487,7 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
@@ -317,7 +504,7 @@ namespace FluentTerminal.App
if (profile == null)
{
// User clicked "Cancel" in the dialog.
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
@@ -325,11 +512,11 @@ namespace FluentTerminal.App
if (mainViewModel == null)
{
await CreateTerminal(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
await CreateTerminalAsync(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
}
else
{
await mainViewModel.AddTerminalAsync(profile);
await mainViewModel.AddTabAsync(profile);
}
return;
}
@@ -339,12 +526,12 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
// ReSharper disable once AssignmentIsFullyDiscarded
_ = mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
if (args is CommandLineActivatedEventArgs commandLineActivated)
else if (args is CommandLineActivatedEventArgs commandLineActivated)
{
var arguments = commandLineActivated.Operation.Arguments;
if (string.IsNullOrWhiteSpace(arguments))
@@ -352,155 +539,19 @@ namespace FluentTerminal.App
arguments = "new";
}
_commandLineParser.ParseArguments(SplitArguments(arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
{
if (verb is SettingsVerb settingsVerb)
{
if (!settingsVerb.Import && !settingsVerb.Export)
{
await ShowSettings().ConfigureAwait(true);
}
else if (settingsVerb.Export)
{
var exportFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var settings = _settingsService.ExportSettings();
await FileIO.WriteTextAsync(exportFile, settings);
await new MessageDialog($"{I18N.Translate("SettingsExported")} {exportFile.Path}").ShowAsync();
}
else if (settingsVerb.Import)
{
var file = await ApplicationData.Current.LocalFolder.GetFileAsync("config.json");
var content = await FileIO.ReadTextAsync(file);
_settingsService.ImportSettings(content);
await new MessageDialog($"{I18N.Translate("SettingsImported")} {file.Path}").ShowAsync();
}
}
else if (verb is NewVerb newVerb)
{
var profile = default(ShellProfile);
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
{
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
}
if (profile == null)
{
profile = _settingsService.GetDefaultShellProfile();
}
if (!string.IsNullOrWhiteSpace(newVerb.Directory) && newVerb.Directory != ".")
{
profile.WorkingDirectory = newVerb.Directory;
}
else
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
else if (verb is RunVerb runVerb)
{
var profile = new ShellProfile
{
Id = Guid.Empty,
Location = null,
Arguments = runVerb.Command,
WorkingDirectory = runVerb.Directory
};
if (!string.IsNullOrWhiteSpace(runVerb.Theme))
{
var theme = _settingsService.GetThemes().FirstOrDefault(x => x.Name.Equals(runVerb.Theme, StringComparison.CurrentCultureIgnoreCase));
if (theme != null)
{
profile.TerminalThemeId = theme.Id;
}
}
if (string.IsNullOrWhiteSpace(profile.WorkingDirectory))
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = runVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: runVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
});
_commandLineParser
.ParseArguments(SplitArguments(arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb))
.WithParsed(async verb => await ParseCommandLineArgumentsAsync(verb, commandLineActivated));
}
}
private async Task ShowOrCreateWindow(ActivationViewSwitcher viewSwitcher)
{
if (_mainViewModels.Count == 0)
{
await CreateTerminal(_settingsService.GetDefaultShellProfile(), NewTerminalLocation.Tab, viewSwitcher);
}
else
{
var viewModel = _mainViewModels.Find(o => o.ApplicationView.Id == _activeWindowId) ?? _mainViewModels.Last();
await ShowAsStandaloneAsync(viewModel, viewSwitcher);
}
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
if (_isLaunching)
{
return;
}
_isLaunching = true;
if (!_alreadyLaunched)
{
await InitializeLogger();
// TODO: Check the reason for such strange using of tasks!
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
var viewModel = _container.Resolve<MainViewModel>();
if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
await viewModel.AddLocalTabOrWindowAsync(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
}
else
{
await viewModel.AddLocalTabAsync();
}
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
Window.Current.Activate();
}
else if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
var location = _applicationSettings.NewTerminalLocation;
var profile = _settingsService.GetShellProfile(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
await CreateTerminal(profile, location, args.ViewSwitcher).ConfigureAwait(true);
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await viewModel.AddLocalTabAsync();
}
_isLaunching = false;
}
private static async Task InitializeLogger()
private static async Task InitializeLoggerAsync()
{
var logDirectory = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("Logs", CreationCollisionOption.OpenIfExists);
var logFile = Path.Combine(logDirectory.Path, "fluentterminal.app.log");
var configFile = await logDirectory.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var configContent = await FileIO.ReadTextAsync(configFile);
if (string.IsNullOrWhiteSpace(configContent))
{
configContent = JsonConvert.SerializeObject(new Logger.Configuration());
@@ -525,7 +576,7 @@ namespace FluentTerminal.App
_appServiceConnection = new AppServiceConnectionAdapter(details.AppServiceConnection);
_trayReady.SetResult(0);
_trayReady.TrySetResult(0);
}
}
}
@@ -540,11 +591,11 @@ namespace FluentTerminal.App
Application.Current.Exit();
}
private async Task CreateMainView(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
private async Task CreateMainViewAsync(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
{
ApplicationViewSwitcher.DisableSystemViewActivationPolicy();
await StartSystemTray().ConfigureAwait(true);
await StartSystemTray();
if (!(Window.Current.Content is Frame rootFrame))
{
@@ -572,9 +623,9 @@ namespace FluentTerminal.App
Window.Current.Activate();
}
private async Task<MainViewModel> CreateNewTerminalWindow()
private async Task<MainViewModel> CreateNewTerminalWindowAsync()
{
var viewModel = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
var viewModel = await CreateSecondaryViewAsync<MainViewModel>(typeof(MainPage), true);
viewModel.Closed += OnMainViewModelClosed;
viewModel.NewWindowRequested += OnNewWindowRequested;
viewModel.ShowSettingsRequested += OnShowSettingsRequested;
@@ -585,22 +636,24 @@ namespace FluentTerminal.App
return viewModel;
}
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool extendViewIntoTitleBar)
private async Task<TViewModel> CreateSecondaryViewAsync<TViewModel>(Type pageType, bool extendViewIntoTitleBar)
{
int windowId = 0;
var windowId = 0;
TViewModel viewModel = default;
await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
var newView = CoreApplication.CreateNewView();
// ConfigureAwait(true) because ApplicationViewSwitcher.TryShowAsStandaloneAsync requires the same thread as CoreApplication.CreateNewView
await newView.Dispatcher.ExecuteAsync(() =>
{
viewModel = _container.Resolve<TViewModel>();
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
newView.TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
var frame = new Frame();
frame.Navigate(pageType, viewModel);
Window.Current.Content = frame;
Window.Current.Activate();
windowId = ApplicationView.GetForCurrentView().Id;
});
}).ConfigureAwait(true);
if (viewModel is SettingsViewModel settingsViewModel)
{
@@ -663,149 +716,99 @@ namespace FluentTerminal.App
{
Logger.Instance.Debug("App.xaml.cs on tab tear off");
var newViewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await newViewModel.AddTerminalAsync(await model.Serialize(), 0);
var newViewModel = await CreateNewTerminalWindowAsync();
await newViewModel.AddTabAsync(await model.SerializeAsync(), 0);
}
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
ShellProfile profile = null;
switch (e.Action)
{
case NewWindowAction.StartDefaultLocalTerminal:
break;
case NewWindowAction.ShowProfileSelection:
profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshProfileSelection:
profile = await _dialogService.ShowSshProfileSelectionDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowCustomCommandDialog:
profile = await _dialogService.ShowCustomCommandDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshInfoDialog:
profile = await _dialogService.ShowSshConnectionInfoDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.StartLocalTerminal:
profile = _settingsService.GetShellProfile(e.ProfileId);
break;
case NewWindowAction.StartSshTerminal:
profile = _settingsService.GetSshProfile(e.ProfileId);
break;
default:
throw new NotImplementedException("this ProfileSelection not implemented yet");
}
var viewModel = await CreateNewTerminalWindowAsync().ConfigureAwait(false);
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
if (profile == null)
{
await viewModel.AddLocalTabAsync();
}
else
{
await viewModel.AddTerminalAsync(profile);
}
await viewModel.AddTabAsync(e.Profile).ConfigureAwait(false);
}
private void OnSettingsClosed(object sender, EventArgs e)
{
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
// ReSharper disable once AssignmentIsFullyDiscarded
_ = JumpListHelper.UpdateAsync(_settingsService);
_settingsViewModel.Closed -= OnSettingsClosed;
_settingsViewModel = null;
_settingsWindowId = null;
}
private async void OnShowSettingsRequested(object sender, EventArgs e)
private void OnShowSettingsRequested(object sender, EventArgs e)
{
await ShowSettings().ConfigureAwait(true);
// ReSharper disable once AssignmentIsFullyDiscarded
_ = ShowSettingsAsync();
}
private async Task ShowAsStandaloneAsync(MainViewModel viewModel, ActivationViewSwitcher viewSwitcher = null)
private Task ShowAsStandaloneAsync(MainViewModel viewModel, ActivationViewSwitcher viewSwitcher = null)
{
int viewId = viewModel.ApplicationView.Id;
var viewId = viewModel.ApplicationView.Id;
if (viewSwitcher != null)
{
await viewModel.ApplicationView.RunOnDispatcherThread(async () => await viewSwitcher.ShowAsStandaloneAsync(viewId));
}
else
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId);
return viewModel.ApplicationView.ExecuteOnUiThreadAsync(async () => await viewSwitcher.ShowAsStandaloneAsync(viewId));
}
return ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId).AsTask();
}
private async Task CreateTerminal(ShellProfile profile, NewTerminalLocation location, ActivationViewSwitcher viewSwitcher = null)
private async Task CreateTerminalAsync(ShellProfile profile, NewTerminalLocation location, ActivationViewSwitcher viewSwitcher = null)
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminalAsync(profile);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
await viewModel.AddTabAsync(profile);
await CreateMainViewAsync(typeof(MainPage), viewModel, true);
}
else if (location == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
{
var item = _mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId) ??
_mainViewModels.Last();
MainViewModel item = _mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId);
if (item == null)
{
item = _mainViewModels.Last();
}
await item.AddTerminalAsync(profile);
await item.AddTabAsync(profile);
await ShowAsStandaloneAsync(item, viewSwitcher);
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await viewModel.AddTerminalAsync(profile);
var viewModel = await CreateNewTerminalWindowAsync();
await viewModel.AddTabAsync(profile);
await ShowAsStandaloneAsync(viewModel, viewSwitcher);
}
}
private async Task ShowSettings()
private Task ShowSettingsAsync()
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<SettingsViewModel>();
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
return CreateMainViewAsync(typeof(SettingsPage), _container.Resolve<SettingsViewModel>(), true);
}
else if (_settingsViewModel == null)
if (_settingsViewModel == null)
{
await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true).ConfigureAwait(true);
return CreateSecondaryViewAsync<SettingsViewModel>(typeof(SettingsPage), true);
}
else
if (_settingsWindowId.HasValue)
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
return ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value).AsTask();
}
return Task.CompletedTask;
}
private async Task StartSystemTray()
private Task StartSystemTray()
{
if (_trayReady.Task.IsCompleted)
{
return Task.CompletedTask;
}
var launch = FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("AppLaunchedParameterGroup").AsTask();
await Task.WhenAll(launch, _trayReady.Task).ConfigureAwait(true);
_trayProcessCommunicationService.Initialize(_appServiceConnection);
return Task.WhenAll(launch, _trayReady.Task).ContinueWith(
t => _trayProcessCommunicationService.Initialize(_appServiceConnection),
TaskContinuationOptions.OnlyOnRanToCompletion);
}
}
}
@@ -14,6 +14,9 @@ namespace FluentTerminal.App.CommandLineArguments
[Option("theme")]
public string Theme { get; set; }
[Option("smart-buffer")]
public bool? SmartBuffer { get; set; }
[Option("target")]
public Target Target { get; set; }
}
@@ -6,8 +6,8 @@ namespace FluentTerminal.App.CommandLineArguments
public class SettingsVerb
{
[Option('i', "import",
HelpText = "Import settings.")]
public bool Import { get; set; }
HelpText = "Import settings from specified file.")]
public string Import { get; set; }
[Option('e', "export",
HelpText = "Export settings.")]
@@ -0,0 +1,43 @@
using System;
using Windows.UI.Xaml.Data;
// ReSharper disable LocalizableElement
namespace FluentTerminal.App.Converters
{
public class BooleanNegationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (targetType == typeof(bool))
{
if (value is bool isValue)
{
return !isValue;
}
throw new ArgumentException($"{nameof(value)} argument has to be of type bool.", nameof(value));
}
if (targetType == typeof(bool?))
{
if (value == null)
{
return null;
}
if (value is bool isValue)
{
return !isValue;
}
throw new ArgumentException($"{nameof(value)} argument has to be of type bool or Nullable<bool>.", nameof(value));
}
throw new ArgumentException($"{nameof(targetType)} argument has to {typeof(bool)}.",
nameof(targetType));
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
Convert(value, targetType, parameter, language);
}
}
@@ -1,6 +1,5 @@
using Microsoft.Toolkit.Uwp.Helpers;
using System;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;
@@ -20,6 +20,10 @@ namespace FluentTerminal.App.Converters
{
return Translate($"{enumType}.{enumValue}");
}
else if (value is string stringValue)
{
return Translate(stringValue);
}
else if (parameter is string resource)
{
return Translate(resource);
@@ -0,0 +1,33 @@
using FluentTerminal.App.ViewModels.Menu;
using Microsoft.Toolkit.Uwp.Helpers;
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;
// ReSharper disable LocalizableElement
namespace FluentTerminal.App.Converters
{
public class IconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Mdl2Icon mdl2Icon)
{
var fontIcon = new FontIcon { FontFamily = new FontFamily("Segoe MDL2 Assets"), Glyph = mdl2Icon.Glyph };
if(!string.IsNullOrWhiteSpace(mdl2Icon.Color))
{
fontIcon.Foreground = new SolidColorBrush(mdl2Icon.Color.ToColor());
}
return fontIcon;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
}
}
@@ -0,0 +1,254 @@
using System;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using FluentTerminal.App.ViewModels.Menu;
using FluentTerminal.App.Views;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation.Metadata;
// ReSharper disable LocalizableElement
namespace FluentTerminal.App.Converters
{
public class MenuItemViewModelBaseToMenuFlayoutItemBaseConverter : IValueConverter
{
#region Static
private static readonly IconConverter IconConverter = new IconConverter();
private static MenuFlyoutItemBase GetItem(MenuItemViewModelBase viewModel)
{
if (viewModel is ExpandableMenuItemViewModel expandable)
{
return GetExpandableItem(expandable);
}
if (viewModel is SeparatorMenuItemViewModel)
{
return GetSeparatorItem();
}
if (viewModel is MenuItemViewModel regular)
{
return GetRegularItem(regular);
}
if (viewModel is ToggleMenuItemViewModel toggle)
{
return GetToggleItem(toggle);
}
if (viewModel is RadioMenuItemViewModel radio)
{
return GetRadioItem(radio);
}
// Won't happen ever, but still...
throw new NotImplementedException(
$"Unexpected {nameof(MenuItemViewModelBase)} type: {viewModel.GetType()}");
}
private static MenuFlyoutSeparator GetSeparatorItem()
{
return new MenuFlyoutSeparator();
}
private static MenuFlyoutItem GetRegularItem(MenuItemViewModel viewModel)
{
var item = new MenuFlyoutItem();
item.SetBinding(MenuFlyoutItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.CommandProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModel.Command)),
Mode = BindingMode.OneTime
});
if (viewModel.KeyBinding != null)
{
if (viewModel.KeyBinding.IsExtendedVirtualKey)
{
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 6))
{
item.KeyboardAcceleratorTextOverride = viewModel.KeyBinding.GetOverrideText();
}
}
else
{
item.KeyboardAccelerators?.Add(new KeyboardAccelerator
{
Key = (VirtualKey)viewModel.KeyBinding.Key,
Modifiers = (VirtualKeyModifiers)viewModel.KeyBinding.KeyModifiers,
IsEnabled = true
});
}
}
return item;
}
private static MenuFlyoutSubItem GetExpandableItem(ExpandableMenuItemViewModel viewModel)
{
var item = new MenuFlyoutSubItem();
item.SetBinding(MenuFlyoutSubItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutSubItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(MenuExtension.SubItemsProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(ExpandableMenuItemViewModel.SubItems)),
Mode = BindingMode.OneWay
});
return item;
}
private static ToggleMenuFlyoutItem GetToggleItem(ToggleMenuItemViewModel viewModel)
{
var item = new ToggleMenuFlyoutItem();
item.SetBinding(MenuFlyoutItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(ToggleMenuFlyoutItem.IsCheckedProperty, new Binding
{
Source = viewModel.BindingSource,
Path = new PropertyPath(viewModel.BindingPath),
Mode = BindingMode.TwoWay
});
return item;
}
private static RadioMenuFlyoutItem GetRadioItem(RadioMenuItemViewModel viewModel)
{
var item = new RadioMenuFlyoutItem();
item.SetBinding(MenuFlyoutItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(RadioMenuFlyoutItem.GroupNameProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(RadioMenuItemViewModel.GroupName)),
Mode = BindingMode.OneWay
});
item.SetBinding(RadioMenuFlyoutItem.IsCheckedProperty, new Binding
{
Source = viewModel.BindingSource,
Path = new PropertyPath(viewModel.BindingPath),
Mode = BindingMode.TwoWay
});
return item;
}
#endregion Static
#region IValueConverter
// ReSharper disable once AssignNullToNotNullAttribute
public object Convert(object value, Type targetType = null, object parameter = null, string language = null)
{
if (value == null)
{
return null;
}
return value is MenuItemViewModelBase menuItemViewModel
? GetItem(menuItemViewModel)
: throw new ArgumentException(
$"Invalid {nameof(value)} argument type: {value.GetType()}. {typeof(MenuItemViewModelBase)} expected.",
nameof(value));
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
#endregion IValueConverter
}
}
@@ -0,0 +1,43 @@
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using FluentTerminal.App.ViewModels.Menu;
using Windows.UI.Xaml.Controls.Primitives;
namespace FluentTerminal.App.Converters
{
public class MenuViewModelToFlyoutMenuConverter : IValueConverter
{
private static readonly MenuItemViewModelBaseToMenuFlayoutItemBaseConverter ItemConverter =
new MenuItemViewModelBaseToMenuFlayoutItemBaseConverter();
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is null)
{
return null;
}
if (!(value is MenuViewModel menuViewModel))
{
throw new ArgumentException(
$"Invalid {nameof(value)} argument type: {value.GetType()}. {typeof(MenuViewModel)} expected.");
}
var menuFlyout = new MenuFlyout
{
Placement = FlyoutPlacementMode.Bottom
};
foreach (var menuItemViewModelBase in menuViewModel.Items)
{
menuFlyout.Items?.Add((MenuFlyoutItemBase) ItemConverter.Convert(menuItemViewModelBase));
}
return menuFlyout;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
}
}
@@ -0,0 +1,19 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
namespace FluentTerminal.App.Converters
{
public class NullToCollapsedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value == null ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}
@@ -1,4 +1,4 @@
using FluentTerminal.Models;
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models.Enums;
using Microsoft.Toolkit.Uwp.Helpers;
using System;
@@ -12,8 +12,9 @@ namespace FluentTerminal.App.Converters
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is TabTheme theme && Enum.TryParse<TabThemeKey>((string)parameter, true, out TabThemeKey tabThemeKey))
if (value is TabThemeViewModel viewModel && Enum.TryParse<TabThemeKey>((string)parameter, true, out TabThemeKey tabThemeKey))
{
var theme = viewModel.Theme;
switch (tabThemeKey)
{
case TabThemeKey.Background:
@@ -1,4 +1,4 @@
using FluentTerminal.Models;
using FluentTerminal.App.ViewModels;
using System;
using Windows.UI.Xaml.Data;
@@ -8,9 +8,9 @@ namespace FluentTerminal.App.Converters
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is TabTheme theme && parameter is string idString && int.TryParse(idString, out var id))
if (value is TerminalViewModel terminal && parameter is string idString && int.TryParse(idString, out var id))
{
return theme.Id == id;
return terminal.TabTheme.Theme.Id == id;
}
return null;
}
@@ -1,17 +1,19 @@
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using GalaSoft.MvvmLight.Command;
using Microsoft.Toolkit.Mvvm.Input;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Input;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Dialogs
{
// ReSharper disable once RedundantExtendsListEntry
public sealed partial class CreateKeyBindingDialog : ContentDialog, ICreateKeyBindingDialog, INotifyPropertyChanged
{
private bool _ctrl;
@@ -23,31 +25,34 @@ namespace FluentTerminal.App.Dialogs
public bool Ctrl
{
get => _ctrl;
set => Set(ref _ctrl, value);
set => SetProperty(ref _ctrl, value);
}
public bool Shift
{
get => _shift;
set => Set(ref _shift, value);
set => SetProperty(ref _shift, value);
}
public bool Alt
{
get => _alt;
set => Set(ref _alt, value);
set => SetProperty(ref _alt, value);
}
public bool Meta
{
get => _meta;
set => Set(ref _meta, value);
set => SetProperty(ref _meta, value);
}
public int Key
{
get => _key;
set => Set(ref _key, value);
set
{
SetProperty(ref _key, value);
}
}
public CreateKeyBindingDialog()
@@ -61,27 +66,17 @@ namespace FluentTerminal.App.Dialogs
this.SecondaryButtonText = I18N.Translate("Cancel");
}
public RelayCommand ResetCommand { get; }
public ICommand ResetCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
public async Task<KeyBinding> CreateKeyBinding()
public Task<KeyBinding> CreateKeyBinding()
{
var result = await ShowAsync();
if (result == ContentDialogResult.Primary)
{
return new KeyBinding
{
Alt = Alt,
Ctrl = Ctrl,
Key = Key,
Meta = Meta,
Shift = Shift
};
}
return null;
return ShowAsync().AsTask()
.ContinueWith(
t => t.Result == ContentDialogResult.Primary
? new KeyBinding {Alt = Alt, Ctrl = Ctrl, Key = Key, Meta = Meta, Shift = Shift}
: null, TaskContinuationOptions.OnlyOnRanToCompletion);
}
private void OnResetButtonPreviewKeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
@@ -130,8 +125,13 @@ namespace FluentTerminal.App.Dialogs
ResetButton.Visibility = Visibility.Collapsed;
}
private void Set<T>(ref T field, T value, [CallerMemberName]string propertyName = "")
private void SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = "")
{
if (field?.Equals(value) ?? value == null)
{
return;
}
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Loaded 100 of 353 files, more files were not shown because too many files have changed in this diff. Show more