Compare commits

...
Author SHA1 Message Date
Max Risuhin fe60f37213 Fixes performance issues of multiple ConPty sessions with fast output. 2020-04-12 03:14:16 +03:00
Felix Seidl 0dc1a1cd5d don't treat AltGr like Alt+Ctrl
fixes #699
2020-04-10 15:03:19 +02:00
Felix Seidl e1fc980779 fix missing translations for tab color names 2020-04-10 14:00:40 +02:00
Felix Seidl ccd8c1eaa4 improve settings import/export 2020-04-10 13:52:23 +02:00
Restia666Ashdoll ad1a933207 Update README.md (#707) 2020-04-08 08:04:46 +02:00
Felix Seidl 8d7c57d6fe allow more keybindings (again)
fixes #697
2020-04-05 18:33:43 +02:00
Felix Seidl 8d039f59ef prevent conpty from changing the title to its executable location
fixes #687
2020-04-05 17:43:53 +02:00
Felix Seidl cb8d3cabc7 fix missing tab color names 2020-04-05 17:23:47 +02:00
Felix Seidl 579def4f63 Fix tab color for unselected tabs
fixes #688
2020-04-05 17:19:08 +02:00
Max Hora 5d54d448b0 Rollback to use double quotes around identity file for ssh cli cmd. (#698) 2020-04-05 13:28:46 +02:00
Max HoraandFelix Seidl dec492e39a Reconnect failed tab. (#682)
* Reconnect failed tab.

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

* some ui changes + localization

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

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

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

* async/await in FluentTerminal.SystemTray project.

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

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

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

* async/await in FluentTerminal.SystemTray project.

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

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

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

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

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

* Renaming a file.

* Code cosmetics.

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

* Reviewed async pattern throughout FluentTerminal.App project.

* Async patterns in TrayProcessCommunicationService reviewed.

* Minor changes.

* Replaced ContinueWith with async/await in TrayProcessCommunicationService.

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

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

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

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

* After merge fixes, and some minor changes.

* DelayedAction fixed and some UI bug fixed.

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

* After merge fixes, and some minor changes.

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

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

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

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

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

* Clicking menu items works.

* Fixing an issue with resources.

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

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

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

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

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

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

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

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

* Merging the latest changed.

* Fixing some after-merge issues.

* Minor refactoring.

* Minor refactoring.

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

* Default profile name in the menu item now updates.

* Updating resources from POEditor.

* New tab/window cascading menues implemented.

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

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

* Configurable tab/window cascading menu.

* "Application menu type" settings value added.

* Minor spelling fix.

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

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

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

* Clicking menu items works.

* Fixing an issue with resources.

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

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

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

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

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

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

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

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

* Merging the latest changed.

* Fixing some after-merge issues.

* Minor refactoring.

* Minor refactoring.

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

* Default profile name in the menu item now updates.

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

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

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

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

* Clicking menu items works.

* Fixing an issue with resources.

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

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

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

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

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

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

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

* Removing some not needed code.

* Attempt closing ContentDialog programaticaly.

* Fixing a bug caused by new changes.

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

* Removing some not needed code.

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

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

* Trivial change.

* Another trivial change in MOSH_README.

* And another trivial...

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

* New mosh from dialog.

* CommandHistoryService implemented.

* Backward compatibility for command history implemented.

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

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

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

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

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

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

* Need to implement custom event handler for hover

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

* Removed clearSelection from leaveCallback

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

* Add the browse command to theme settings page

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

* Try to show the image that we download

* Change the save image method in file service

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

* Reorganize the UI for image import

* Add background brush converter

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

* Use the File object insted of a image path

* Add background style to content and toolbar

* Create the ImageFile object

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

* Remove commented code

* Remove the base64 string of the image file

* Disable if it's a preinstalled theme

* Add new event when background image changed

* Make the RemoveImportedImage method async

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

* Add the delete background image command

* Remove unused converters that we created

* Save background image in temporary folder

* Remove the temporary images and save the last one

* Implement the cancel and delete background

* Show the background image in terminal

* Split the image file methods in another interface

* Change the name of image saving method

* Prepare the object and methods to export encoded image

* Add import method to deserialize correctly the theme

* Remove the private from the set accessor

* Implememnt the import method

* Implement the conversion base64 image to real image

* Create the background image and import it

* Fix some actions bug

* Get the new image file name after import

* Fix some bugs when the image is deleted

* Move the background image in terminal view

* Translate the background image text

* Add the delete background command

* Fix the background image deletion if we click on cancel

* Add background opacity property to terminal view model

* Need to create a theme changed event

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

* Remove the background from the theme settings page

* Fix error of last edited image

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

* Push tab leak fix.

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

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

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

* ApplicationSettingsChanged event replaced by MVVMLight Messenger implementation.

* CurrentThemeChanged event replaced by MVVMLight Messenger implementation.

* SettingsService.KeyBindingsChanged event replaced with MVVMLight Messenger implementation.

* Further simplifying KeyBindingsChangedMessage propagation.

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

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

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

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

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

Add contributors button (Shields.io button)

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

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

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

French can use the app ;')

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

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

* Delayed command history saving improved.

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

* Opening auto-suggest list on down arrow implemented.

* Minor change.

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

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

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

* Adding "Quick Launch" to menu.

* Auto-complete, initial version.

* Builds, but not finished.

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

* Minor changes.

* Refactroing request/response identifiers.

* Improving command history.

* Minor refactoring.

* Deleting history by using "Del" key.

* Handling mosh special case in quick commands.

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

* New resource strings.

* Fixing quick command links.

* Fixing after-merging bugs.

* Improving filtering.

* Finishing filtering.

* Changing placeholder text.

* Minor refactoring.

* Updating resources.

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

* Turn on AlwaysShowTabs by default.

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

* Notify tray process on settings change.
2019-07-16 22:17:45 +02:00
FelixSe 90c299da83 Increase version 2019-07-14 18:06:18 +02:00
FelixSe 02a695056a Revert "Upgrade xterm.js to v3.15.0-beta70 (#444)"
This reverts commit b87ec9f348.
2019-07-14 11:58:31 +02:00
FelixSe 50a3a7c422 Increase version 2019-07-11 21:33:41 +02:00
FelixSe 3f699f40e3 Update translations 2019-07-11 21:14:36 +02:00
Max Risuhin b87ec9f348 Upgrade xterm.js to v3.15.0-beta70 (#444) 2019-07-11 20:52:33 +02:00
Felix 79dda1d4fd Fix crash on profiles (#440) 2019-07-09 22:00:05 +02:00
Max Risuhin 5467bafaed Settings option to mute conhost. (#437) 2019-07-09 21:49:07 +02:00
Aleksandar Pesic ac3190111d Fixing a bug with creating new profiles. (#436) 2019-07-09 21:20:42 +02:00
Aleksandar Pesic 298b402d5d Implementing more descriptive error messages on invalid quick-command input. (#434) 2019-07-07 21:34:33 +02:00
Aleksandar Pesic a7b734b3bf Adding some translations, fixing remote profiles layout, and fixing a minor bug with quick commands. (#433) 2019-07-07 15:52:30 +02:00
Aleksandar Pesic 3dd95e994f Fixing a bug with opening shortcuts. (#432) 2019-07-06 15:40:44 +02:00
Aleksandar Pesic 96e32fcc96 Implemented "Quick SSH" dialog (#423)
* Simplifying view models, step 1: Extracting common terminal configuration into TerminalInfoViewModel.cs.

* Simplifying a bit.

* SSH view models simplfication.

* Refactoring duplicated code.

* Refactoring duplicated code.

* Renaming a resource file.

* Cleanup.

* Fixing an issue with TemplateSelectors.

* QuickSshDialog created.

* Minor fix.

* Wiring up QuickSshDialog.

* Minor fix.

* Fixed an issue with shared resources.

* Single-text-box dialog works.

* Improving "Quick SSH" look.

* Improving data validation workflow to avoid double validations.

* Refactoring view models and templates to avoid duplicated code.

* Minor fix.

* Fixing a bug with mosh.

* Cleanup.

* Cleanup.

* Moving profile view models in a separate folder (namespace).

* Few comment lines (short description) added to profile view models.

* Removing IDisposable from view-models because event-caused memory leaks will be solved differently.

* Implemented quick-ssh shortcuts.

* Fixing quick SSH command validation.

* Minor change.

* Minor change.

* Combining "SSH Info" and "Quick SSH" into a single dialog.

* Quick SSH profiles.

* Fixing a minor bug with custom links.

* Fixing remote profiles look and feel.

* Minor improvements.

* Fixing a minor bug.

* Minor changes based on Felix' comments.

* Saving executed commands history implemented.
2019-07-06 13:53:00 +02:00
516 changed files with 35173 additions and 11184 deletions

No files matched your search

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