Compare commits

...
141 Commits
Author SHA1 Message Date
Max Risuhin e17cf894b8 Rollback to use double quotes around identity file for ssh cli cmd. 2020-04-05 14:12:41 +03: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
317 changed files with 25984 additions and 8872 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<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowCreateKeyBindingDialog();
@@ -36,9 +36,9 @@ namespace FluentTerminal.App.Services.Test
var title = string.Empty;
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("title");
}
@@ -49,9 +49,9 @@ namespace FluentTerminal.App.Services.Test
var title = _fixture.Create<string>();
var content = string.Empty;
var buttons = _fixture.CreateMany<DialogButton>(2);
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("content");
}
@@ -61,9 +61,9 @@ namespace FluentTerminal.App.Services.Test
{
var title = _fixture.Create<string>();
var content = _fixture.Create<string>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsnyc(title, content);
Func<Task<DialogButton>> showMessageDialogAsnyc = () => dialogService.ShowMessageDialogAsync(title, content);
showMessageDialogAsnyc.Should().Throw<ArgumentException>().And.ParamName.Should().Be("buttons");
}
@@ -75,30 +75,19 @@ namespace FluentTerminal.App.Services.Test
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var messageDialog = new Mock<IMessageDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, () => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(() => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowMessageDialogAsnyc(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
messageDialog.Verify(x => x.ShowAsync(), Times.Once);
}
[Fact]
public void ShowProfileSelectionDialogAsync_Default_UsesShellProfileSelectionDialog()
{
var shellProfileSelectionDialog = new Mock<IShellProfileSelectionDialog>();
var dialogService = new DialogService(() => shellProfileSelectionDialog.Object, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowProfileSelectionDialogAsync();
shellProfileSelectionDialog.Verify(x => x.SelectProfile(), Times.Once);
}
[Fact]
public void ShowInputDialogAsync_UsesIInputDialogSetTitle()
{
var title = "title";
var inputDialog = new Mock<IInputDialog>();
var dialogService = new DialogService(Mock.Of<IShellProfileSelectionDialog>, Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<ISshProfileSelectionDialog>, Mock.Of<IAboutDialog>);
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowInputDialogAsync(title);
@@ -12,10 +12,10 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="FluentAssertions" Version="5.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Moq" Version="4.13.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<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>
@@ -38,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);
@@ -60,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>(),
@@ -88,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);
@@ -113,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);
@@ -136,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);
@@ -157,6 +162,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 currentThemeChangedEventInvoked = false;
@@ -181,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);
@@ -206,7 +213,8 @@ namespace FluentTerminal.App.Services.Test
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = roamingSettings.Object,
ShellProfiles = Mock.Of<IApplicationDataContainer>()
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
@@ -229,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);
@@ -251,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);
@@ -274,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);
@@ -297,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);
@@ -320,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);
@@ -342,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);
@@ -364,7 +378,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);
Messenger.Default.Register<TerminalOptionsChangedMessage>(this,
@@ -388,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);
@@ -410,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);
@@ -432,7 +449,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);
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this,
@@ -459,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);
@@ -483,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);
@@ -508,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);
@@ -531,7 +552,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);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
@@ -553,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);
@@ -578,7 +601,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);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
@@ -602,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);
@@ -627,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);
@@ -648,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>(),
@@ -670,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>(),
@@ -691,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>(),
@@ -715,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,
@@ -743,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,7 +68,7 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.ResizeTerminal(terminalId, terminalSize);
await trayProcessCommunicationService.ResizeTerminalAsync(terminalId, terminalSize);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
}
@@ -88,7 +88,7 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.Write(terminalId, data);
await trayProcessCommunicationService.WriteAsync(terminalId, data);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
}
@@ -107,7 +107,7 @@ namespace FluentTerminal.App.Services.Test
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.CloseTerminal(terminalId);
await trayProcessCommunicationService.CloseTerminalAsync(terminalId);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
}
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Storage;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App.Services.Adapters
{
@@ -56,7 +57,8 @@ namespace FluentTerminal.App.Services.Adapters
public void WriteValueAsJson<T>(string name, T value)
{
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value);
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value,
PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
}
}
}
@@ -1,12 +0,0 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IShellProfileSelectionDialog
{
ShellProfile SelectedProfile { get; }
Task<ShellProfile> SelectProfile();
}
}
@@ -1,12 +0,0 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshProfileSelectionDialog
{
SshProfile SelectedProfile { get; }
Task<SshProfile> SelectProfile();
}
}
@@ -1,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));
}
}
@@ -6,13 +6,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="PListNet" Version="3.0.0" />
<PackageReference Include="RestSharp" Version="106.6.10" />
<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>
@@ -0,0 +1,12 @@
using FluentTerminal.Models.Enums;
namespace FluentTerminal.App.Services
{
// This validator is needed because accelerator key has to be member of VirtualKey enum.
public interface IAcceleratorKeyValidator
{
bool Valid(int key);
bool Valid(ExtendedVirtualKey key);
}
}
@@ -1,6 +1,7 @@
using FluentTerminal.App.Services.EventArgs;
using System;
using System.Threading.Tasks;
using Windows.UI.Core;
namespace FluentTerminal.App.Services
{
@@ -14,9 +15,13 @@ namespace FluentTerminal.App.Services
int Id { get; }
string Title { get; set; }
Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true);
Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false);
Task<bool> TryClose();
Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false);
Task<bool> TryCloseAsync();
bool ToggleFullScreen();
bool IsApiContractPresent(string api, ushort version);
@@ -4,7 +4,7 @@ namespace FluentTerminal.App.Services
{
public interface IClipboardService
{
Task<string> GetText();
Task<string> GetTextAsync();
void SetText(string text);
}
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface ICommandHistoryService
{
/// <summary>
/// Returns the most recent <paramref name="top"/> commands from history.
/// </summary>
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
/// <param name="top">Maximal number of items to return.</param>
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
/// already have them loaded.</param>
List<ExecutedCommand> GetHistoryRecentFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profilesProvider = null);
/// <summary>
/// Returns the most used <paramref name="top"/> commands from history.
/// </summary>
/// <param name="includeProfiles">Indicates if never used saved profiles should also be returned.</param>
/// <param name="top">Maximal number of items to return.</param>
/// <param name="profilesProvider">List of all saved profiles. If this argument isn't specified, the method will
/// get the list from <see cref="ISettingsService"/>. The purpose of this argument is to avoid loading the
/// profiles from settings again (since it includes file system access and JSON deserialization), if we
/// already have them loaded.</param>
List<ExecutedCommand> GetHistoryMostUsedFirst(bool includeProfiles = false, int top = int.MaxValue,
Func<List<ShellProfile>> profilesProvider = null);
void MarkUsed(ShellProfile profile);
void Delete(ExecutedCommand executedCommand);
}
}
@@ -5,25 +5,23 @@ namespace FluentTerminal.App.Services
{
public enum DialogButton
{
// ReSharper disable once InconsistentNaming
OK,
Cancel
}
public interface IDialogService
{
Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons);
Task<ShellProfile> ShowProfileSelectionDialogAsync();
Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons);
Task<KeyBinding> ShowCreateKeyBindingDialog();
Task<string> ShowInputDialogAsync(string title);
Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null);
Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null);
Task<SshProfile> ShowSshProfileSelectionDialogAsync();
Task ShowAboutDialogAsync();
}
}
@@ -1,16 +0,0 @@
using System;
namespace FluentTerminal.App.Services
{
public interface IDispatcherTimer
{
void Start();
void Stop();
TimeSpan Interval { get; set; }
bool IsEnabled { get; }
event EventHandler<object> Tick;
}
}
@@ -6,10 +6,12 @@ namespace FluentTerminal.App.Services
{
public interface IFileSystemService
{
Task<File> OpenFile(IEnumerable<string> fileTypes);
Task<File> OpenFileAsync(IEnumerable<string> fileTypes);
Task<string> BrowseForDirectory();
Task<string> BrowseForDirectoryAsync();
Task SaveTextFile(string name, string fileTypeDescription, string fileType, string content);
Task SaveTextFileAsync(string name, string fileTypeDescription, string fileType, string content);
Task<ImageFile> SaveImageInRoamingAsync(ImageFile imageFile);
}
}
@@ -0,0 +1,19 @@
using FluentTerminal.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IImageFileSystemService
{
Task<ImageFile> ImportTemporaryImageFileAsync(IEnumerable<string> fileTypes);
Task RemoveTemporaryBackgroundThemeImageAsync();
Task RemoveImportedImageAsync(string fileName);
string EncodeImage(ImageFile imageFile);
Task<ImageFile> ImportThemeImageAsync(ImageFile backgroundImage, string encodedImage);
}
}
@@ -13,12 +13,14 @@ namespace FluentTerminal.App.Services
TerminalTheme GetCurrentTheme();
Guid GetCurrentThemeId();
ShellProfile GetDefaultShellProfile();
SshProfile GetDefaultSshProfile();
Guid GetDefaultShellProfileId();
Guid GetDefaultSshProfileId();
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
IEnumerable<ShellProfile> GetShellProfiles();
IEnumerable<SshProfile> GetSshProfiles();
/// <summary>
/// Returns union of <see cref="GetShellProfiles"/> and <see cref="GetSshProfiles"/>.
/// </summary>
IEnumerable<ShellProfile> GetAllProfiles();
IEnumerable<TabTheme> GetTabThemes();
TerminalOptions GetTerminalOptions();
TerminalTheme GetTheme(Guid id);
@@ -28,7 +30,6 @@ namespace FluentTerminal.App.Services
void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id);
void SaveDefaultSshProfileId(Guid id);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
@@ -5,8 +5,8 @@ namespace FluentTerminal.App.Services
{
public interface IStartupTaskService
{
Task DisableStartupTask();
Task<StartupTaskStatus> EnableStartupTask();
Task<StartupTaskStatus> GetStatus();
Task DisableStartupTaskAsync();
Task<StartupTaskStatus> EnableStartupTaskAsync();
Task<StartupTaskStatus> GetStatusAsync();
}
}
@@ -10,5 +10,7 @@ namespace FluentTerminal.App.Services
IEnumerable<string> SupportedFileTypes { get; }
Task<TerminalTheme> Parse(string fileName, Stream fileContent);
Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent);
}
}
@@ -12,24 +12,26 @@ namespace FluentTerminal.App.Services
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType);
Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType);
Task<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause);
Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause);
Task ResizeTerminal(byte id, TerminalSize size);
Task ResizeTerminalAsync(byte id, TerminalSize size);
Task UpdateToggleWindowKeyBindings();
Task UpdateToggleWindowKeyBindingsAsync();
Task Write(byte terminalId, byte[] data);
Task WriteAsync(byte terminalId, byte[] data);
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
void UnsubscribeFromTerminalOutput(byte terminalId);
Task CloseTerminal(byte terminalId);
Task CloseTerminalAsync(byte terminalId);
byte GetNextTerminalId();
Task<string> GetUserName();
Task<string> GetUserNameAsync();
Task SaveTextFileAsync(string path, string content);
@@ -39,10 +41,12 @@ namespace FluentTerminal.App.Services
Task<bool> CheckFileExistsAsync(string path);
void MuteTerminal(bool mute);
Task MuteTerminalAsync(bool mute);
void UpdateSettings(ApplicationSettings settings);
Task<string> GetCommandPathAsync(string command);
Task QuitApplicationAsync();
}
}
@@ -5,7 +5,7 @@ namespace FluentTerminal.App.Services
{
public interface IUpdateService
{
Task CheckForUpdate(bool notifyNoUpdate = false);
Task CheckForUpdateAsync(bool notifyNoUpdate = false);
Version GetCurrentVersion();
Task<Version> GetLatestVersionAsync();
}
@@ -27,6 +27,7 @@ namespace FluentTerminal.App.Services.Implementation
UnderlineSelectedTab = false,
InactiveTabColorMode = InactiveTabColorMode.Background,
NewTerminalLocation = NewTerminalLocation.Tab,
TabWindowCascadingAppMenu = true,
TabsPosition = TabsPosition.Top,
CopyOnSelect = false,
MouseMiddleClickAction = MouseAction.None,
@@ -40,7 +41,9 @@ namespace FluentTerminal.App.Services.Implementation
MuteTerminalBeeps = true,
EnableLogging = false,
PrintableOutputOnly = true,
LogDirectoryPath = logDirectoryPath
LogDirectoryPath = logDirectoryPath,
UseConPty = true,
ShowTextCopied = false
};
}
@@ -112,6 +115,18 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ReconnectTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ReconnectTab),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.R
}
};
case Command.NewTab:
return new List<KeyBinding>
{
@@ -124,18 +139,6 @@ namespace FluentTerminal.App.Services.Implementation
}
};
case Command.ConfigurableNewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ConfigurableNewTab),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewSshTab:
return new List<KeyBinding>
{
@@ -167,8 +170,8 @@ namespace FluentTerminal.App.Services.Implementation
{
Command = nameof(Command.NewCustomCommandTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Q
Alt = true,
Key = (int)ExtendedVirtualKey.T
}
};
@@ -180,35 +183,10 @@ namespace FluentTerminal.App.Services.Implementation
Command = nameof(Command.NewCustomCommandWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.Q
Key = (int)ExtendedVirtualKey.N
}
};
case Command.SavedSshNewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.SavedSshNewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SavedSshNewWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.U
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
@@ -245,18 +223,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>
{
@@ -265,7 +231,8 @@ namespace FluentTerminal.App.Services.Implementation
Command = nameof(Command.ShowSettings),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Comma
// I've had to replace ExtendedVirtualKey.Comma because it isn't a member of VirtualKey enum, thus cannot be shown in menu.
Key = (int)ExtendedVirtualKey.S
}
};
@@ -432,11 +399,12 @@ namespace FluentTerminal.App.Services.Implementation
ScrollBarStyle = ScrollBarStyle.Hidden,
FontFamily = "Consolas",
FontSize = 13,
BoldText = false,
FontWeight = 400,
BackgroundOpacity = 0.8,
Padding = 12,
ScrollBackLimit = 1000,
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;"
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;",
UseAcrylicBackground = true
};
}
@@ -458,8 +426,8 @@ namespace FluentTerminal.App.Services.Implementation
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+
UseBuffer = false,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
@@ -486,8 +454,8 @@ namespace FluentTerminal.App.Services.Implementation
Location = @"C:\Windows\System32\cmd.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.DoNotModify,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
UseBuffer = true,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
@@ -514,8 +482,8 @@ namespace FluentTerminal.App.Services.Implementation
Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
LineEndingTranslation = LineEndingStyle.ToLF,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
UseBuffer = true, //TODO: Set to false if the buffer causes issues with WSL.
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
@@ -7,29 +7,23 @@ namespace FluentTerminal.App.Services.Implementation
{
public class DialogService : IDialogService
{
private readonly Func<IShellProfileSelectionDialog> _shellProfileSelectionDialogFactory;
private readonly Func<IMessageDialog> _messageDialogFactory;
private readonly Func<ICreateKeyBindingDialog> _createKeyBindingDialogFactory;
private readonly Func<IInputDialog> _inputDialogFactory;
private readonly Func<ISshConnectionInfoDialog> _sshConnectionInfoDialogFactory;
private readonly Func<ICustomCommandDialog> _customCommandDialogFactory;
private readonly Func<ISshProfileSelectionDialog> _sshProfileSelectionDialogFactory;
private readonly Func<IAboutDialog> _aboutDialogFactory;
public DialogService(Func<IShellProfileSelectionDialog> shellProfileSelectionDialogFactory,
Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
public DialogService(Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
Func<ICustomCommandDialog> customCommandDialogFactory,
Func<ISshProfileSelectionDialog> sshProfileSelectionDialogFactory,
Func<IAboutDialog> aboutDialogFactory)
{
_shellProfileSelectionDialogFactory = shellProfileSelectionDialogFactory;
_messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory;
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
_customCommandDialogFactory = customCommandDialogFactory;
_sshProfileSelectionDialogFactory = sshProfileSelectionDialogFactory;
_aboutDialogFactory = aboutDialogFactory;
}
@@ -40,7 +34,7 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.CreateKeyBinding();
}
public Task<DialogButton> ShowMessageDialogAsnyc(string title, string content, params DialogButton[] buttons)
public Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons)
{
if (string.IsNullOrWhiteSpace(title))
{
@@ -77,26 +71,12 @@ namespace FluentTerminal.App.Services.Implementation
return dialog.GetInput();
}
public Task<ShellProfile> ShowProfileSelectionDialogAsync()
{
var dialog = _shellProfileSelectionDialogFactory();
return dialog.SelectProfile();
}
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) =>
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
_customCommandDialogFactory().GetCustomCommandAsync(input);
public Task<SshProfile> ShowSshProfileSelectionDialogAsync()
{
var dialog = _sshProfileSelectionDialogFactory();
return dialog.SelectProfile();
}
public Task ShowAboutDialogAsync()
{
var dialog = _aboutDialogFactory();
@@ -9,9 +9,20 @@ namespace FluentTerminal.App.Services.Implementation
{
public class FluentTerminalThemeParser : IThemeParser
{
public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".flutecolors" };
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".flutecolors" };
public async Task<TerminalTheme> Parse(string fileName, Stream fileContent)
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent)
{
return DeserializeThemeAsync<ExportedTerminalTheme>(fileName, fileContent);
}
public Task<TerminalTheme> Parse(string fileName, Stream fileContent)
{
return DeserializeThemeAsync<TerminalTheme>(fileName, fileContent);
}
private async Task<T> DeserializeThemeAsync<T>(string fileName, Stream fileContent)
where T : TerminalTheme
{
if (string.IsNullOrWhiteSpace(fileName))
{
@@ -25,8 +36,8 @@ namespace FluentTerminal.App.Services.Implementation
using (var streamReader = new StreamReader(fileContent))
{
var content = await streamReader.ReadToEndAsync();
var theme = JsonConvert.DeserializeObject<TerminalTheme>(content);
var content = await streamReader.ReadToEndAsync().ConfigureAwait(false);
var theme = JsonConvert.DeserializeObject<T>(content);
theme.PreInstalled = false;
theme.Id = Guid.NewGuid();
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
// ReSharper disable InconsistentNaming
namespace FluentTerminal.App.Services.Implementation
{
@@ -14,7 +15,7 @@ namespace FluentTerminal.App.Services.Implementation
{
private const byte Opacity30Percent = 77;
public IEnumerable<string> SupportedFileTypes { get; } = new string[] { ".itermcolors" };
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".itermcolors" };
private static class ITermThemeKeys
{
@@ -36,7 +37,7 @@ namespace FluentTerminal.App.Services.Implementation
public const string Ansi15Color = "Ansi 15 Color";
public const string BackgroundColor = "Background Color";
public const string BoldColor = "Bold Color";
//public const string BoldColor = "Bold Color";
public const string CursorColor = "Cursor Color";
public const string CursorTextColor = "Cursor Text Color";
public const string ForegroundColor = "Foreground Color";
@@ -133,5 +134,30 @@ namespace FluentTerminal.App.Services.Implementation
{
return (alpha / 256.0).ToString(CultureInfo.InvariantCulture);
}
public Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent)
{
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentNullException(nameof(fileName));
}
if (fileContent == null)
{
throw new ArgumentNullException(nameof(fileContent));
}
var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException("Root node was not a dictionary.");
var terminalTheme = new TerminalTheme
{
Name = Path.GetFileNameWithoutExtension(fileName),
Colors = GetColors(node),
Id = Guid.NewGuid(),
PreInstalled = false
};
return Task.FromResult(new ExportedTerminalTheme(terminalTheme, string.Empty));
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.Text.RegularExpressions;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Implementation
{
// The purpose of this class is to ensure backward compatibility of mosh profiles and command history
// TODO: This class should be probably deleted in few months or so
public static class MoshBackwardCompatibility
{
private static readonly string MoshCommandExe = $"{Constants.MoshCommandName}.exe";
private static readonly Regex OldFormatRx = new Regex(@".+\s(?<ports>\d{1,5}:\d{1,5})$", RegexOptions.Compiled);
private static readonly Regex TargetRx =
new Regex(@"(^|\s+)(?<user>[^\s@;]+)@(?<host>[^\s@:]+)(:(?<port>\d{1,5}))?\s+", RegexOptions.Compiled);
private static bool IsMoshProfile(ShellProfile profile) =>
Constants.MoshCommandName.Equals(profile.Location, StringComparison.OrdinalIgnoreCase) ||
MoshCommandExe.Equals(profile.Location, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Checks if the profile contains obsolete mosh syntax, and changes it if it does.
/// </summary>
/// <param name="profile">The profile. Note that the method doesn't change the input profile at all, but creates and
/// returns its clone if there's something to change.</param>
/// <remarks>
/// The method actually checks if <see cref="ShellProfile.Arguments"/> of the input <paramref name="profile"/> is in
/// obsolete <c>mosh</c> format <c>[ssh_arguments] [target] [mosh_port_range]</c>, and if it is changes it to the new
/// format <c>--ssh="[ssh_arguments]" -p [mosh_ports] [target]</c>.
/// </remarks>
/// <returns>
/// If the method detected that the input profile is obsolete mosh profile, it will create its clone with the updated
/// command line arguments, and return it. Otherwise it will simply return unchanged input profile (the same reference
/// as the input profile). It allows us to simply check if anything is changed by comparing the input and output
/// profiles for reference equality.
/// </returns>
public static ShellProfile FixProfile(ShellProfile profile)
{
// If there aren't any arguments, or if the profile isn't a mosh profile, there's nothing to do. Just return.
if (string.IsNullOrWhiteSpace(profile.Arguments) || !IsMoshProfile(profile)) return profile;
var arguments = profile.Arguments.Trim();
var moshPortsMatch = OldFormatRx.Match(arguments);
if (!moshPortsMatch.Success) return profile;// Arguments don't end with mosh ports (i.e. 60000:61000), so it isn't an obsolete format.
var targetMatches = TargetRx.Matches(arguments);
if (targetMatches.Count != 1) return profile;// Target part (user@host:port) not found or found multiple times. Cannot reliably determine the target, so don't change.
var targetMatch = targetMatches[0];
// Removing [mosh_ports] from the original arguments
arguments = arguments.Substring(0, arguments.Length - moshPortsMatch.Groups["ports"].Length).Trim();
var newProfile = profile.Clone();
// Removing [target] from the original arguments, thus getting "pure" ssh arguments
var sshArguments = targetMatch.Index > 0 ? arguments.Substring(0, targetMatch.Index).Trim() : string.Empty;
if (targetMatch.Index + targetMatch.Length < arguments.Length)
{
sshArguments += $" {arguments.Substring(targetMatch.Index + targetMatch.Length).Trim()}";
}
// If the ssh port is defined in the original target (i.e. user@host:port), which was allowed in the obsolete mosh, extracting the port part into a separate ssh argument
if (targetMatch.Groups["port"].Success && uint.TryParse(targetMatch.Groups["port"].Value, out var port) &&
port != SshProfile.DefaultSshPort)
{
sshArguments += $" -p {targetMatch.Groups["port"].Value}";
}
// Replacing any double quotes in ssh arguments with single quotes, since ssh arguments will be double-quoted as a whole
sshArguments = sshArguments.Replace("\"", "'").Trim();
arguments = $"-p {moshPortsMatch.Groups["ports"].Value}";
if (!string.IsNullOrEmpty(sshArguments))
{
// If there were some ssh arguments, add them as defined in the new format
arguments += $" --ssh=\"ssh {sshArguments}\"";
}
// Finally append the target to the arguments
arguments += $" {targetMatch.Groups["user"]}@{targetMatch.Groups["host"]}";
newProfile.Arguments = arguments;
return newProfile;
}
}
}
@@ -5,6 +5,7 @@ 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;
@@ -80,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)
@@ -129,7 +130,6 @@ namespace FluentTerminal.App.Services.Implementation
existingProfile.Arguments = profile.Arguments;
existingProfile.TabThemeId = profile.TabThemeId;
existingProfile.TerminalThemeId = profile.TerminalThemeId;
existingProfile.LineEndingTranslation = profile.LineEndingTranslation;
existingProfile.KeyBindings = profile.KeyBindings;
SaveShellProfile(profile, isNew);
continue;
@@ -157,7 +157,7 @@ namespace FluentTerminal.App.Services.Implementation
public void DeleteSshProfile(Guid id)
{
_sshProfiles.Delete(id.ToString());
Messenger.Default.Send(new SshProfileDeletedMessage(id));
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
@@ -217,13 +217,6 @@ namespace FluentTerminal.App.Services.Implementation
}
return profile;
}
public SshProfile GetDefaultSshProfile()
{
var id = GetDefaultSshProfileId();
var profile = _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
return profile;
}
public ShellProfile GetShellProfile(Guid id)
{
@@ -242,14 +235,6 @@ namespace FluentTerminal.App.Services.Implementation
}
return _defaultValueProvider.GetDefaultShellProfileId();
}
public Guid GetDefaultSshProfileId()
{
if (_localSettings.TryGetValue(DefaultSshProfileKey, out object value))
{
return (Guid)value;
}
return System.Guid.Empty;
}
public IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings()
{
@@ -264,20 +249,43 @@ namespace FluentTerminal.App.Services.Implementation
public IEnumerable<ShellProfile> GetShellProfiles()
{
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string)x)).ToList();
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string) x))
.Select(MoshBackwardCompatibilityFixProfile);
}
public IEnumerable<SshProfile> GetSshProfiles()
{
if (_sshProfiles == null)
return _sshProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x))
.Select(MoshBackwardCompatibilityFixProfile).Cast<SshProfile>();
}
public IEnumerable<ShellProfile> GetAllProfiles()
{
return GetShellProfiles().Union(GetSshProfiles());
}
private ShellProfile MoshBackwardCompatibilityFixProfile(ShellProfile profile)
{
var fixedProfile = MoshBackwardCompatibility.FixProfile(profile);
if (ReferenceEquals(fixedProfile, profile))
{
return new List<SshProfile>();
// Nothing changed
return fixedProfile;
}
if (fixedProfile is SshProfile sshProfile)
{
DeleteSshProfile(profile.Id);
SaveSshProfile(sshProfile);
}
else
{
return _sshProfiles.GetAll()
.Select(x => JsonConvert.DeserializeObject<SshProfile>((string)x)).ToList();
DeleteShellProfile(profile.Id);
SaveShellProfile(fixedProfile);
}
return fixedProfile;
}
public IEnumerable<TabTheme> GetTabThemes()
@@ -313,12 +321,12 @@ namespace FluentTerminal.App.Services.Implementation
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
{
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
{
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings));
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void SaveCurrentThemeId(Guid id)
@@ -331,10 +339,8 @@ namespace FluentTerminal.App.Services.Implementation
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)
@@ -359,7 +365,12 @@ namespace FluentTerminal.App.Services.Implementation
{
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);
@@ -369,7 +380,11 @@ namespace FluentTerminal.App.Services.Implementation
if (newShell)
{
Messenger.Default.Send(new SshProfileAddedMessage(sshProfile));
Messenger.Default.Send(new ShellProfileAddedMessage(sshProfile));
}
else
{
Messenger.Default.Send(new ShellProfileChangedMessage(sshProfile));
}
}
@@ -8,14 +8,35 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App.Services.Implementation
{
public class TrayProcessCommunicationService : ITrayProcessCommunicationService
{
#region Static
private static string _userName;
private static string _sshConfigDir;
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
private static ValueSet CreateMessage(IMessage content)
{
return new ValueSet
{
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(content,
PreserveDictionaryKeyCaseContractResolver.SerializerSettings)
};
}
#endregion Static
private readonly ISettingsService _settingsService;
private IAppServiceConnection _appServiceConnection;
private readonly Dictionary<byte, Action<byte[]>> _terminalOutputHandlers;
// ReSharper disable once RedundantDefaultMemberInitializer
private byte _nextTerminalId = 0;
public event EventHandler<TerminalExitStatus> TerminalExited;
@@ -31,9 +52,7 @@ namespace FluentTerminal.App.Services.Implementation
return _nextTerminalId++;
}
private string _userName;
public async Task<string> GetUserName()
public async Task<string> GetUserNameAsync()
{
if (!string.IsNullOrEmpty(_userName))
{
@@ -41,52 +60,44 @@ namespace FluentTerminal.App.Services.Implementation
return _userName;
}
StringValueResponse response;
var response = await GetResponseAsync<StringValueResponse>(new GetUserNameRequest()).ConfigureAwait(false);
// No need to crash for username, so try/catch
try
if (response.Success)
{
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new GetUserNameRequest()));
response = JsonConvert.DeserializeObject<StringValueResponse>((string)responseMessage[MessageKeys.Content]);
_userName = response.Value;
}
catch (Exception e)
{
Logger.Instance.Error(e, "Error while trying to get username.");
return null;
}
Logger.Instance.Debug("Received GetUserNameResponse: {@response}", response);
_userName = response.Value;
return _userName;
}
public async Task SaveTextFileAsync(string path, string content)
{
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(new SaveTextFileRequest {Path = path, Content = content}));
var response = JsonConvert.DeserializeObject<CommonResponse>((string)responseMessage[MessageKeys.Content]);
var response =
await GetResponseAsync<CommonResponse>(new SaveTextFileRequest {Path = path, Content = content})
.ConfigureAwait(false);
if (!response.Success)
{
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error) ? "Failed to save the file." : response.Error);
throw new SaveTextFileException(string.IsNullOrEmpty(response.Error)
? "Failed to save the file."
: response.Error);
}
}
private static string _sshConfigDir;
public async Task<string> GetSshConfigDirAsync()
{
if (string.IsNullOrEmpty(_sshConfigDir))
if (!string.IsNullOrEmpty(_sshConfigDir))
{
var response = await GetSshConfigFolderAsync(false);
return _sshConfigDir;
}
if (response?.Success ?? false)
{
_sshConfigDir = response.Path;
}
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{ IncludeContent = false }).ConfigureAwait(false);
if (response.Success)
{
_sshConfigDir = response.Path;
}
return _sshConfigDir;
@@ -94,42 +105,31 @@ namespace FluentTerminal.App.Services.Implementation
public async Task<string[]> GetFilesFromSshConfigDirAsync()
{
var response = await GetSshConfigFolderAsync(false);
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{IncludeContent = true}).ConfigureAwait(false);
if (response == null || !response.Success)
{
return null;
}
if (string.IsNullOrEmpty(_sshConfigDir))
if (response.Success)
{
_sshConfigDir = response.Path;
return response.Files;
}
return response.Files;
}
private async Task<GetSshConfigFolderResponse> GetSshConfigFolderAsync(bool includeContent)
{
var responseMessage =
await _appServiceConnection.SendMessageAsync(CreateMessage(new GetSshConfigFolderRequest
{IncludeContent = includeContent}));
return JsonConvert.DeserializeObject<GetSshConfigFolderResponse>(
(string) responseMessage[MessageKeys.Content]);
return null;
}
public async Task<bool> CheckFileExistsAsync(string path)
{
var responseMessage =
await _appServiceConnection.SendMessageAsync(CreateMessage(new CheckFileExistsRequest {Path = path}));
var response = await GetResponseAsync<CommonResponse>(new CheckFileExistsRequest {Path = path})
.ConfigureAwait(false);
return JsonConvert.DeserializeObject<CommonResponse>((string) responseMessage[MessageKeys.Content]).Success;
return response.Success;
}
public void MuteTerminal(bool mute)
public Task MuteTerminalAsync(bool mute)
{
_appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest { Mute = mute }));
return _appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest {Mute = mute}));
}
public void UpdateSettings(ApplicationSettings settings)
@@ -137,7 +137,8 @@ namespace FluentTerminal.App.Services.Implementation
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
}
public async Task<CreateTerminalResponse> CreateTerminal(byte id, TerminalSize size, ShellProfile shellProfile, SessionType sessionType)
public async Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType)
{
var request = new CreateTerminalRequest
{
@@ -149,24 +150,17 @@ namespace FluentTerminal.App.Services.Implementation
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response = JsonConvert.DeserializeObject<CreateTerminalResponse>((string)responseMessage[MessageKeys.Content]);
var response = await GetResponseAsync<CreateTerminalResponse>(request).ConfigureAwait(false);
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
return response;
}
public async Task<PauseTerminalOutputResponse> PauseTerminalOutput(byte id, bool pause)
public Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause)
{
var request = new PauseTerminalOutputRequest
{
Id = id,
Pause = pause
};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
return JsonConvert.DeserializeObject<PauseTerminalOutputResponse>((string)responseMessage[MessageKeys.Content]);
return GetResponseAsync<PauseTerminalOutputResponse>(
new PauseTerminalOutputRequest {Id = id, Pause = pause});
}
public void Initialize(IAppServiceConnection appServiceConnection)
@@ -206,15 +200,10 @@ namespace FluentTerminal.App.Services.Implementation
}
}
public Task ResizeTerminal(byte id, TerminalSize size)
public Task ResizeTerminalAsync(byte id, TerminalSize size)
{
var request = new ResizeTerminalRequest
{
TerminalId = id,
NewSize = size
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
return _appServiceConnection.SendMessageAsync(CreateMessage(new ResizeTerminalRequest
{TerminalId = id, NewSize = size}));
}
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
@@ -230,19 +219,15 @@ namespace FluentTerminal.App.Services.Implementation
}
}
public Task UpdateToggleWindowKeyBindings()
public Task UpdateToggleWindowKeyBindingsAsync()
{
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
var request = new SetToggleWindowKeyBindingsRequest
{
KeyBindings = keyBindings
};
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
return _appServiceConnection.SendMessageAsync(CreateMessage(new SetToggleWindowKeyBindingsRequest
{KeyBindings = keyBindings}));
}
public Task Write(byte id, byte[] data)
public Task WriteAsync(byte id, byte[] data)
{
var message = new ValueSet
{
@@ -254,7 +239,7 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(message);
}
public Task CloseTerminal(byte terminalId)
public Task CloseTerminalAsync(byte terminalId)
{
var request = new TerminalExitedRequest(terminalId, -1);
@@ -263,10 +248,6 @@ namespace FluentTerminal.App.Services.Implementation
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
private static readonly Dictionary<string, string> CommandPaths = new Dictionary<string, string>();
private static readonly Dictionary<string, string> CommandErrors = new Dictionary<string, string>();
public async Task<string> GetCommandPathAsync(string command)
{
if (string.IsNullOrWhiteSpace(command))
@@ -288,11 +269,8 @@ namespace FluentTerminal.App.Services.Implementation
throw new Exception(error);
}
var request = new GetCommandPathRequest {Command = command};
var responseMessage = await _appServiceConnection.SendMessageAsync(CreateMessage(request));
var response =
JsonConvert.DeserializeObject<StringValueResponse>((string) responseMessage[MessageKeys.Content]);
var response = await GetResponseAsync<StringValueResponse>(new GetCommandPathRequest {Command = command})
.ConfigureAwait(false);
if (response.Success)
{
@@ -306,13 +284,17 @@ namespace FluentTerminal.App.Services.Implementation
throw new Exception(response.Error);
}
private ValueSet CreateMessage(IMessage content)
public Task QuitApplicationAsync()
{
return new ValueSet
{
[MessageKeys.Type] = content.Identifier,
[MessageKeys.Content] = JsonConvert.SerializeObject(content)
};
return _appServiceConnection.SendMessageAsync(CreateMessage(new QuitApplicationRequest()));
}
private async Task<T> GetResponseAsync<T>(IMessage request)
{
var messageResponse =
await _appServiceConnection.SendMessageAsync(CreateMessage(request)).ConfigureAwait(false);
return JsonConvert.DeserializeObject<T>((string)messageResponse[MessageKeys.Content]);
}
}
}
@@ -17,9 +17,9 @@ namespace FluentTerminal.App.Services.Implementation
_notificationService = notificationService;
}
public async Task CheckForUpdate(bool notifyNoUpdate = false)
public async Task CheckForUpdateAsync(bool notifyNoUpdate = false)
{
var latest = await GetLatestVersionAsync();
var latest = await GetLatestVersionAsync().ConfigureAwait(false);
if (latest > GetCurrentVersion())
{
_notificationService.ShowNotification("Update available",
@@ -42,7 +42,7 @@ namespace FluentTerminal.App.Services.Implementation
var restClient = new RestClient(apiEndpoint);
var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET);
var restResponse = await restClient.ExecuteTaskAsync(restRequest);
var restResponse = await restClient.ExecuteAsync(restRequest).ConfigureAwait(false);
if (restResponse.IsSuccessful)
{
dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content);
+24 -21
View File
@@ -11,9 +11,10 @@ namespace FluentTerminal.App.Services
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
private bool _closingFromUI;
private bool _closingFromUi;
private bool _exited;
private readonly bool _requireShellProcessStart;
private string _fallbackTitle;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
{
@@ -23,6 +24,12 @@ namespace FluentTerminal.App.Services
_requireShellProcessStart = !terminalId.HasValue;
}
public void Reconnect()
{
_exited = false;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
}
private void OnTerminalExited(object sender, TerminalExitStatus status)
{
if (status.TerminalId != Id)
@@ -33,7 +40,7 @@ namespace FluentTerminal.App.Services
_exited = true;
Exited?.Invoke(this, status.ExitCode);
if (_closingFromUI || status.ExitCode <= 0)
if (_closingFromUi || status.ExitCode <= 0)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
@@ -68,23 +75,21 @@ namespace FluentTerminal.App.Services
/// </summary>
public event EventHandler<string> TitleChanged;
public string FallbackTitle { get; private set; }
public byte Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public async Task Close()
public Task CloseAsync()
{
if (_exited)
{
Closed?.Invoke(this, System.EventArgs.Empty);
return;
return Task.CompletedTask;
}
_closingFromUI = true;
_closingFromUi = true;
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
await _trayProcessCommunicationService.CloseTerminal(Id).ConfigureAwait(true);
return _trayProcessCommunicationService.CloseTerminalAsync(Id);
}
/// <summary>
@@ -115,9 +120,9 @@ namespace FluentTerminal.App.Services
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSize(TerminalSize size)
public async Task SetSizeAsync(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminal(Id, size);
await _trayProcessCommunicationService.ResizeTerminalAsync(Id, size).ConfigureAwait(false);
SizeChanged?.Invoke(this, size);
}
@@ -128,18 +133,15 @@ namespace FluentTerminal.App.Services
{
if (string.IsNullOrWhiteSpace(title))
{
title = FallbackTitle;
title = _fallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// to be called by view when ready
/// To be called by view when ready
/// </summary>
/// <param name="shellProfile"></param>
/// <param name="size"></param>
/// <param name="sessionType"></param>
public async Task<TerminalResponse> StartShellProcess(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
public async Task<TerminalResponse> StartShellProcessAsync(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
{
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
{
@@ -150,24 +152,25 @@ namespace FluentTerminal.App.Services
if (_requireShellProcessStart)
{
var response = await _trayProcessCommunicationService.CreateTerminal(Id, size, shellProfile, sessionType).ConfigureAwait(true);
var response = await _trayProcessCommunicationService
.CreateTerminalAsync(Id, size, shellProfile, sessionType).ConfigureAwait(false);
if (response.Success)
{
FallbackTitle = response.ShellExecutableName;
SetTitle(FallbackTitle);
_fallbackTitle = response.Name;
SetTitle(_fallbackTitle);
}
return response;
}
else
{
return await _trayProcessCommunicationService.PauseTerminalOutput(Id, false);
return await _trayProcessCommunicationService.PauseTerminalOutputAsync(Id, false);
}
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.Write(Id, data);
return _trayProcessCommunicationService.WriteAsync(Id, data);
}
/// <summary>
@@ -8,10 +8,11 @@ namespace FluentTerminal.App.Services.Utilities
/// </summary>
public static class I18N
{
private static readonly ResourceLoader ResourceLoader = ResourceLoader.GetForViewIndependentUse();
public static string Translate(string resource)
{
var resourceLoader = ResourceLoader.GetForCurrentView();
return resourceLoader.GetString(resource.Replace('.', '/'));
return ResourceLoader.GetString(resource.Replace('.', '/'));
}
public static string TranslateWithFallback(string resource, string fallback)
@@ -0,0 +1,21 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace FluentTerminal.App.Services.Utilities
{
public class PreserveDictionaryKeyCaseContractResolver : DefaultContractResolver
{
public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
{ ContractResolver = new PreserveDictionaryKeyCaseContractResolver() };
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
contract.DictionaryKeyResolver = key => key;
return contract;
}
}
}
@@ -5,7 +5,7 @@ using System.Reflection;
namespace FluentTerminal.App.Services.Utilities
{
public class TerminalThemeContractResolver : DefaultContractResolver
public class TerminalThemeContractResolver : PreserveDictionaryKeyCaseContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
@@ -24,4 +24,4 @@ namespace FluentTerminal.App.Services.Utilities
return property;
}
}
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
@@ -7,7 +8,7 @@ namespace FluentTerminal.App.ViewModels
{
private const int DelayMilliseconds = 5000;
private readonly CommandProfileProviderViewModel _vm;
private readonly Action _saveAction;
private readonly object _lock = new object();
@@ -18,9 +19,9 @@ namespace FluentTerminal.App.ViewModels
private bool _toSave = true;
internal DelayedHistorySaver(CommandProfileProviderViewModel vm)
internal DelayedHistorySaver(Action saveAction)
{
_vm = vm;
_saveAction = saveAction;
}
public void SetSuccessfulSessionStart()
@@ -86,7 +87,7 @@ namespace FluentTerminal.App.ViewModels
{
if (_toSave)
{
_vm.SaveToHistory();
_saveAction.Invoke();
}
_toSave = false;
@@ -13,4 +13,11 @@
<ProjectReference Include="..\FluentTerminal.Models\FluentTerminal.Models.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.UniversalApiContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.UniversalApiContract\5.0.0.0\Windows.Foundation.UniversalApiContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
+13 -10
View File
@@ -1,18 +1,21 @@
using FluentTerminal.Models;
using System;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public interface ITerminalView
public interface ITerminalView : IDisposable
{
Task ChangeTheme(TerminalTheme theme);
Task ChangeKeyBindings();
Task ChangeOptions(TerminalOptions options);
Task Initialize(TerminalViewModel viewModel);
Task ChangeThemeAsync(TerminalTheme theme);
Task ChangeKeyBindingsAsync();
Task ChangeOptionsAsync(TerminalOptions options);
Task InitializeAsync(TerminalViewModel viewModel);
Task ReconnectAsync();
void DisposalPrepare();
Task FindNext(string searchText);
Task FindPrevious(string searchText);
Task FocusTerminal();
Task<string> SerializeXtermState();
Task FindNextAsync(SearchRequest request);
Task FindPreviousAsync(SearchRequest request);
Task FocusTerminalAsync();
Task<string> SerializeXtermStateAsync();
void Paste(string text);
}
}
@@ -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();
}
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,147 @@
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
namespace FluentTerminal.App.ViewModels.Menu
{
public class MenuItemKeyBindingViewModel : ViewModelBase
{
#region Properties
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();
}
}
}
#endregion Properties
#region Constructors
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)
{
}
#endregion Constructors
#region Methods
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;
}
#endregion Methods
}
}
@@ -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,19 +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 = new DispatcherTimer {Interval = new TimeSpan(0, 0, 2)};
}
public bool ShowOverlay
@@ -17,7 +17,6 @@ namespace FluentTerminal.App.ViewModels
/// <summary>
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class ProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
{
#region Fields
@@ -63,6 +62,7 @@ namespace FluentTerminal.App.ViewModels
#region Events
// Needs to be triggered from UI thread
public event EventHandler Deleted;
#endregion Events
@@ -102,9 +102,9 @@ namespace FluentTerminal.App.ViewModels
Initialize(shellProfile);
DeleteCommand = new AsyncCommand(Delete, CanDelete);
DeleteCommand = new AsyncCommand(DeleteAsync, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEdit);
CancelEditCommand = new AsyncCommand(CancelEditAsync);
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
}
@@ -113,6 +113,7 @@ namespace FluentTerminal.App.ViewModels
#region Methods
// Requires UI thread
// Loads view model properties from the input shellProfile
protected void Initialize(ShellProfile shellProfile)
{
@@ -134,9 +135,11 @@ namespace FluentTerminal.App.ViewModels
|| !(_environmentVariablesBeforeEdit?.SequenceEqual(EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value)) ?? false);
}
private async Task Delete()
// Requires UI thread
private async Task DeleteAsync()
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
// ConfigureAwait(true) because we need to trigger Deleted event from the calling (UI) thread.
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
@@ -162,58 +165,64 @@ namespace FluentTerminal.App.ViewModels
InEditMode = true;
}
private async Task CancelEdit()
// Requires UI thread
private async Task CancelEditAsync()
{
if (IsNew)
{
await Delete();
}
else
{
if (HasChanges())
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true);
if (result == DialogButton.OK)
{
// Cancelled, so rollback
ProfileVm.RejectChanges();
KeyBindings.Editable = false;
InEditMode = false;
}
}
else
{
KeyBindings.Editable = false;
InEditMode = false;
}
}
}
public virtual async Task SaveChangesAsync()
{
var error = await ProfileVm.AcceptChangesAsync();
if (!string.IsNullOrEmpty(error))
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), error, DialogButton.OK);
await DeleteAsync().ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Any(x => string.IsNullOrWhiteSpace(x)))
if (!HasChanges())
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK);
KeyBindings.Editable = false;
InEditMode = false;
return;
}
// ConfigureAwait(true) because we're setting some view-model properties afterwards
if (await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true) == DialogButton.OK)
{
// Cancelled, so rollback
// ConfigureAwait(true) because we're setting some view-model properties afterwards
await ProfileVm.RejectChangesAsync().ConfigureAwait(true);
KeyBindings.Editable = false;
InEditMode = false;
}
}
// Requires UI thread
private async Task SaveChangesAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var error = await ProfileVm.AcceptChangesAsync().ConfigureAwait(true);
if (!string.IsNullOrEmpty(error))
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"), error, DialogButton.OK)
.ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Any(string.IsNullOrWhiteSpace))
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK).ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
{
await DialogService.ShowMessageDialogAsnyc(I18N.Translate("InvalidInput"), I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK);
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK).ConfigureAwait(false);
return;
}
@@ -239,9 +248,9 @@ namespace FluentTerminal.App.ViewModels
IsNew = false;
}
public Task AddKeyboardShortcut()
private Task AddKeyboardShortcut()
{
return KeyBindings.ShowAddKeyBindingDialog();
return KeyBindings.ShowAddKeyBindingDialogAsync();
}
#endregion Methods
@@ -90,7 +90,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile);
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
profile.Location = _location;
profile.Arguments = _arguments;
@@ -99,7 +99,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync();
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
@@ -123,18 +123,22 @@ namespace FluentTerminal.App.ViewModels.Profiles
!Model.Arguments.NullableEqualTo(_arguments);
}
// Requires UI thread
private async Task BrowseForCustomShell()
{
var file = await _fileSystemService.OpenFile(new[] { ".exe" }).ConfigureAwait(true);
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var file = await _fileSystemService.OpenFileAsync(new[] { ".exe" }).ConfigureAwait(true);
if (file != null)
{
Location = file.Path;
}
}
// Requires UI thread
private async Task BrowseForWorkingDirectory()
{
var directory = await _fileSystemService.BrowseForDirectory().ConfigureAwait(true);
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var directory = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (directory != null)
{
WorkingDirectory = directory;
@@ -1,18 +0,0 @@
using System;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels.Profiles
{
public class ExecutedCommand
{
public string Value { get; set; }
public bool IsProfile { get; set; }
public DateTime LastExecution { get; set; }
public int ExecutionCount { get; set; }
public ShellProfile ShellProfile { get; set; }
}
}
@@ -4,9 +4,9 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Windows.UI.Core;
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight;
@@ -18,13 +18,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
/// </summary>
public abstract class ProfileProviderViewModelBase : ViewModelBase
{
#region Static
private static readonly LineEndingStyle[] LineEndingStylesArray =
Enum.GetValues(typeof(LineEndingStyle)).Cast<LineEndingStyle>().ToArray();
#endregion Static
#region Fields
protected readonly ISettingsService SettingsService;
@@ -67,17 +60,6 @@ namespace FluentTerminal.App.ViewModels.Profiles
public ObservableCollection<TerminalTheme> TerminalThemes { get; }
public ObservableCollection<LineEndingStyle> LineEndingStyles { get; } =
new ObservableCollection<LineEndingStyle>(LineEndingStylesArray);
private LineEndingStyle _lineEndingTranslation;
public LineEndingStyle LineEndingTranslation
{
get => _lineEndingTranslation;
set => Set(ref _lineEndingTranslation, value);
}
private TabTheme _selectedTabTheme;
public TabTheme SelectedTabTheme
@@ -129,13 +111,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
if (theme == null)
{
// How to handle attempt to setting null theme?
// 1) Throw an exception:
//throw new ArgumentNullException();
// 2) Ignore the attempt:
//return;
// 3) Use default theme (probably the best):
theme = TerminalThemes.First();
return;
}
else if (!TerminalThemes.Contains(theme))
{
@@ -172,6 +148,14 @@ namespace FluentTerminal.App.ViewModels.Profiles
set => Set(ref _useConPty, value);
}
private bool _useBuffer;
public bool UseBuffer
{
get => _useBuffer;
set => Set(ref _useBuffer, value);
}
#endregion Properties
#region Constructor
@@ -214,7 +198,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return;
}
ApplicationView.RunOnDispatcherThread(() =>
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
TerminalThemes.Remove(theme);
@@ -222,18 +206,18 @@ namespace FluentTerminal.App.ViewModels.Profiles
{
SelectedTerminalTheme = TerminalThemes.First();
}
}, false);
}, CoreDispatcherPriority.Low, true);
}
private void OnThemeAdded(ThemeAddedMessage message)
{
ApplicationView.RunOnDispatcherThread(() => TerminalThemes.Add(message.Theme), false);
ApplicationView.ExecuteOnUiThreadAsync(() => TerminalThemes.Add(message.Theme), CoreDispatcherPriority.Low);
}
private void Initialize(ShellProfile profile)
{
LineEndingTranslation = profile.LineEndingTranslation;
UseConPty = profile.UseConPty;
UseBuffer = profile.UseBuffer;
TerminalThemeId = profile.TerminalThemeId;
TabThemeId = profile.TabThemeId;
}
@@ -245,8 +229,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
protected virtual Task CopyToProfileAsync(ShellProfile profile)
{
profile.LineEndingTranslation = _lineEndingTranslation;
profile.UseConPty = _useConPty;
profile.UseBuffer = _useBuffer;
profile.TerminalThemeId = _terminalThemeId;
profile.TabThemeId = _tabThemeId;
return Task.CompletedTask;
@@ -263,8 +247,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
/// </summary>
public virtual bool HasChanges()
{
return Model.LineEndingTranslation != _lineEndingTranslation ||
Model.UseConPty != _useConPty ||
return Model.UseConPty != _useConPty ||
Model.UseBuffer != _useBuffer ||
!Model.TerminalThemeId.Equals(_terminalThemeId) ||
Model.TabThemeId != _tabThemeId;
}
@@ -280,19 +264,19 @@ namespace FluentTerminal.App.ViewModels.Profiles
/// message returned by <see cref="ValidateAsync"/> method.</returns>
public async Task<string> AcceptChangesAsync(bool acceptIfInvalid = false)
{
var error = await ValidateAsync();
var error = await ValidateAsync().ConfigureAwait(false);
if (acceptIfInvalid || string.IsNullOrEmpty(error))
{
await CopyToProfileAsync(Model);
await CopyToProfileAsync(Model).ConfigureAwait(false);
}
return error;
}
public void RejectChanges()
public Task RejectChangesAsync()
{
ApplicationView.RunOnDispatcherThread(() => LoadFromProfile(Model), false);
return ApplicationView.ExecuteOnUiThreadAsync(() => LoadFromProfile(Model));
}
#endregion Methods
@@ -300,7 +284,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
#region Links/shortcuts related
private const string UseConPtyQueryStringName = "conpty";
private const string LineEndingQueryStringName = "lineending";
private const string UseBufferQueryStringName = "buffer";
private const string TerminalThemeIdQueryStringName = "theme";
private const string TabThemeIdQueryStringName = "tab";
@@ -340,8 +324,7 @@ URL={0}
public string GetBaseQueryString()
{
var queryString =
$"{UseConPtyQueryStringName}={_useConPty}&{LineEndingQueryStringName}={_lineEndingTranslation}";
var queryString = $"{UseConPtyQueryStringName}={_useConPty}&{UseBufferQueryStringName}={_useBuffer}";
if (_tabThemeId != TabThemes.First().Id)
{
@@ -372,11 +355,11 @@ URL={0}
}
keyValue = queryStringParams.FirstOrDefault(t =>
LineEndingQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
UseBufferQueryStringName.Equals(t.Item1, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(keyValue?.Item2) && Enum.TryParse(keyValue.Item2, true, out LineEndingStyle lineEndingTranslation))
if (!string.IsNullOrEmpty(keyValue?.Item2) && bool.TryParse(keyValue.Item2?.ToLower(), out bool useBuffer))
{
LineEndingTranslation = lineEndingTranslation;
UseConPty = useBuffer;
}
keyValue = queryStringParams.FirstOrDefault(t =>
@@ -123,14 +123,16 @@ namespace FluentTerminal.App.ViewModels.Profiles
public SshConnectViewModel(ISettingsService settingsService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService, IFileSystemService fileSystemService,
SshProfile original = null) : base(settingsService, applicationView, true,
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault })
original ?? new SshProfile { UseMosh = settingsService.GetApplicationSettings().UseMoshByDefault,
RequestConPty = settingsService.GetApplicationSettings().UseConPty
})
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_fileSystemService = fileSystemService;
Initialize((SshProfile)Model);
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFile);
BrowseForIdentityFileCommand = new AsyncCommand(BrowseForIdentityFileAsync);
}
#endregion Constructor
@@ -147,13 +149,13 @@ namespace FluentTerminal.App.ViewModels.Profiles
if (string.IsNullOrEmpty(Username))
{
_trayProcessCommunicationService.GetUserName().ContinueWith(t =>
_trayProcessCommunicationService.GetUserNameAsync().ContinueWith(t =>
{
var username = t.Result;
if (string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(username))
{
ApplicationView.RunOnDispatcherThread(() => Username = username, false);
ApplicationView.ExecuteOnUiThreadAsync(() => Username = username);
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
}
@@ -164,9 +166,11 @@ namespace FluentTerminal.App.ViewModels.Profiles
MoshPortTo = sshProfile.MoshPortTo;
}
private async Task BrowseForIdentityFile()
// Requires UI thread
private async Task BrowseForIdentityFileAsync()
{
var file = await _fileSystemService.OpenFile(new[] { "*" });
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var file = await _fileSystemService.OpenFileAsync(new[] { "*" }).ConfigureAwait(true);
if (file != null)
{
IdentityFile = file.Path;
@@ -175,20 +179,28 @@ namespace FluentTerminal.App.ViewModels.Profiles
private string GetArgumentsString()
{
StringBuilder sb = new StringBuilder();
var sshArguments = GetSshArguments();
if (_sshPort != SshProfile.DefaultSshPort)
sb.Append($"-p {_sshPort:#####} ");
if (!_useMosh)
{
return string.IsNullOrWhiteSpace(sshArguments) ? $"{_username}@{_host}" : $"{sshArguments} {_username}@{_host}";
}
if (!string.IsNullOrEmpty(_identityFile))
sb.Append($"-i \"{_identityFile}\" ");
return string.IsNullOrEmpty(sshArguments)
? $"-p {_moshPortFrom}:{_moshPortTo} {_username}@{_host}"
: $"-p {_moshPortFrom}:{_moshPortTo} --ssh=\"ssh {sshArguments}\" {_username}@{_host}";
}
sb.Append($"{_username}@{_host}");
private string GetSshArguments()
{
if (string.IsNullOrEmpty(_identityFile))
{
return _sshPort == SshProfile.DefaultSshPort ? null : $"-p {_sshPort:#####}";
}
if (_useMosh)
sb.Append($" {_moshPortFrom}:{_moshPortTo}");
return sb.ToString();
return _sshPort == SshProfile.DefaultSshPort
? $"-i \"{_identityFile}\""
: $"-p {_sshPort:#####} -i \"{_identityFile}\"";
}
protected override void LoadFromProfile(ShellProfile profile)
@@ -200,9 +212,9 @@ namespace FluentTerminal.App.ViewModels.Profiles
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile);
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
SshProfile sshProfile = (SshProfile) profile;
var sshProfile = (SshProfile) profile;
sshProfile.Location = _useMosh ? Constants.MoshCommandName : Constants.SshCommandName;
sshProfile.Arguments = GetArgumentsString();
@@ -220,14 +232,14 @@ namespace FluentTerminal.App.ViewModels.Profiles
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync();
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
return error;
}
var result = await GetSshInfoValidationResultAsync();
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
if (result == SshConnectionInfoValidationResult.Valid)
{
@@ -254,7 +266,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
original.MoshPortFrom != _moshPortFrom || original.MoshPortTo != _moshPortTo;
}
public async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
private async Task<SshConnectionInfoValidationResult> GetSshInfoValidationResultAsync()
{
var result = SshConnectionInfoValidationResult.Valid;
@@ -273,7 +285,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
result |= SshConnectionInfoValidationResult.SshPortZeroOrNegative;
}
if (!await CheckIdentityFileExistsAsync())
if (!await CheckIdentityFileExistsAsync().ConfigureAwait(false))
{
result |= SshConnectionInfoValidationResult.IdentityFileDoesNotExist;
}
@@ -296,6 +308,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return result;
}
// Requires UI thread
private async Task<bool> CheckIdentityFileExistsAsync()
{
var identityFile = _identityFile;
@@ -315,7 +328,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
}
else
{
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync();
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var sshConfigDir = await _trayProcessCommunicationService.GetSshConfigDirAsync().ConfigureAwait(true);
if (string.IsNullOrEmpty(sshConfigDir))
{
@@ -325,7 +339,8 @@ namespace FluentTerminal.App.ViewModels.Profiles
fullPath = Path.Combine(sshConfigDir, identityFile);
}
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath))
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
if (await _trayProcessCommunicationService.CheckFileExistsAsync(fullPath).ConfigureAwait(true))
{
_validatedIdentityFile = identityFile;
@@ -435,14 +450,14 @@ namespace FluentTerminal.App.ViewModels.Profiles
public override async Task<Tuple<bool, string>> GetUrlAsync()
{
var error = await base.ValidateAsync();
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
return Tuple.Create(false, error);
}
var result = await GetSshInfoValidationResultAsync();
var result = await GetSshInfoValidationResultAsync().ConfigureAwait(false);
if (result != SshConnectionInfoValidationResult.Valid &&
// For links we can ignore missing username
@@ -459,7 +474,7 @@ namespace FluentTerminal.App.ViewModels.Profiles
return Tuple.Create(false, error);
}
StringBuilder sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
var sb = new StringBuilder(_useMosh ? MoshUriScheme : SshUriScheme);
sb.Append("://");
@@ -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
@@ -18,7 +17,7 @@ namespace FluentTerminal.App.ViewModels.Settings
_updateService = updateService;
_applicationView = applicationView;
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdate(true));
CheckForUpdatesCommand = new AsyncCommand(() => CheckForUpdateAsync(true));
}
public IAsyncCommand CheckForUpdatesCommand { get; }
@@ -28,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}";
}
}
@@ -59,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);
}
}
}
@@ -4,7 +4,6 @@ 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;
@@ -40,8 +39,8 @@ namespace FluentTerminal.App.ViewModels.Settings
_applicationSettings = _settingsService.GetApplicationSettings();
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectory);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
BrowseLogDirectoryCommand = new AsyncCommand(BrowseLogDirectoryAsync);
}
public IEnumerable<string> Languages => _applicationLanguageService.Languages;
@@ -62,9 +61,11 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public async Task OnNavigatedTo()
// Requires UI thread
public async Task OnNavigatedToAsync()
{
var startupTaskStatus = await _startupTaskService.GetStatus();
// ConfigureAwait(true) because we want to execute SetStartupTaskPropertiesForStatus from the calling (UI) thread.
var startupTaskStatus = await _startupTaskService.GetStatusAsync().ConfigureAwait(true);
SetStartupTaskPropertiesForStatus(startupTaskStatus);
}
@@ -172,7 +173,7 @@ namespace FluentTerminal.App.ViewModels.Settings
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
_trayProcessCommunicationService.MuteTerminal(value);
_trayProcessCommunicationService.MuteTerminalAsync(value);
}
}
}
@@ -218,6 +219,20 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
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;
@@ -268,6 +283,21 @@ namespace FluentTerminal.App.ViewModels.Settings
_applicationSettings.NewTerminalLocation = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
RaisePropertyChanged(nameof(TabIsSelected));
}
}
}
public bool TabWindowCascadingAppMenu
{
get => _applicationSettings.TabWindowCascadingAppMenu;
set
{
if (_applicationSettings.TabWindowCascadingAppMenu != value)
{
_applicationSettings.TabWindowCascadingAppMenu = value;
_settingsService.SaveApplicationSettings(_applicationSettings);
RaisePropertyChanged();
}
}
}
@@ -281,11 +311,10 @@ namespace FluentTerminal.App.ViewModels.Settings
get => _startupTaskEnabled;
set
{
if (_startupTaskEnabled != value)
if (Set(ref _startupTaskEnabled, value))
{
_startupTaskEnabled = value;
RaisePropertyChanged(nameof(StartupTaskEnabled));
SetStartupTaskState(value);
// ReSharper disable once AssignmentIsFullyDiscarded
_ = SetStartupTaskStateAsync(value);
}
}
}
@@ -299,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
@@ -362,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)
{
@@ -390,9 +431,12 @@ namespace FluentTerminal.App.ViewModels.Settings
EnableLogging = defaults.EnableLogging;
PrintableOutputOnly = defaults.PrintableOutputOnly;
LogDirectoryPath = defaults.LogDirectoryPath;
UseConPty = defaults.UseConPty;
ShowTextCopied = defaults.ShowTextCopied;
}
}
// Requires UI thread
private void SetStartupTaskPropertiesForStatus(StartupTaskStatus startupTaskStatus)
{
switch (startupTaskStatus)
@@ -420,27 +464,39 @@ namespace FluentTerminal.App.ViewModels.Settings
StartupTaskErrorMessage = I18N.Translate("DisabledByPolicy");
CanEnableStartupTask = false;
break;
case StartupTaskStatus.EnabledByPolicy:
StartupTaskEnabled = true;
StartupTaskErrorMessage = I18N.TranslateWithFallback("EnabledByPolicy", "Enabled by policy.");
CanEnableStartupTask = false;
break;
}
}
private async Task SetStartupTaskState(bool enabled)
// Requires UI thread
private async Task SetStartupTaskStateAsync(bool enabled)
{
StartupTaskStatus status;
if (enabled)
{
status = await _startupTaskService.EnableStartupTask();
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
status = await _startupTaskService.EnableStartupTaskAsync().ConfigureAwait(true);
}
else
{
await _startupTaskService.DisableStartupTask();
status = await _startupTaskService.GetStatus();
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
await _startupTaskService.DisableStartupTaskAsync().ConfigureAwait(true);
// ConfigureAwait(true) because we need to execute SetStartupTaskPropertiesForStatus in the calling (UI) thread.
status = await _startupTaskService.GetStatusAsync().ConfigureAwait(true);
}
SetStartupTaskPropertiesForStatus(status);
}
private async Task BrowseLogDirectory()
// Requires UI thread
private async Task BrowseLogDirectoryAsync()
{
var folder = await _fileSystemService.BrowseForDirectory();
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var folder = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (folder != null)
{
@@ -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)
{
@@ -23,8 +23,8 @@ namespace FluentTerminal.App.ViewModels.Settings
_settingsService = settingsService;
_dialogService = dialogService;
_trayProcessCommunicationService = trayProcessCommunicationService;
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaults().ConfigureAwait(false));
AddCommand = new RelayCommand<string>(async command => await Add(command).ConfigureAwait(false));
RestoreDefaultsCommand = new RelayCommand(async () => await RestoreDefaultsAsync().ConfigureAwait(false));
AddCommand = new RelayCommand<string>(async command => await AddAsync(command).ConfigureAwait(false));
Initialize(_settingsService.GetCommandKeyBindings());
}
@@ -33,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)
@@ -49,6 +49,7 @@ namespace FluentTerminal.App.ViewModels.Settings
KeyBindings.Clear();
}
// Requires UI thread
private void Initialize(IDictionary<string, ICollection<KeyBinding>> keyBindings)
{
ClearKeyBindings();
@@ -69,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);
@@ -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);
@@ -68,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;
@@ -82,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;
@@ -150,34 +178,6 @@ namespace FluentTerminal.App.ViewModels.Settings
}
}
public bool BoldText
{
get => _terminalOptions.BoldText;
set
{
if (_terminalOptions.BoldText != value)
{
_terminalOptions.BoldText = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public bool ShowTextCopied
{
get => _terminalOptions.ShowTextCopied;
set
{
if (_terminalOptions.ShowTextCopied != value)
{
_terminalOptions.ShowTextCopied = value;
_settingsService.SaveTerminalOptions(_terminalOptions);
RaisePropertyChanged();
}
}
}
public IEnumerable<FontInfo> Fonts { get; }
public int FontSize
@@ -226,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)
{
@@ -238,10 +242,9 @@ namespace FluentTerminal.App.ViewModels.Settings
ScrollBarStyle = defaults.ScrollBarStyle;
FontFamily = defaults.FontFamily;
FontSize = defaults.FontSize;
BoldText = defaults.BoldText;
FontWeight = defaults.FontWeight;
BackgroundOpacity = defaults.BackgroundOpacity;
ScrollBackLimit = defaults.ScrollBackLimit.ToString();
ShowTextCopied = defaults.ShowTextCopied;
WordSeparator = defaults.WordSeparator;
}
}
@@ -252,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();
}
@@ -21,20 +21,28 @@ namespace FluentTerminal.App.ViewModels.Settings
private double _backgroundOpacity;
private readonly IThemeParserFactory _themeParserFactory;
private readonly IFileSystemService _fileSystemService;
private readonly IImageFileSystemService _imageFileSystemService;
public event EventHandler<string> SelectedThemeBackgroundColorChanged;
public event EventHandler<ImageFile> SelectedThemeBackgroundImageChanged;
public event EventHandler<ThemeViewModel> SelectedThemeChanged;
public ThemesPageViewModel(ISettingsService settingsService, IDialogService dialogService, IDefaultValueProvider defaultValueProvider,
IThemeParserFactory themeParserFactory, IFileSystemService fileSystemService)
public ThemesPageViewModel(ISettingsService settingsService,
IDialogService dialogService,
IDefaultValueProvider defaultValueProvider,
IThemeParserFactory themeParserFactory,
IFileSystemService fileSystemService,
IImageFileSystemService imageFileSystemService)
{
_settingsService = settingsService;
_dialogService = dialogService;
_defaultValueProvider = defaultValueProvider;
_themeParserFactory = themeParserFactory;
_fileSystemService = fileSystemService;
_imageFileSystemService = imageFileSystemService;
CreateThemeCommand = new RelayCommand(CreateTheme);
ImportThemeCommand = new AsyncCommand(ImportTheme);
ImportThemeCommand = new AsyncCommand(ImportThemeAsync);
CloneCommand = new RelayCommand<ThemeViewModel>(CloneTheme);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
@@ -44,7 +52,7 @@ namespace FluentTerminal.App.ViewModels.Settings
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;
@@ -76,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;
}
}
}
@@ -113,28 +127,49 @@ namespace FluentTerminal.App.ViewModels.Settings
AddTheme(theme);
}
private async Task ImportTheme()
// Requires UI thread
private async Task ImportThemeAsync()
{
var file = await _fileSystemService.OpenFile(_themeParserFactory.SupportedFileTypes).ConfigureAwait(true);
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
var file = await _fileSystemService.OpenFileAsync(_themeParserFactory.SupportedFileTypes)
.ConfigureAwait(true);
if (file != null)
{
var parser = _themeParserFactory.GetParser(file.FileType);
if (parser == null)
{
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
await _dialogService.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"),
I18N.Translate("NoSuitableParserFound"), DialogButton.OK).ConfigureAwait(false);
return;
}
try
{
var theme = await parser.Parse(file.Name, file.Content).ConfigureAwait(true);
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
var exportedTheme = await parser.Import(file.Name, file.Content).ConfigureAwait(true);
AddTheme(theme);
if (!string.IsNullOrWhiteSpace(exportedTheme.EncodedImage))
{
// ConfigureAwait(true) because we need to execute AddTheme method in the calling (UI) thread.
var importedImage = await _imageFileSystemService
.ImportThemeImageAsync(exportedTheme.BackgroundImage, exportedTheme.EncodedImage)
.ConfigureAwait(true);
exportedTheme.BackgroundImage = importedImage;
}
var terminalTheme = new TerminalTheme(exportedTheme);
AddTheme(terminalTheme);
}
catch (Exception exception)
{
await _dialogService.ShowMessageDialogAsnyc(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK).ConfigureAwait(false);
await _dialogService
.ShowMessageDialogAsync(I18N.Translate("ImportThemeFailed"), exception.Message, DialogButton.OK)
.ConfigureAwait(false);
}
}
}
@@ -143,7 +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;
@@ -192,5 +227,10 @@ namespace FluentTerminal.App.ViewModels.Settings
{
SelectedThemeBackgroundColorChanged?.Invoke(this, e);
}
private void OnSelectedThemeBackgroundImageChanged(object sender, ImageFile e)
{
SelectedThemeBackgroundImageChanged?.Invoke(this, e);
}
}
}
@@ -10,13 +10,14 @@ namespace FluentTerminal.App.ViewModels
public SettingsViewModel(ISettingsService settingsService, IDefaultValueProvider defaultValueProvider, IDialogService dialogService,
ITrayProcessCommunicationService trayProcessCommunicationService, IThemeParserFactory themeParserFactory, ISystemFontService systemFontService,
IFileSystemService fileSystemService, IStartupTaskService startupTaskService, IApplicationView applicationView,
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers)
IApplicationLanguageService applicationLanguageService, ApplicationDataContainers containers,
IImageFileSystemService imageFileSystemService)
{
KeyBindings = new KeyBindingsPageViewModel(settingsService, dialogService, trayProcessCommunicationService);
General = new GeneralPageViewModel(settingsService, dialogService, defaultValueProvider, startupTaskService, applicationLanguageService, trayProcessCommunicationService, fileSystemService);
Profiles = new ProfilesPageViewModel(settingsService, dialogService, defaultValueProvider, fileSystemService, applicationView);
Terminal = new TerminalPageViewModel(settingsService, dialogService, defaultValueProvider, systemFontService);
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService);
Themes = new ThemesPageViewModel(settingsService, dialogService, defaultValueProvider, themeParserFactory, fileSystemService, imageFileSystemService);
Mouse = new MousePageViewModel(settingsService, dialogService, defaultValueProvider);
SshProfiles = new SshProfilesPageViewModel(settingsService, dialogService, fileSystemService,
applicationView, trayProcessCommunicationService, containers.HistoryContainer);
@@ -60,21 +60,23 @@ namespace FluentTerminal.App.ViewModels
shellProfile);
SetDefaultCommand = new RelayCommand(SetDefault);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaults);
RestoreDefaultsCommand = new AsyncCommand(RestoreDefaultsAsync);
}
#endregion Constrcutor
#region Methods
private async Task RestoreDefaults()
// Requires UI thread
private async Task RestoreDefaultsAsync()
{
if (InEditMode || !ProfileVm.PreInstalled)
{
throw new InvalidOperationException();
}
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"),
// ConfigureAwait(true) because we need to execute Initialize method in the calling (UI) thread.
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmRestoreProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
@@ -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; }
}
}
@@ -9,10 +9,15 @@ 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
{
@@ -25,21 +30,25 @@ namespace FluentTerminal.App.ViewModels
#endregion Static
#region Serialize terminal state
private class TerminalState
{
public bool HasCustomTitle { get; set; }
public string ShellTitle { get; set; }
public string TabTitle { get; set; }
public TerminalTheme TerminalTheme { get; set; }
public TabTheme TabTheme { get; set; }
public int TabThemeId { get; set; }
public bool ShowSearchPanel { get; set; }
public string SearchText { get; set; }
public bool SearchMatchCase { get; set; }
public bool SearchWholeWord { get; set; }
public bool SearchWithRegex { get; set; }
public string XtermBufferState { get; set; }
public byte TerminalId { get; set; }
public ShellProfile ShellProfile { get; set; }
}
public async Task<string> Serialize()
public async Task<string> SerializeAsync()
{
TerminalState state = new TerminalState
{
@@ -47,10 +56,13 @@ namespace FluentTerminal.App.ViewModels
ShellTitle = ShellTitle,
TabTitle = TabTitle,
TerminalTheme = TerminalTheme,
TabTheme = TabTheme,
TabThemeId = TabTheme.Theme.Id,
ShowSearchPanel = ShowSearchPanel,
SearchText = SearchText,
XtermBufferState = await SerializeXtermState(),
SearchMatchCase = SearchMatchCase,
SearchWholeWord = SearchWholeWord,
SearchWithRegex = SearchWithRegex,
XtermBufferState = await SerializeXtermStateAsync().ConfigureAwait(false),
TerminalId = Terminal.Id,
ShellProfile = ShellProfile
};
@@ -67,12 +79,17 @@ namespace FluentTerminal.App.ViewModels
ShellTitle = state.ShellTitle;
TabTitle = state.TabTitle;
TerminalTheme = state.TerminalTheme;
TabTheme = state.TabTheme;
TabTheme = TabThemes.FirstOrDefault(t => t.Theme.Id == state.TabThemeId);
ShowSearchPanel = state.ShowSearchPanel;
SearchText = state.SearchText;
SearchMatchCase = state.SearchMatchCase;
SearchWholeWord = state.SearchWholeWord;
SearchWithRegex = state.SearchWithRegex;
XtermBufferState = state.XtermBufferState;
_terminalId = state.TerminalId;
ShellProfile = state.ShellProfile;
TabTheme.IsSelected = true;
}
}
@@ -85,21 +102,29 @@ namespace FluentTerminal.App.ViewModels
private bool _hasExitedWithError;
private string _searchText;
private bool _showSearchPanel;
private TabTheme _tabTheme;
private bool _searchMatchCase;
private bool _searchWholeWord;
private bool _searchWithRegex;
private TabThemeViewModel _tabTheme;
private TerminalTheme _terminalTheme;
private TerminalOptions _terminalOptions;
private string _tabTitle;
private string _shellTitle;
private bool _hasCustomTitle;
private byte? _terminalId;
private bool _hasSelection;
private string _hoveredUri;
private MenuViewModel _contextMenu;
private MenuViewModel _tabContextMenu;
public TerminalViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService,
IKeyboardCommandService keyboardCommandService, ApplicationSettings applicationSettings, ShellProfile shellProfile,
IApplicationView applicationView, IDispatcherTimer dispatcherTimer, IClipboardService clipboardService, string terminalState = null)
IApplicationView applicationView, IClipboardService clipboardService, string terminalState = null)
{
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
MessengerInstance.Register<CurrentThemeChangedMessage>(this, OnCurrentThemeChanged);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
MessengerInstance.Register<KeyBindingsChangedMessage>(this, OnKeyBindingsChanged);
SettingsService = settingsService;
@@ -116,24 +141,31 @@ namespace FluentTerminal.App.ViewModels
ShellProfile = shellProfile;
TerminalTheme = shellProfile.TerminalThemeId == Guid.Empty ? SettingsService.GetCurrentTheme() : SettingsService.GetTheme(shellProfile.TerminalThemeId);
TabThemes = new ObservableCollection<TabTheme>(SettingsService.GetTabThemes());
TabTheme = TabThemes.FirstOrDefault(t => t.Id == ShellProfile.TabThemeId);
CloseCommand = new AsyncCommand(CloseTab, CanExecuteCommand);
TabThemes = new ObservableCollection<TabThemeViewModel>(SettingsService.GetTabThemes().Select(theme => new TabThemeViewModel(theme, this)));
CloseCommand = new 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);
SelectTabThemeCommand = new RelayCommand<string>(SelectTabTheme, CanExecuteCommand);
EditTitleCommand = new AsyncCommand(EditTitle, 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);
if (!String.IsNullOrEmpty(terminalState))
if (!string.IsNullOrEmpty(terminalState))
{
Restore(terminalState);
}
else
{
var defaultTabTheme = TabThemes.FirstOrDefault(t => t.Theme.Id == ShellProfile.TabThemeId);
defaultTabTheme.IsSelected = true;
}
Terminal = new Terminal(TrayProcessCommunicationService, _terminalId);
Terminal.KeyboardCommandReceived += Terminal_KeyboardCommandReceived;
@@ -143,8 +175,20 @@ 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;
@@ -161,12 +205,10 @@ namespace FluentTerminal.App.ViewModels
public event EventHandler Activated;
public event EventHandler Closed;
public event EventHandler<string> FindNextRequested;
public event EventHandler<string> FindPreviousRequested;
public event EventHandler<SearchRequest> FindNextRequested;
public event EventHandler<SearchRequest> FindPreviousRequested;
public event EventHandler SearchStarted;
public event EventHandler<TerminalTheme> ThemeChanged;
public event EventHandler<string> ShellTitleChanged;
public event EventHandler<string> CustomTitleChanged;
public event EventHandler CloseLeftTabsRequested;
public event EventHandler CloseRightTabsRequested;
public event EventHandler CloseOtherTabsRequested;
@@ -181,7 +223,7 @@ namespace FluentTerminal.App.ViewModels
// 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 :
_tabTheme?.Theme :
SettingsService.GetTabThemes().FirstOrDefault(t => t.Color == null);
}
@@ -209,6 +251,18 @@ namespace FluentTerminal.App.ViewModels
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;
@@ -218,8 +272,13 @@ 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));
@@ -227,6 +286,24 @@ namespace FluentTerminal.App.ViewModels
}
}
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;
@@ -245,7 +322,7 @@ namespace FluentTerminal.App.ViewModels
}
public bool IsUnderlined => (IsSelected && ApplicationSettings.UnderlineSelectedTab) ||
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Color != null);
(!IsSelected && ApplicationSettings.InactiveTabColorMode == InactiveTabColorMode.Underlined && TabTheme.Theme.Color != null);
public bool HasNewOutput
{
@@ -262,6 +339,7 @@ namespace FluentTerminal.App.ViewModels
{
HasNewOutput = false;
}
ReconnectTabCommand.RaiseCanExecuteChanged();
}
}
@@ -271,7 +349,23 @@ 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; }
@@ -283,7 +377,7 @@ namespace FluentTerminal.App.ViewModels
set => Set(ref _showSearchPanel, value);
}
public TabTheme TabTheme
public TabThemeViewModel TabTheme
{
get => _tabTheme;
set
@@ -294,11 +388,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
{
@@ -334,10 +428,7 @@ namespace FluentTerminal.App.ViewModels
}
}
if (Set(ref _tabTitle, title))
{
CustomTitleChanged?.Invoke(this, title);
}
Set(ref _tabTitle, title);
}
}
@@ -355,25 +446,32 @@ namespace FluentTerminal.App.ViewModels
public ITrayProcessCommunicationService TrayProcessCommunicationService { get; }
public Task Close()
public Task CloseAsync()
{
MessengerInstance.Unregister(this);
return Terminal.Close();
return Terminal.CloseAsync();
}
public void CopyText(string text)
public Task CopyTextAsync(string text)
{
ClipboardService.SetText(text);
if (_terminalOptions.ShowTextCopied)
return ApplicationView.ExecuteOnUiThreadAsync(() =>
{
Overlay.Show(I18N.Translate("TextCopied"));
}
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))
@@ -411,11 +509,6 @@ namespace FluentTerminal.App.ViewModels
return Initialized && !_disposalRequested;
}
private async Task CloseTab()
{
await TryClose().ConfigureAwait(false);
}
private void CloseLeftTabs()
{
CloseLeftTabsRequested?.Invoke(this, EventArgs.Empty);
@@ -433,19 +526,19 @@ namespace FluentTerminal.App.ViewModels
private void FindNext()
{
FindNextRequested?.Invoke(this, SearchText);
FindNextRequested?.Invoke(this, new SearchRequest { MatchCase = SearchMatchCase, Regex = SearchWithRegex, Term = SearchText, WholeWord = SearchWholeWord });
}
public ITerminalView TerminalView { get; set; }
public async Task<string> SerializeXtermState()
private Task<string> SerializeXtermStateAsync()
{
return await TerminalView?.SerializeXtermState();
return TerminalView?.SerializeXtermStateAsync() ?? Task.FromResult(string.Empty);
}
private void FindPrevious()
{
FindPreviousRequested?.Invoke(this, SearchText);
FindPreviousRequested?.Invoke(this, new SearchRequest { MatchCase = SearchMatchCase, Regex = SearchWithRegex, Term = SearchText, WholeWord = SearchWholeWord });
}
private void DuplicateTab()
@@ -453,38 +546,61 @@ namespace FluentTerminal.App.ViewModels
DuplicateTabRequested?.Invoke(this, EventArgs.Empty);
}
private async void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
public async Task ReconnectTabAsync()
{
await ApplicationView.RunOnDispatcherThread(() =>
HasExitedWithError = false;
await TerminalView?.ReconnectAsync();
}
private void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
{
ApplicationSettings = message.ApplicationSettings;
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
ApplicationSettings = message.ApplicationSettings;
RaisePropertyChanged(nameof(IsUnderlined));
RaisePropertyChanged(nameof(BackgroundTabTheme));
});
}
private async void OnCurrentThemeChanged(CurrentThemeChangedMessage message)
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(message.ThemeId);
TerminalTheme = currentTheme;
ThemeChanged?.Invoke(this, currentTheme);
}
});
});
}
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
{
_terminalOptions = message.TerminalOptions;
ApplicationView.ExecuteOnUiThreadAsync(() => RaisePropertyChanged(nameof(BackgroundOpacity)), CoreDispatcherPriority.Low);
}
private void SelectTabTheme(string id)
private void OnKeyBindingsChanged(KeyBindingsChangedMessage message)
{
TabTheme = TabThemes.FirstOrDefault(t => t.Id == int.Parse(id));
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
var contextMenu = BuidContextMenu();
if (!contextMenu.EquivalentTo(ContextMenu))
{
ContextMenu = contextMenu;
}
var tabContextMenu = BuildTabContextMenu();
if (!tabContextMenu.EquivalentTo(TabContextMenu))
{
TabContextMenu = tabContextMenu;
}
}, CoreDispatcherPriority.Low, true);
}
private void Terminal_Exited(object sender, int exitCode)
@@ -494,13 +610,12 @@ namespace FluentTerminal.App.ViewModels
tracker.SetInvalid();
}
ApplicationView.RunOnDispatcherThread(() => HasExitedWithError = exitCode > 0);
ApplicationView.ExecuteOnUiThreadAsync(() => HasExitedWithError = exitCode > 0);
}
private void Terminal_Closed(object sender, EventArgs e)
{
ApplicationView.RunOnDispatcherThread(() => Closed?.Invoke(this, EventArgs.Empty));
Closed?.Invoke(this, EventArgs.Empty);
Terminal.KeyboardCommandReceived -= Terminal_KeyboardCommandReceived;
Terminal.OutputReceived -= Terminal_OutputReceived;
Terminal.SizeChanged -= Terminal_SizeChanged;
@@ -515,43 +630,41 @@ namespace FluentTerminal.App.ViewModels
{
case nameof(Command.Copy):
{
var selection = await Terminal.GetSelectedText().ConfigureAwait(true);
CopyText(selection);
break;
await Copy().ConfigureAwait(false);
return;
}
case nameof(Command.Paste):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
if (content != null)
{
content = ShellProfile.TranslateLineEndings(content);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
}
break;
await Paste().ConfigureAwait(false);
return;
}
case nameof(Command.PasteWithoutNewlines):
{
string content = await ClipboardService.GetText().ConfigureAwait(true);
string content = await ClipboardService.GetTextAsync().ConfigureAwait(false);
if (content != null)
{
content = ShellProfile.NewlinePattern.Replace(content, string.Empty);
await Terminal.Write(Encoding.UTF8.GetBytes(content)).ConfigureAwait(true);
TerminalView.Paste(content);
}
break;
return;
}
case nameof(Command.Search):
{
ShowSearchPanel = !ShowSearchPanel;
if (ShowSearchPanel)
await ApplicationView.ExecuteOnUiThreadAsync(() =>
{
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;
}
}
}
@@ -565,39 +678,152 @@ namespace FluentTerminal.App.ViewModels
if (!IsSelected && ApplicationSettings.ShowNewOutputIndicator)
{
ApplicationView.RunOnDispatcherThread(() => HasNewOutput = true);
ApplicationView.ExecuteOnUiThreadAsync(() => HasNewOutput = true);
}
}
private void Terminal_SizeChanged(object sender, TerminalSize e)
{
Overlay.Show($"{e.Columns} x {e.Rows}");
ApplicationView.ExecuteOnUiThreadAsync(() => Overlay.Show($"{e.Columns} x {e.Rows}"));
}
private void Terminal_TitleChanged(object sender, string e)
{
ShellTitle = e;
ShellTitleChanged?.Invoke(this, e);
ApplicationView.ExecuteOnUiThreadAsync(() => ShellTitle = e);
}
private async Task TryClose()
private async Task TryCloseAsync()
{
if (ShellProfile?.Tag is ISessionSuccessTracker tracker)
{
tracker.SetInvalid();
}
if (ApplicationSettings.ConfirmClosingTabs)
if (!ApplicationSettings.ConfirmClosingTabs || await DialogService
.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
string.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK,
DialogButton.Cancel).ConfigureAwait(false) == DialogButton.OK)
{
var result = await DialogService.ShowMessageDialogAsnyc(I18N.Translate("PleaseConfirm"), String.Format(I18N.Translate("ConfirmCloseTab"), ShellTitle), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
await CloseAsync().ConfigureAwait(false);
}
}
if (result == DialogButton.Cancel)
{
return;
}
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());
}
await Close().ConfigureAwait(true);
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.{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);
}
}
}
}
}
+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));
}
}
@@ -1,7 +1,5 @@
using FluentTerminal.App.Services;
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
@@ -27,7 +25,7 @@ namespace FluentTerminal.App.Adapters
public async Task<ValueSet> SendMessageAsync(ValueSet message)
{
var response = await _appServiceConnection.SendMessageAsync(message).AsTask();
var response = await _appServiceConnection.SendMessageAsync(message);
if (response.Status == AppServiceResponseStatus.Success)
{
@@ -7,6 +7,7 @@ using Windows.Foundation.Metadata;
using Windows.UI.Core;
using Windows.UI.Core.Preview;
using Windows.UI.ViewManagement;
using FluentTerminal.App.Utilities;
namespace FluentTerminal.App.Adapters
{
@@ -24,7 +25,7 @@ namespace FluentTerminal.App.Adapters
_applicationView = ApplicationView.GetForCurrentView();
_applicationView.Consolidated += _applicationView_Consolidated;
_dispatcher = CoreApplication.GetCurrentView().Dispatcher;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequest;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested += OnCloseRequested;
Logger.Instance.Debug("Created ApplicationViewAdapter for ApplicationView with Id: {Id}", _applicationView.Id);
}
@@ -48,27 +49,21 @@ namespace FluentTerminal.App.Adapters
return ApiInformation.IsApiContractPresent(api, version);
}
public Task RunOnDispatcherThread(Action action, bool enforceNewSchedule = true)
{
if (!enforceNewSchedule && _dispatcher.HasThreadAccess)
{
action();
public Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false) => _dispatcher.ExecuteAsync(action, priority, enforceNewSchedule);
return Task.CompletedTask;
}
public Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false) => _dispatcher.ExecuteAsync(func, priority, enforceNewSchedule);
return _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
}
public async Task<bool> TryClose()
public async Task<bool> TryCloseAsync()
{
if (_closed)
{
Logger.Instance.Debug("ApplicationViewAdapter.TryClose was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
Logger.Instance.Debug("ApplicationViewAdapter.TryCloseAsync was called, but was already closed. ApplicationView.Id: {Id}", _applicationView.Id);
return true;
}
Logger.Instance.Debug("TryClose ApplicationView with Id: {Id}", _applicationView.Id);
_closed = await _applicationView.TryConsolidateAsync().AsTask();
Logger.Instance.Debug("TryCloseAsync ApplicationView with Id: {Id}", _applicationView.Id);
_closed = await _applicationView.TryConsolidateAsync();
return _closed;
}
@@ -79,28 +74,27 @@ namespace FluentTerminal.App.Adapters
_applicationView.ExitFullScreenMode();
return true;
}
else
{
return _applicationView.TryEnterFullScreenMode();
}
return _applicationView.TryEnterFullScreenMode();
}
private async void OnCloseRequest(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
private async void OnCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
{
var deferral = e.GetDeferral();
var args = new CancelableEventArgs();
if (CloseRequested != null)
if (CloseRequested is { } closeRequestedHandler)
{
await CloseRequested.Invoke(this, args);
// ConfigureAwait(true) because SystemNavigationManagerPreview.GetForCurrentView().CloseRequested requires the calling (UI) thread
await closeRequestedHandler(this, args).ConfigureAwait(true);
}
e.Handled = args.Cancelled;
if (!e.Handled)
{
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequest;
SystemNavigationManagerPreview.GetForCurrentView().CloseRequested -= OnCloseRequested;
}
deferral.Complete();
@@ -1,43 +0,0 @@
using FluentTerminal.App.Services;
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.Adapters
{
public class DispatcherTimerAdapter : IDispatcherTimer
{
private readonly DispatcherTimer _dispatcherTimer;
public DispatcherTimerAdapter()
{
_dispatcherTimer = new DispatcherTimer();
}
private void OnDispatcherTimerTick(object sender, object e)
{
Tick?.Invoke(this, EventArgs.Empty);
}
public TimeSpan Interval
{
get => _dispatcherTimer.Interval;
set => _dispatcherTimer.Interval = value;
}
public bool IsEnabled => _dispatcherTimer.IsEnabled;
public event EventHandler<object> Tick;
public void Start()
{
_dispatcherTimer.Tick += OnDispatcherTimerTick;
_dispatcherTimer.Start();
}
public void Stop()
{
_dispatcherTimer.Stop();
_dispatcherTimer.Tick -= OnDispatcherTimerTick;
}
}
}
@@ -38,11 +38,10 @@ namespace FluentTerminal.App.Adapters
_messageDialog.Commands.Add(command);
}
public async Task<DialogButton> ShowAsync()
public Task<DialogButton> ShowAsync()
{
var result = await _messageDialog.ShowAsync();
return (DialogButton)result.Id;
return _messageDialog.ShowAsync().AsTask().ContinueWith(t => (DialogButton) t.Result.Id,
TaskContinuationOptions.OnlyOnRanToCompletion);
}
}
}
+2
View File
@@ -41,6 +41,8 @@
<converters:ColorResourceKeyFallbackConverter x:Key="ColorResourceKeyFallbackConverter" />
<converters:TrueToBoldConverter x:Key="TrueToBoldConverter" />
<converters:I18NConverter x:Key="I18NConverter" />
<converters:NullToCollapsedConverter x:Key="NullToCollapsedConverter" />
<converters:MenuViewModelToFlyoutMenuConverter x:Key="MenuViewModelToFlyoutMenuConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>
+189 -223
View File
@@ -27,7 +27,6 @@ using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.ApplicationModel.Core;
using Windows.Storage;
using Windows.UI.Core;
using Windows.UI.Popups;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
@@ -68,6 +67,9 @@ namespace FluentTerminal.App
UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += (sender, e) =>
Logger.Instance.Error(e.Exception, "Unobserved Task Exception");
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = new ApplicationDataContainerAdapter(ApplicationData.Current.LocalSettings),
@@ -78,8 +80,11 @@ namespace FluentTerminal.App
SshProfiles = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.SshProfilesContainerName, ApplicationDataCreateDisposition.Always)),
HistoryContainer = new ApplicationDataContainerAdapter(ApplicationData.Current.RoamingSettings.CreateContainer(Constants.ExecutedCommandsContainerName, ApplicationDataCreateDisposition.Always))
};
var builder = new ContainerBuilder();
builder.RegisterInstance(applicationDataContainers);
builder.RegisterType<SettingsService>().As<ISettingsService>().SingleInstance();
builder.RegisterType<CommandHistoryService>().As<ICommandHistoryService>().SingleInstance();
builder.RegisterType<DefaultValueProvider>().As<IDefaultValueProvider>().SingleInstance();
builder.RegisterType<TrayProcessCommunicationService>().As<ITrayProcessCommunicationService>().SingleInstance();
builder.RegisterType<DialogService>().As<IDialogService>().SingleInstance();
@@ -93,9 +98,8 @@ namespace FluentTerminal.App
builder.RegisterType<FluentTerminalThemeParser>().As<IThemeParser>().SingleInstance();
builder.RegisterType<ClipboardService>().As<IClipboardService>().SingleInstance();
builder.RegisterType<FileSystemService>().As<IFileSystemService>().SingleInstance();
builder.RegisterType<ImageFileSystemService>().As<IImageFileSystemService>().SingleInstance();
builder.RegisterType<SystemFontService>().As<ISystemFontService>().SingleInstance();
builder.RegisterType<ShellProfileSelectionDialog>().As<IShellProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<SshProfileSelectionDialog>().As<ISshProfileSelectionDialog>().InstancePerDependency();
builder.RegisterType<CreateKeyBindingDialog>().As<ICreateKeyBindingDialog>().InstancePerDependency();
builder.RegisterType<InputDialog>().As<IInputDialog>().InstancePerDependency();
builder.RegisterType<AboutDialog>().As<IAboutDialog>().InstancePerDependency();
@@ -103,11 +107,10 @@ namespace FluentTerminal.App
builder.RegisterType<SshInfoDialog>().As<ISshConnectionInfoDialog>().InstancePerDependency();
builder.RegisterType<CustomCommandDialog>().As<ICustomCommandDialog>().InstancePerDependency();
builder.RegisterType<ApplicationViewAdapter>().As<IApplicationView>().InstancePerDependency();
builder.RegisterType<DispatcherTimerAdapter>().As<IDispatcherTimer>().InstancePerDependency();
builder.RegisterType<StartupTaskService>().As<IStartupTaskService>().SingleInstance();
builder.RegisterType<ApplicationLanguageService>().As<IApplicationLanguageService>().SingleInstance();
builder.RegisterType<ShellProfileMigrationService>().As<IShellProfileMigrationService>().SingleInstance();
builder.RegisterInstance(applicationDataContainers);
builder.RegisterType<AcceleratorKeyValidator>().As<IAcceleratorKeyValidator>().SingleInstance();
_container = builder.Build();
@@ -187,7 +190,7 @@ namespace FluentTerminal.App
{
if (protocolActivated.Uri == new Uri("ftcmd://fluent.terminal?focus"))
{
await ShowOrCreateWindow(protocolActivated.ViewSwitcher);
await ShowOrCreateWindowAsync(protocolActivated.ViewSwitcher);
return;
}
@@ -207,7 +210,7 @@ namespace FluentTerminal.App
// in order to be able to communicate with user.
mainViewModel = _container.Resolve<MainViewModel>();
await CreateMainView(typeof(MainPage), mainViewModel, true);
await CreateMainViewAsync(typeof(MainPage), mainViewModel, true);
applicationView = mainViewModel.ApplicationView;
}
@@ -225,7 +228,7 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
@@ -247,19 +250,19 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
if (_applicationSettings.AutoFallbackToWindowsUsernameInLinks && string.IsNullOrEmpty(vm.Username))
{
vm.Username = await _trayProcessCommunicationService.GetUserName();
vm.Username = await _trayProcessCommunicationService.GetUserNameAsync();
}
var error = await vm.AcceptChangesAsync(true);
SshProfile profile = (SshProfile) vm.Model;
var profile = (SshProfile) vm.Model;
if (!string.IsNullOrEmpty(error))
{
@@ -269,16 +272,16 @@ namespace FluentTerminal.App
if (profile == null)
{
// User clicked "Cancel" in the dialog.
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
}
if (mainViewModel == null)
await CreateTerminal(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
await CreateTerminalAsync(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
else
await mainViewModel.AddTerminalAsync(profile);
await mainViewModel.AddTabAsync(profile);
return;
}
@@ -291,7 +294,7 @@ namespace FluentTerminal.App
{
vm = CommandProfileProviderViewModel.ParseUri(protocolActivated.Uri, _settingsService,
applicationView, _trayProcessCommunicationService,
_container.Resolve<ApplicationDataContainers>().HistoryContainer);
_container.Resolve<ICommandHistoryService>());
}
catch (Exception ex)
{
@@ -300,7 +303,7 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
@@ -317,7 +320,7 @@ namespace FluentTerminal.App
if (profile == null)
{
// User clicked "Cancel" in the dialog.
mainViewModel?.ApplicationView.TryClose();
mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
@@ -325,11 +328,11 @@ namespace FluentTerminal.App
if (mainViewModel == null)
{
await CreateTerminal(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
await CreateTerminalAsync(profile, _applicationSettings.NewTerminalLocation, protocolActivated.ViewSwitcher);
}
else
{
await mainViewModel.AddTerminalAsync(profile);
await mainViewModel.AddTabAsync(profile);
}
return;
}
@@ -339,7 +342,8 @@ namespace FluentTerminal.App
"Invalid Link")
.ShowAsync();
mainViewModel?.ApplicationView.TryClose();
// ReSharper disable once AssignmentIsFullyDiscarded
_ = mainViewModel?.ApplicationView.TryCloseAsync();
return;
}
@@ -352,103 +356,109 @@ namespace FluentTerminal.App
arguments = "new";
}
_commandLineParser.ParseArguments(SplitArguments(arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb)).WithParsed(async verb =>
{
if (verb is SettingsVerb settingsVerb)
{
if (!settingsVerb.Import && !settingsVerb.Export)
{
await ShowSettings().ConfigureAwait(true);
}
else if (settingsVerb.Export)
{
var exportFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var settings = _settingsService.ExportSettings();
await FileIO.WriteTextAsync(exportFile, settings);
await new MessageDialog($"{I18N.Translate("SettingsExported")} {exportFile.Path}").ShowAsync();
}
else if (settingsVerb.Import)
{
var file = await ApplicationData.Current.LocalFolder.GetFileAsync("config.json");
var content = await FileIO.ReadTextAsync(file);
_settingsService.ImportSettings(content);
await new MessageDialog($"{I18N.Translate("SettingsImported")} {file.Path}").ShowAsync();
}
}
else if (verb is NewVerb newVerb)
{
var profile = default(ShellProfile);
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
{
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
}
if (profile == null)
{
profile = _settingsService.GetDefaultShellProfile();
}
if (!string.IsNullOrWhiteSpace(newVerb.Directory) && newVerb.Directory != ".")
{
profile.WorkingDirectory = newVerb.Directory;
}
else
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
else if (verb is RunVerb runVerb)
{
var profile = new ShellProfile
{
Id = Guid.Empty,
Location = null,
Arguments = runVerb.Command,
WorkingDirectory = runVerb.Directory
};
if (!string.IsNullOrWhiteSpace(runVerb.Theme))
{
var theme = _settingsService.GetThemes().FirstOrDefault(x => x.Name.Equals(runVerb.Theme, StringComparison.CurrentCultureIgnoreCase));
if (theme != null)
{
profile.TerminalThemeId = theme.Id;
}
}
if (string.IsNullOrWhiteSpace(profile.WorkingDirectory))
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = runVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: runVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminal(profile, location).ConfigureAwait(true);
}
});
_commandLineParser
.ParseArguments(SplitArguments(arguments), typeof(NewVerb), typeof(RunVerb), typeof(SettingsVerb))
.WithParsed(async verb => await CliRunAsync(verb, commandLineActivated));
}
}
private async Task ShowOrCreateWindow(ActivationViewSwitcher viewSwitcher)
private async Task CliRunAsync(object verb, CommandLineActivatedEventArgs commandLineActivated)
{
if (_mainViewModels.Count == 0)
if (verb is SettingsVerb settingsVerb)
{
await CreateTerminal(_settingsService.GetDefaultShellProfile(), NewTerminalLocation.Tab, viewSwitcher);
if (!settingsVerb.Import && !settingsVerb.Export)
{
await ShowSettingsAsync();
}
else if (settingsVerb.Export)
{
var exportFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var settings = _settingsService.ExportSettings();
await FileIO.WriteTextAsync(exportFile, settings);
await new MessageDialog($"{I18N.Translate("SettingsExported")} {exportFile.Path}").ShowAsync();
}
else if (settingsVerb.Import)
{
var file = await ApplicationData.Current.LocalFolder.GetFileAsync("config.json");
var content = await FileIO.ReadTextAsync(file);
_settingsService.ImportSettings(content);
await new MessageDialog($"{I18N.Translate("SettingsImported")} {file.Path}").ShowAsync();
}
}
else
else if (verb is NewVerb newVerb)
{
var viewModel = _mainViewModels.Find(o => o.ApplicationView.Id == _activeWindowId) ?? _mainViewModels.Last();
await ShowAsStandaloneAsync(viewModel, viewSwitcher);
var profile = default(ShellProfile);
if (!string.IsNullOrWhiteSpace(newVerb.Profile))
{
profile = _settingsService.GetShellProfiles().FirstOrDefault(x => x.Name.Equals(newVerb.Profile, StringComparison.CurrentCultureIgnoreCase));
}
if (profile == null)
{
profile = _settingsService.GetDefaultShellProfile();
}
if (!string.IsNullOrWhiteSpace(newVerb.Directory) && newVerb.Directory != ".")
{
profile.WorkingDirectory = newVerb.Directory;
}
else
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = newVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: newVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminalAsync(profile, location);
}
else if (verb is RunVerb runVerb)
{
var profile = new ShellProfile
{
Id = Guid.Empty,
Location = null,
Arguments = runVerb.Command,
WorkingDirectory = runVerb.Directory
};
if (!string.IsNullOrEmpty(runVerb.Buffer) && bool.TryParse(runVerb.Buffer, out var useBuffer))
{
profile.UseBuffer = useBuffer;
}
if (!string.IsNullOrWhiteSpace(runVerb.Theme))
{
var theme = _settingsService.GetThemes().FirstOrDefault(x => x.Name.Equals(runVerb.Theme, StringComparison.CurrentCultureIgnoreCase));
if (theme != null)
{
profile.TerminalThemeId = theme.Id;
}
}
if (string.IsNullOrWhiteSpace(profile.WorkingDirectory))
{
profile.WorkingDirectory = commandLineActivated.Operation.CurrentDirectoryPath;
}
var location = runVerb.Target == Target.Default ? _applicationSettings.NewTerminalLocation
: runVerb.Target == Target.Tab ? NewTerminalLocation.Tab
: NewTerminalLocation.Window;
await CreateTerminalAsync(profile, location);
}
}
private Task ShowOrCreateWindowAsync(ActivationViewSwitcher viewSwitcher)
{
var viewModel = _mainViewModels.Find(o => o.ApplicationView.Id == _activeWindowId) ??
_mainViewModels.LastOrDefault();
return viewModel == null
? CreateTerminalAsync(_settingsService.GetDefaultShellProfile(), NewTerminalLocation.Tab, viewSwitcher)
: ShowAsStandaloneAsync(viewModel, viewSwitcher);
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
@@ -459,48 +469,49 @@ namespace FluentTerminal.App
}
_isLaunching = true;
if (!_alreadyLaunched)
{
await InitializeLogger();
await InitializeLoggerAsync();
// TODO: Check the reason for such strange using of tasks!
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
// ReSharper disable once AssignmentIsFullyDiscarded
_ = JumpListHelper.UpdateAsync(_settingsService);
var viewModel = _container.Resolve<MainViewModel>();
if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
await viewModel.AddLocalTabOrWindowAsync(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
await viewModel.AddProfileByGuidAsync(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
}
else
{
await viewModel.AddLocalTabAsync();
await viewModel.AddDefaultProfileAsync(NewTerminalLocation.Tab);
}
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
await CreateMainViewAsync(typeof(MainPage), viewModel, true);
Window.Current.Activate();
}
else if (args.Arguments.StartsWith(JumpListHelper.ShellProfileFlag))
{
var location = _applicationSettings.NewTerminalLocation;
var profile = _settingsService.GetShellProfile(Guid.Parse(args.Arguments.Replace(JumpListHelper.ShellProfileFlag, string.Empty)));
await CreateTerminal(profile, location, args.ViewSwitcher).ConfigureAwait(true);
await CreateTerminalAsync(profile, location, args.ViewSwitcher);
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await viewModel.AddLocalTabAsync();
var viewModel = await CreateNewTerminalWindowAsync();
await viewModel.AddDefaultProfileAsync(NewTerminalLocation.Tab);
await ShowAsStandaloneAsync(viewModel, args.ViewSwitcher);
}
_isLaunching = false;
}
private static async Task InitializeLogger()
private static async Task InitializeLoggerAsync()
{
var logDirectory = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("Logs", CreationCollisionOption.OpenIfExists);
var logFile = Path.Combine(logDirectory.Path, "fluentterminal.app.log");
var configFile = await logDirectory.CreateFileAsync("config.json", CreationCollisionOption.OpenIfExists);
var configContent = await FileIO.ReadTextAsync(configFile);
if (string.IsNullOrWhiteSpace(configContent))
{
configContent = JsonConvert.SerializeObject(new Logger.Configuration());
@@ -525,7 +536,7 @@ namespace FluentTerminal.App
_appServiceConnection = new AppServiceConnectionAdapter(details.AppServiceConnection);
_trayReady.SetResult(0);
_trayReady.TrySetResult(0);
}
}
}
@@ -540,11 +551,11 @@ namespace FluentTerminal.App
Application.Current.Exit();
}
private async Task CreateMainView(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
private async Task CreateMainViewAsync(Type pageType, INotifyPropertyChanged viewModel, bool extendViewIntoTitleBar)
{
ApplicationViewSwitcher.DisableSystemViewActivationPolicy();
await StartSystemTray().ConfigureAwait(true);
await StartSystemTray();
if (!(Window.Current.Content is Frame rootFrame))
{
@@ -572,9 +583,9 @@ namespace FluentTerminal.App
Window.Current.Activate();
}
private async Task<MainViewModel> CreateNewTerminalWindow()
private async Task<MainViewModel> CreateNewTerminalWindowAsync()
{
var viewModel = await CreateSecondaryView<MainViewModel>(typeof(MainPage), true).ConfigureAwait(true);
var viewModel = await CreateSecondaryViewAsync<MainViewModel>(typeof(MainPage), true);
viewModel.Closed += OnMainViewModelClosed;
viewModel.NewWindowRequested += OnNewWindowRequested;
viewModel.ShowSettingsRequested += OnShowSettingsRequested;
@@ -585,22 +596,24 @@ namespace FluentTerminal.App
return viewModel;
}
private async Task<TViewModel> CreateSecondaryView<TViewModel>(Type pageType, bool extendViewIntoTitleBar)
private async Task<TViewModel> CreateSecondaryViewAsync<TViewModel>(Type pageType, bool extendViewIntoTitleBar)
{
int windowId = 0;
var windowId = 0;
TViewModel viewModel = default;
await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
var newView = CoreApplication.CreateNewView();
// ConfigureAwait(true) because ApplicationViewSwitcher.TryShowAsStandaloneAsync requires the same thread as CoreApplication.CreateNewView
await newView.Dispatcher.ExecuteAsync(() =>
{
viewModel = _container.Resolve<TViewModel>();
CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
newView.TitleBar.ExtendViewIntoTitleBar = extendViewIntoTitleBar;
var frame = new Frame();
frame.Navigate(pageType, viewModel);
Window.Current.Content = frame;
Window.Current.Activate();
windowId = ApplicationView.GetForCurrentView().Id;
});
}).ConfigureAwait(true);
if (viewModel is SettingsViewModel settingsViewModel)
{
@@ -638,7 +651,15 @@ namespace FluentTerminal.App
_mainViewModels.Remove(viewModel);
Window.Current.Content = null;
try
{
// try/catch because the method cannot be called on the last window
Window.Current.Close();
}
catch
{
// ignored
}
}
}
@@ -655,149 +676,94 @@ namespace FluentTerminal.App
{
Logger.Instance.Debug("App.xaml.cs on tab tear off");
var newViewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await newViewModel.AddTerminalAsync(await model.Serialize(), 0);
var newViewModel = await CreateNewTerminalWindowAsync();
await newViewModel.AddTabAsync(await model.SerializeAsync(), 0);
}
private async void OnNewWindowRequested(object sender, NewWindowRequestedEventArgs e)
{
ShellProfile profile = null;
switch (e.Action)
{
case NewWindowAction.StartDefaultLocalTerminal:
break;
case NewWindowAction.ShowProfileSelection:
profile = await _dialogService.ShowProfileSelectionDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshProfileSelection:
profile = await _dialogService.ShowSshProfileSelectionDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowCustomCommandDialog:
profile = await _dialogService.ShowCustomCommandDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.ShowSshInfoDialog:
profile = await _dialogService.ShowSshConnectionInfoDialogAsync();
if (profile == null)
{
// Nothing to do if user cancels.
return;
}
break;
case NewWindowAction.StartLocalTerminal:
profile = _settingsService.GetShellProfile(e.ProfileId);
break;
case NewWindowAction.StartSshTerminal:
profile = _settingsService.GetSshProfile(e.ProfileId);
break;
default:
throw new NotImplementedException("this ProfileSelection not implemented yet");
}
var viewModel = await CreateNewTerminalWindowAsync().ConfigureAwait(false);
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
if (profile == null)
{
await viewModel.AddLocalTabAsync();
}
else
{
await viewModel.AddTerminalAsync(profile);
}
await viewModel.AddTabAsync(e.Profile).ConfigureAwait(false);
}
private void OnSettingsClosed(object sender, EventArgs e)
{
Task.Run(async () => await JumpListHelper.Update(_settingsService.GetShellProfiles()));
// ReSharper disable once AssignmentIsFullyDiscarded
_ = JumpListHelper.UpdateAsync(_settingsService);
_settingsViewModel.Closed -= OnSettingsClosed;
_settingsViewModel = null;
_settingsWindowId = null;
}
private async void OnShowSettingsRequested(object sender, EventArgs e)
private void OnShowSettingsRequested(object sender, EventArgs e)
{
await ShowSettings().ConfigureAwait(true);
// ReSharper disable once AssignmentIsFullyDiscarded
_ = ShowSettingsAsync();
}
private async Task ShowAsStandaloneAsync(MainViewModel viewModel, ActivationViewSwitcher viewSwitcher = null)
private Task ShowAsStandaloneAsync(MainViewModel viewModel, ActivationViewSwitcher viewSwitcher = null)
{
int viewId = viewModel.ApplicationView.Id;
var viewId = viewModel.ApplicationView.Id;
if (viewSwitcher != null)
{
await viewModel.ApplicationView.RunOnDispatcherThread(async () => await viewSwitcher.ShowAsStandaloneAsync(viewId));
}
else
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId);
return viewModel.ApplicationView.ExecuteOnUiThreadAsync(async () => await viewSwitcher.ShowAsStandaloneAsync(viewId));
}
return ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId).AsTask();
}
private async Task CreateTerminal(ShellProfile profile, NewTerminalLocation location, ActivationViewSwitcher viewSwitcher = null)
private async Task CreateTerminalAsync(ShellProfile profile, NewTerminalLocation location, ActivationViewSwitcher viewSwitcher = null)
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<MainViewModel>();
await viewModel.AddTerminalAsync(profile);
await CreateMainView(typeof(MainPage), viewModel, true).ConfigureAwait(true);
await viewModel.AddTabAsync(profile);
await CreateMainViewAsync(typeof(MainPage), viewModel, true);
}
else if (location == NewTerminalLocation.Tab && _mainViewModels.Count > 0)
{
var item = _mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId) ??
_mainViewModels.Last();
MainViewModel item = _mainViewModels.FirstOrDefault(o => o.ApplicationView.Id == _activeWindowId);
if (item == null)
{
item = _mainViewModels.Last();
}
await item.AddTerminalAsync(profile);
await item.AddTabAsync(profile);
await ShowAsStandaloneAsync(item, viewSwitcher);
}
else
{
var viewModel = await CreateNewTerminalWindow().ConfigureAwait(true);
await viewModel.AddTerminalAsync(profile);
var viewModel = await CreateNewTerminalWindowAsync();
await viewModel.AddTabAsync(profile);
await ShowAsStandaloneAsync(viewModel, viewSwitcher);
}
}
private async Task ShowSettings()
private Task ShowSettingsAsync()
{
if (!_alreadyLaunched)
{
var viewModel = _container.Resolve<SettingsViewModel>();
await CreateMainView(typeof(SettingsPage), viewModel, true).ConfigureAwait(true);
return CreateMainViewAsync(typeof(SettingsPage), _container.Resolve<SettingsViewModel>(), true);
}
else if (_settingsViewModel == null)
if (_settingsViewModel == null)
{
await CreateSecondaryView<SettingsViewModel>(typeof(SettingsPage), true).ConfigureAwait(true);
return CreateSecondaryViewAsync<SettingsViewModel>(typeof(SettingsPage), true);
}
else
if (_settingsWindowId.HasValue)
{
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value);
return ApplicationViewSwitcher.TryShowAsStandaloneAsync(_settingsWindowId.Value).AsTask();
}
return Task.CompletedTask;
}
private async Task StartSystemTray()
private Task StartSystemTray()
{
var launch = FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("AppLaunchedParameterGroup").AsTask();
await Task.WhenAll(launch, _trayReady.Task).ConfigureAwait(true);
_trayProcessCommunicationService.Initialize(_appServiceConnection);
return Task.WhenAll(launch, _trayReady.Task).ContinueWith(
t => _trayProcessCommunicationService.Initialize(_appServiceConnection),
TaskContinuationOptions.OnlyOnRanToCompletion);
}
}
}
@@ -14,6 +14,9 @@ namespace FluentTerminal.App.CommandLineArguments
[Option("theme")]
public string Theme { get; set; }
[Option("buffer")]
public string Buffer { get; set; }
[Option("target")]
public Target Target { get; set; }
}
@@ -0,0 +1,43 @@
using System;
using Windows.UI.Xaml.Data;
// ReSharper disable LocalizableElement
namespace FluentTerminal.App.Converters
{
public class BooleanNegationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (targetType == typeof(bool))
{
if (value is bool isValue)
{
return !isValue;
}
throw new ArgumentException($"{nameof(value)} argument has to be of type bool.", nameof(value));
}
if (targetType == typeof(bool?))
{
if (value == null)
{
return null;
}
if (value is bool isValue)
{
return !isValue;
}
throw new ArgumentException($"{nameof(value)} argument has to be of type bool or Nullable<bool>.", nameof(value));
}
throw new ArgumentException($"{nameof(targetType)} argument has to {typeof(bool)}.",
nameof(targetType));
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
Convert(value, targetType, parameter, language);
}
}
@@ -0,0 +1,33 @@
using FluentTerminal.App.ViewModels.Menu;
using Microsoft.Toolkit.Uwp.Helpers;
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;
// ReSharper disable LocalizableElement
namespace FluentTerminal.App.Converters
{
public class IconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Mdl2Icon mdl2Icon)
{
var fontIcon = new FontIcon { FontFamily = new FontFamily("Segoe MDL2 Assets"), Glyph = mdl2Icon.Glyph };
if(!string.IsNullOrWhiteSpace(mdl2Icon.Color))
{
fontIcon.Foreground = new SolidColorBrush(mdl2Icon.Color.ToColor());
}
return fontIcon;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
}
}
@@ -0,0 +1,243 @@
using System;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using FluentTerminal.App.ViewModels.Menu;
using FluentTerminal.App.Views;
using Microsoft.UI.Xaml.Controls;
// ReSharper disable LocalizableElement
namespace FluentTerminal.App.Converters
{
public class MenuItemViewModelBaseToMenuFlayoutItemBaseConverter : IValueConverter
{
#region Static
private static readonly IconConverter IconConverter = new IconConverter();
private static MenuFlyoutItemBase GetItem(MenuItemViewModelBase viewModel)
{
if (viewModel is ExpandableMenuItemViewModel expandable)
{
return GetExpandableItem(expandable);
}
if (viewModel is SeparatorMenuItemViewModel)
{
return GetSeparatorItem();
}
if (viewModel is MenuItemViewModel regular)
{
return GetRegularItem(regular);
}
if (viewModel is ToggleMenuItemViewModel toggle)
{
return GetToggleItem(toggle);
}
if (viewModel is RadioMenuItemViewModel radio)
{
return GetRadioItem(radio);
}
// Won't happen ever, but still...
throw new NotImplementedException(
$"Unexpected {nameof(MenuItemViewModelBase)} type: {viewModel.GetType()}");
}
private static MenuFlyoutSeparator GetSeparatorItem()
{
return new MenuFlyoutSeparator();
}
private static MenuFlyoutItem GetRegularItem(MenuItemViewModel viewModel)
{
var item = new MenuFlyoutItem();
item.SetBinding(MenuFlyoutItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.CommandProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModel.Command)),
Mode = BindingMode.OneTime
});
if (viewModel.KeyBinding != null)
{
item.KeyboardAccelerators?.Add(new KeyboardAccelerator
{
Key = (VirtualKey) viewModel.KeyBinding.Key,
Modifiers = (VirtualKeyModifiers) viewModel.KeyBinding.KeyModifiers,
IsEnabled = true
});
}
return item;
}
private static MenuFlyoutSubItem GetExpandableItem(ExpandableMenuItemViewModel viewModel)
{
var item = new MenuFlyoutSubItem();
item.SetBinding(MenuFlyoutSubItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutSubItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(MenuExtension.SubItemsProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(ExpandableMenuItemViewModel.SubItems)),
Mode = BindingMode.OneWay
});
return item;
}
private static ToggleMenuFlyoutItem GetToggleItem(ToggleMenuItemViewModel viewModel)
{
var item = new ToggleMenuFlyoutItem();
item.SetBinding(MenuFlyoutItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(ToggleMenuFlyoutItem.IsCheckedProperty, new Binding
{
Source = viewModel.BindingSource,
Path = new PropertyPath(viewModel.BindingPath),
Mode = BindingMode.TwoWay
});
return item;
}
private static RadioMenuFlyoutItem GetRadioItem(RadioMenuItemViewModel viewModel)
{
var item = new RadioMenuFlyoutItem();
item.SetBinding(MenuFlyoutItem.TextProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Text)),
Mode = BindingMode.OneWay
});
item.SetBinding(ToolTipService.ToolTipProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Description)),
Mode = BindingMode.OneWay
});
item.SetBinding(MenuFlyoutItem.IconProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(MenuItemViewModelBase.Icon)),
Converter = IconConverter,
Mode = BindingMode.OneWay
});
item.SetBinding(RadioMenuFlyoutItem.GroupNameProperty, new Binding
{
Source = viewModel,
Path = new PropertyPath(nameof(RadioMenuItemViewModel.GroupName)),
Mode = BindingMode.OneWay
});
item.SetBinding(RadioMenuFlyoutItem.IsCheckedProperty, new Binding
{
Source = viewModel.BindingSource,
Path = new PropertyPath(viewModel.BindingPath),
Mode = BindingMode.TwoWay
});
return item;
}
#endregion Static
#region IValueConverter
// ReSharper disable once AssignNullToNotNullAttribute
public object Convert(object value, Type targetType = null, object parameter = null, string language = null)
{
if (value == null)
{
return null;
}
return value is MenuItemViewModelBase menuItemViewModel
? GetItem(menuItemViewModel)
: throw new ArgumentException(
$"Invalid {nameof(value)} argument type: {value.GetType()}. {typeof(MenuItemViewModelBase)} expected.",
nameof(value));
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
#endregion IValueConverter
}
}
@@ -0,0 +1,44 @@
using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using FluentTerminal.App.ViewModels.Menu;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.Foundation.Metadata;
namespace FluentTerminal.App.Converters
{
public class MenuViewModelToFlyoutMenuConverter : IValueConverter
{
private static readonly MenuItemViewModelBaseToMenuFlayoutItemBaseConverter ItemConverter =
new MenuItemViewModelBaseToMenuFlayoutItemBaseConverter();
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is null)
{
return null;
}
if (!(value is MenuViewModel menuViewModel))
{
throw new ArgumentException(
$"Invalid {nameof(value)} argument type: {value.GetType()}. {typeof(MenuViewModel)} expected.");
}
var menuFlyout = new MenuFlyout
{
Placement = FlyoutPlacementMode.Bottom
};
foreach (var menuItemViewModelBase in menuViewModel.Items)
{
menuFlyout.Items?.Add((MenuFlyoutItemBase) ItemConverter.Convert(menuItemViewModelBase));
}
return menuFlyout;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotSupportedException();
}
}
@@ -0,0 +1,19 @@
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
namespace FluentTerminal.App.Converters
{
public class NullToCollapsedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value == null ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}
@@ -1,4 +1,4 @@
using FluentTerminal.Models;
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models.Enums;
using Microsoft.Toolkit.Uwp.Helpers;
using System;
@@ -12,8 +12,9 @@ namespace FluentTerminal.App.Converters
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is TabTheme theme && Enum.TryParse<TabThemeKey>((string)parameter, true, out TabThemeKey tabThemeKey))
if (value is TabThemeViewModel viewModel && Enum.TryParse<TabThemeKey>((string)parameter, true, out TabThemeKey tabThemeKey))
{
var theme = viewModel.Theme;
switch (tabThemeKey)
{
case TabThemeKey.Background:
@@ -1,4 +1,5 @@
using FluentTerminal.Models;
using FluentTerminal.App.ViewModels;
using FluentTerminal.Models;
using System;
using Windows.UI.Xaml.Data;
@@ -8,9 +9,9 @@ namespace FluentTerminal.App.Converters
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is TabTheme theme && parameter is string idString && int.TryParse(idString, out var id))
if (value is TerminalViewModel terminal && parameter is string idString && int.TryParse(idString, out var id))
{
return theme.Id == id;
return terminal.TabTheme.Theme.Id == id;
}
return null;
}
@@ -9,11 +9,15 @@ using System.Threading.Tasks;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using FluentTerminal.App.Services;
namespace FluentTerminal.App.Dialogs
{
// ReSharper disable once RedundantExtendsListEntry
public sealed partial class CreateKeyBindingDialog : ContentDialog, ICreateKeyBindingDialog, INotifyPropertyChanged
{
private readonly IAcceleratorKeyValidator _acceleratorKeyValidator;
private bool _ctrl;
private bool _shift;
private bool _alt;
@@ -47,11 +51,19 @@ namespace FluentTerminal.App.Dialogs
public int Key
{
get => _key;
set => Set(ref _key, value);
set
{
// We're ignoring invalid keys.
if (_acceleratorKeyValidator.Valid(value))
{
Set(ref _key, value);
}
}
}
public CreateKeyBindingDialog()
public CreateKeyBindingDialog(IAcceleratorKeyValidator acceleratorKeyValidator)
{
_acceleratorKeyValidator = acceleratorKeyValidator;
InitializeComponent();
ResetCommand = new RelayCommand(Reset);
PreviewKeyDown += RegisterKeyBindingDialog_PreviewKeyDown;
@@ -65,23 +77,13 @@ namespace FluentTerminal.App.Dialogs
public event PropertyChangedEventHandler PropertyChanged;
public async Task<KeyBinding> CreateKeyBinding()
public Task<KeyBinding> CreateKeyBinding()
{
var result = await ShowAsync();
if (result == ContentDialogResult.Primary)
{
return new KeyBinding
{
Alt = Alt,
Ctrl = Ctrl,
Key = Key,
Meta = Meta,
Shift = Shift
};
}
return null;
return ShowAsync().AsTask()
.ContinueWith(
t => t.Result == ContentDialogResult.Primary
? new KeyBinding {Alt = Alt, Ctrl = Ctrl, Key = Key, Meta = Meta, Shift = Shift}
: null, TaskContinuationOptions.OnlyOnRanToCompletion);
}
private void OnResetButtonPreviewKeyDown(object sender, Windows.UI.Xaml.Input.KeyRoutedEventArgs e)
@@ -132,6 +134,11 @@ namespace FluentTerminal.App.Dialogs
private void Set<T>(ref T field, T value, [CallerMemberName]string propertyName = "")
{
if (field?.Equals(value) ?? value == null)
{
return;
}
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
@@ -15,6 +15,7 @@
DefaultButton="Primary"
PrimaryButtonClick="OnPrimaryButtonClick"
PrimaryButtonText="Button1"
SecondaryButtonClick="OnSecondaryButtonClick"
SecondaryButtonText="Button2"
mc:Ignorable="d">
<ContentDialog.Resources>
@@ -33,6 +34,7 @@
Margin="4,8,4,0"
AutoMaximizeSuggestionArea="True"
ItemsSource="{x:Bind ViewModel.Commands, Mode=OneWay}"
KeyDown="CommandTextBox_OnKeyDown"
KeyUp="CommandTextBox_OnKeyUp"
PlaceholderText="Enter a command (e.g. 'ssh username@host')"
PreviewKeyUp="CommandTextBox_OnPreviewKeyUp"
@@ -43,7 +45,14 @@
TextChanged="CommandTextBox_OnTextChanged">
<AutoSuggestBox.ItemTemplate>
<DataTemplate x:DataType="viewModels:CommandItemViewModel">
<ContentControl HorizontalAlignment="Stretch" Content="{x:Bind RichTextBlock, Mode=OneWay}" />
<Grid HorizontalAlignment="Stretch">
<ContentControl HorizontalAlignment="Stretch" Content="{x:Bind RichTextBlock, Mode=OneWay}" />
<!-- I've had to create a transparent overlay over RichTextBox in order to make mouse selection work. Weird but true. -->
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent" />
</Grid>
</DataTemplate>
</AutoSuggestBox.ItemTemplate>
@@ -73,39 +82,16 @@
<RowDefinition />
</Grid.RowDefinitions>
<!-- Row -->
<TextBlock
x:Uid="LineEndingModeOnPaste"
Grid.Row="0"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="Line ending on paste:" />
<ComboBox
Grid.Row="0"
Grid.Column="1"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{x:Bind ViewModel.LineEndingStyles}"
SelectedItem="{x:Bind ViewModel.LineEndingTranslation, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="enums:LineEndingStyle">
<TextBlock Text="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- Row -->
<TextBlock
x:Uid="SshInfoDialog_TabColor"
Grid.Row="1"
Grid.Row="0"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="Tab color:" />
<ComboBox
Grid.Row="1"
Grid.Row="0"
Grid.Column="1"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
@@ -116,13 +102,13 @@
<!-- Row -->
<TextBlock
x:Uid="Theme"
Grid.Row="2"
Grid.Row="1"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="Theme:" />
<ComboBox
Grid.Row="2"
Grid.Row="1"
Grid.Column="1"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
@@ -132,13 +118,22 @@
<!-- Row -->
<CheckBox
Grid.Row="3"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
Content="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseConPty}"
IsChecked="{x:Bind ViewModel.UseConPty, Mode=TwoWay}" />
<!-- Row -->
<CheckBox
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0,8,0,0"
Content="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseBuffer}"
IsChecked="{x:Bind ViewModel.UseBuffer, Mode=TwoWay}" />
</Grid>
</controls:Expander>
@@ -12,6 +12,7 @@ using FluentTerminal.App.Utilities;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.Models;
using System.Linq;
using Windows.Foundation;
using Windows.System;
using Windows.UI.Xaml.Input;
using FluentTerminal.App.ViewModels;
@@ -27,21 +28,27 @@ namespace FluentTerminal.App.Dialogs
private readonly ISettingsService _settingsService;
private readonly IApplicationView _applicationView;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly IApplicationDataContainer _historyContainer;
private readonly ICommandHistoryService _historyService;
private ExecutedCommand _lastChosenCommand;
private IAsyncOperation<ContentDialogResult> _showDialogOperation;
private ContentDialogResult _dialogResult = ContentDialogResult.None;
private CommandItemViewModel _lastChosenCommand;
// TODO: The following field is for hacking strange behavior that deletes command text after Tab selection. Consider finding a better fix.
private CommandItemViewModel _tabSelectedCommand;
public CommandProfileProviderViewModel ViewModel { get; private set; }
public IAsyncCommand SaveLinkCommand { get; }
public CustomCommandDialog(ISettingsService settingsService, IApplicationView applicationView,
ITrayProcessCommunicationService trayProcessCommunicationService, ApplicationDataContainers containers)
ITrayProcessCommunicationService trayProcessCommunicationService, ICommandHistoryService historyService)
{
_settingsService = settingsService;
_applicationView = applicationView;
_trayProcessCommunicationService = trayProcessCommunicationService;
_historyContainer = containers.HistoryContainer;
_historyService = historyService;
InitializeComponent();
@@ -65,7 +72,11 @@ namespace FluentTerminal.App.Dialogs
var error = await ViewModel.AcceptChangesAsync();
if (!string.IsNullOrEmpty(error))
if (string.IsNullOrEmpty(error))
{
_dialogResult = ContentDialogResult.Primary;
}
else
{
args.Cancel = true;
@@ -77,6 +88,11 @@ namespace FluentTerminal.App.Dialogs
deferral.Complete();
}
private void OnSecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
_dialogResult = ContentDialogResult.Secondary;
}
private async Task SaveLink()
{
var link = await ViewModel.GetUrlAsync();
@@ -117,11 +133,19 @@ namespace FluentTerminal.App.Dialogs
{
ViewModel.SetFilter(sender.Text.Trim());
}
// TODO: Else branch added for Tab-selection hack mentioned above.
else if (_tabSelectedCommand != null)
{
ViewModel.Command = _tabSelectedCommand.ExecutedCommand.Value;
CommandSelected(_tabSelectedCommand);
_tabSelectedCommand = null;
}
}
private void CommandTextBox_OnSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
{
_lastChosenCommand = (args.SelectedItem as CommandItemViewModel)?.ExecutedCommand;
_lastChosenCommand = args.SelectedItem as CommandItemViewModel;
}
private void CommandTextBox_OnQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
@@ -130,34 +154,53 @@ namespace FluentTerminal.App.Dialogs
if (args.ChosenSuggestion is CommandItemViewModel commandItem)
{
var executedCommand = ViewModel.Commands
.FirstOrDefault(c =>
c.ExecutedCommand.Value.Equals(commandItem.ExecutedCommand.Value.ToString(),
StringComparison.OrdinalIgnoreCase))?.ExecutedCommand;
CommandSelected(commandItem);
}
}
if (executedCommand != null)
{
ViewModel.SetProfile(executedCommand.ShellProfile.Clone());
}
private void CommandSelected(CommandItemViewModel commandItem)
{
var executedCommand = ViewModel.Commands.FirstOrDefault(c =>
c.ExecutedCommand.Value.Equals(commandItem.ExecutedCommand.Value,
StringComparison.OrdinalIgnoreCase))?.ExecutedCommand;
if (executedCommand != null)
{
ViewModel.SetProfile(executedCommand.ShellProfile.Clone());
}
}
private void CommandTextBox_OnPreviewKeyUp(object sender, KeyRoutedEventArgs e)
{
var command = _lastChosenCommand;
switch (e.Key)
{
case VirtualKey.Delete:
if (e.Key == VirtualKey.Delete && !ViewModel.IsProfileCommand(command))
{
ViewModel.RemoveCommand(command);
e.Handled = true;
}
else
{
e.Handled = false;
if (_lastChosenCommand?.ExecutedCommand is { } command)
{
if (!ViewModel.IsProfileCommand(command))
{
ViewModel.RemoveCommand(command);
}
e.Handled = true;
}
else
{
e.Handled = false;
}
return;
default:
e.Handled = false;
return;
}
}
private void CommandTextBox_OnKeyUp(object sender, KeyRoutedEventArgs e)
private async void CommandTextBox_OnKeyUp(object sender, KeyRoutedEventArgs e)
{
switch (e.Key)
{
@@ -169,7 +212,7 @@ namespace FluentTerminal.App.Dialogs
CommandTextBox.IsSuggestionListOpen = true;
}
break;
return;
case VirtualKey.Enter:
@@ -179,48 +222,56 @@ namespace FluentTerminal.App.Dialogs
}
else
{
// TODO: Try to find a better way to handle [Enter] on auto-complete field.
// Weird way to move the focus to the primary button...
var error = await ViewModel.AcceptChangesAsync();
if (!(FocusManager.FindLastFocusableElement(this) is Control secondaryButton) ||
!secondaryButton.Name.Equals("SecondaryButton"))
if (string.IsNullOrEmpty(error))
{
return;
_dialogResult = ContentDialogResult.Primary;
_showDialogOperation.Cancel();
}
secondaryButton.Focus(FocusState.Programmatic);
var options = new FindNextElementOptions
else
{
SearchRoot = this,
XYFocusNavigationStrategyOverride = XYFocusNavigationStrategyOverride.Projection
};
if (FocusManager.FindNextElement(FocusNavigationDirection.Left, options) is Control primaryButton)
{
primaryButton.Focus(FocusState.Programmatic);
await new MessageDialog(error, I18N.Translate("InvalidInput")).ShowAsync();
}
}
break;
return;
}
}
public async Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null)
private void CommandTextBox_OnKeyDown(object sender, KeyRoutedEventArgs e)
{
_tabSelectedCommand = null;
if (e.Key == VirtualKey.Tab)
{
_tabSelectedCommand = _lastChosenCommand;
if (_lastChosenCommand != null)
{
ViewModel.Command = _lastChosenCommand.ExecutedCommand.Value;
CommandSelected(_lastChosenCommand);
}
}
e.Handled = false;
}
public Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null)
{
ViewModel = new CommandProfileProviderViewModel(_settingsService, _applicationView,
_trayProcessCommunicationService, _historyContainer, input);
_trayProcessCommunicationService, _historyService, input);
SetupFocus();
if (await ShowAsync() != ContentDialogResult.Primary)
{
return null;
}
_showDialogOperation = ShowAsync();
ViewModel.Model.Tag = new DelayedHistorySaver(ViewModel);
return ViewModel.Model;
return _showDialogOperation.AsTask().ContinueWith(t =>
(t.Status == TaskStatus.Canceled || t.Status == TaskStatus.RanToCompletion) &&
_dialogResult == ContentDialogResult.Primary
? ViewModel.Model
: null);
}
}
}
@@ -10,10 +10,12 @@ using FluentTerminal.App.Utilities;
namespace FluentTerminal.App.Dialogs
{
// ReSharper disable once RedundantExtendsListEntry
public sealed partial class InputDialog : ContentDialog, IInputDialog
{
private bool EnterPressed = false;
// ReSharper disable once RedundantDefaultMemberInitializer
private bool _enterPressed = false;
public string Input { get; private set; }
public InputDialog(ISettingsService settingsService)
@@ -25,14 +27,11 @@ namespace FluentTerminal.App.Dialogs
RequestedTheme = ContrastHelper.GetIdealThemeForBackgroundColor(currentTheme.Colors.Background);
}
public async Task<string> GetInput()
public Task<string> GetInput()
{
var result = await ShowAsync();
if (result == ContentDialogResult.Primary || EnterPressed)
{
return Input;
}
return null;
return ShowAsync().AsTask()
.ContinueWith(t => t.Result == ContentDialogResult.Primary || _enterPressed ? Input : null,
TaskContinuationOptions.OnlyOnRanToCompletion);
}
public void SetTitle(string title)
@@ -44,7 +43,7 @@ namespace FluentTerminal.App.Dialogs
{
if (e.Key == VirtualKey.Enter)
{
EnterPressed = true;
_enterPressed = true;
Hide();
}
}
@@ -1,17 +0,0 @@
<ContentDialog
x:Class="FluentTerminal.App.Dialogs.ShellProfileSelectionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Uid="SelectProfileUI"
Title="Select Profile"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
PrimaryButtonText="Start"
mc:Ignorable="d">
<ComboBox
HorizontalAlignment="Stretch"
DisplayMemberPath="Name"
ItemsSource="{x:Bind Profiles}"
SelectedItem="{x:Bind SelectedProfile, Mode=TwoWay}" />
</ContentDialog>
@@ -1,40 +0,0 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.Utilities;
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Dialogs
{
public sealed partial class ShellProfileSelectionDialog : ContentDialog, IShellProfileSelectionDialog
{
public IEnumerable<ShellProfile> Profiles { get; }
public ShellProfile SelectedProfile { get; private set; }
public ShellProfileSelectionDialog(ISettingsService settingsService)
{
Profiles = settingsService.GetShellProfiles();
SelectedProfile = Profiles.First();
this.InitializeComponent();
SecondaryButtonText = I18N.Translate("Cancel");
var currentTheme = settingsService.GetCurrentTheme();
RequestedTheme = ContrastHelper.GetIdealThemeForBackgroundColor(currentTheme.Colors.Background);
}
public async Task<ShellProfile> SelectProfile()
{
var result = await ShowAsync();
if (result == ContentDialogResult.Primary)
{
return SelectedProfile;
}
return null;
}
}
}
+18 -33
View File
@@ -140,7 +140,7 @@
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="Mosh ports:"
Visibility="{x:Bind ViewModel.UseMosh, Converter={StaticResource TrueToVisibleConverter}}" />
Visibility="{x:Bind ViewModel.UseMosh, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<TextBox
Grid.Row="0"
Grid.Column="1"
@@ -148,14 +148,14 @@
VerticalAlignment="Center"
BeforeTextChanging="Port_OnBeforeTextChanging"
Text="{x:Bind ViewModel.MoshPortFrom, Mode=TwoWay}"
Visibility="{x:Bind ViewModel.UseMosh, Converter={StaticResource TrueToVisibleConverter}}" />
Visibility="{x:Bind ViewModel.UseMosh, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="2,8,0,0"
VerticalAlignment="Center"
Text=":"
Visibility="{x:Bind ViewModel.UseMosh, Converter={StaticResource TrueToVisibleConverter}}" />
Visibility="{x:Bind ViewModel.UseMosh, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<TextBox
Grid.Row="0"
Grid.Column="3"
@@ -163,42 +163,18 @@
VerticalAlignment="Center"
BeforeTextChanging="Port_OnBeforeTextChanging"
Text="{x:Bind ViewModel.MoshPortTo, Mode=TwoWay}"
Visibility="{x:Bind ViewModel.UseMosh, Converter={StaticResource TrueToVisibleConverter}}" />
<!-- Row -->
<TextBlock
x:Uid="LineEndingModeOnPaste"
Grid.Row="1"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="Line ending on paste:" />
<ComboBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="3"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{x:Bind ViewModel.LineEndingStyles}"
SelectedItem="{x:Bind ViewModel.LineEndingTranslation, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="enums:LineEndingStyle">
<TextBlock Text="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=LineEndingStyle}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Visibility="{x:Bind ViewModel.UseMosh, Mode=OneWay, Converter={StaticResource TrueToVisibleConverter}}" />
<!-- Row -->
<TextBlock
x:Uid="SshInfoDialog_TabColor"
Grid.Row="2"
Grid.Row="1"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="Tab color:" />
<ComboBox
Grid.Row="2"
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="3"
Margin="0,8,0,0"
@@ -210,13 +186,13 @@
<!-- Row -->
<TextBlock
x:Uid="Theme"
Grid.Row="3"
Grid.Row="2"
Grid.Column="0"
Margin="0,8,8,0"
VerticalAlignment="Center"
Text="Theme:" />
<ComboBox
Grid.Row="3"
Grid.Row="2"
Grid.Column="1"
Grid.ColumnSpan="3"
Margin="0,8,0,0"
@@ -227,13 +203,22 @@
<!-- Row -->
<CheckBox
Grid.Row="4"
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="4"
Margin="0,8,0,0"
Content="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseConPty}"
IsChecked="{x:Bind ViewModel.UseConPty, Mode=TwoWay}" />
<!-- Row -->
<CheckBox
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="4"
Margin="0,8,0,0"
Content="{x:Bind Converter={StaticResource I18NConverter}, ConverterParameter=UseBuffer}"
IsChecked="{x:Bind ViewModel.UseBuffer, Mode=TwoWay}" />
</Grid>
</controls:Expander>
@@ -146,17 +146,12 @@ namespace FluentTerminal.App.Dialogs
if (string.IsNullOrEmpty(ViewModel.Username))
{
ViewModel.Username = await _trayProcessCommunicationService.GetUserName();
ViewModel.Username = await _trayProcessCommunicationService.GetUserNameAsync();
}
SetupFocus();
if (await ShowAsync() != ContentDialogResult.Primary)
{
return null;
}
return (SshProfile)ViewModel.Model;
return await ShowAsync() == ContentDialogResult.Primary ? (SshProfile) ViewModel.Model : null;
}
}
}
@@ -1,19 +0,0 @@
<ContentDialog
x:Class="FluentTerminal.App.Dialogs.SshProfileSelectionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Uid="SelectSshProfileUI"
Title="Select SSH Profile"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
DefaultButton="Primary"
PrimaryButtonText="Select"
SecondaryButtonText="Cancel"
mc:Ignorable="d">
<ComboBox
HorizontalAlignment="Stretch"
DisplayMemberPath="Name"
ItemsSource="{x:Bind Profiles}"
SelectedItem="{x:Bind SelectedProfile, Mode=TwoWay}" />
</ContentDialog>
@@ -1,54 +0,0 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Utilities;
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace FluentTerminal.App.Dialogs
{
public sealed partial class SshProfileSelectionDialog : ContentDialog, ISshProfileSelectionDialog
{
private IDialogService _dialogService;
public ObservableCollection<SshProfile> Profiles { get; }
public SshProfile SelectedProfile { get; private set; }
public SshProfileSelectionDialog(ISettingsService settingsService, IDialogService dialogService)
{
_dialogService = dialogService;
Profiles = new ObservableCollection<SshProfile>(settingsService.GetSshProfiles().OrderBy(p => p.Name));
var selectedProfile = settingsService.GetDefaultSshProfile();
if (selectedProfile != null)
{
selectedProfile = Profiles.FirstOrDefault(p => p.Id.Equals(selectedProfile.Id));
}
SelectedProfile = selectedProfile ?? Profiles.FirstOrDefault();
this.InitializeComponent();
var currentTheme = settingsService.GetCurrentTheme();
RequestedTheme = ContrastHelper.GetIdealThemeForBackgroundColor(currentTheme.Colors.Background);
}
public async Task<SshProfile> SelectProfile()
{
if (!Profiles.Any())
{
await _dialogService.ShowMessageDialogAsnyc("SSH Profile Selection", "There are no saved SSH Profiles.",
DialogButton.OK);
return null;
}
return await ShowAsync() == ContentDialogResult.Primary ? SelectedProfile : null;
}
}
}
+67 -31
View File
@@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>FluentTerminal.App</RootNamespace>
<AssemblyName>FluentTerminal.App</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<DefaultLanguage>en</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.18362.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.16299.0</TargetPlatformMinVersion>
@@ -28,7 +28,7 @@
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>7.1</LangVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
@@ -41,7 +41,7 @@
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
<LangVersion>7.1</LangVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
<DebugSymbols>true</DebugSymbols>
@@ -53,6 +53,7 @@
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
<OutputPath>bin\ARM\Release\</OutputPath>
@@ -65,6 +66,7 @@
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
@@ -76,7 +78,7 @@
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<LangVersion>7.1</LangVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
@@ -89,15 +91,15 @@
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
<LangVersion>7.1</LangVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<Compile Include="Actions\FocusAction.cs" />
<Compile Include="Adapters\ApplicationViewAdapter.cs" />
<Compile Include="Adapters\AppServiceConnectionAdapter.cs" />
<Compile Include="Adapters\DispatcherTimerAdapter.cs" />
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
</Compile>
@@ -106,9 +108,14 @@
<Compile Include="CommandLineArguments\RunVerb.cs" />
<Compile Include="CommandLineArguments\SettingsVerb.cs" />
<Compile Include="CommandLineArguments\Target.cs" />
<Compile Include="Converters\MenuViewModelToFlyoutMenuConverter.cs" />
<Compile Include="Converters\BackgroundToApplicationThemeConverter.cs" />
<Compile Include="Converters\BooleanNegationConverter.cs" />
<Compile Include="Converters\ColorResourceKeyFallbackConverter.cs" />
<Compile Include="Converters\I18NConverter.cs" />
<Compile Include="Converters\IconConverter.cs" />
<Compile Include="Converters\MenuItemViewModelBaseToMenuFlayoutItemBaseConverter.cs" />
<Compile Include="Converters\NullToCollapsedConverter.cs" />
<Compile Include="Converters\TabColorFallbackConverter.cs" />
<Compile Include="Converters\EnumValueToVisibiltyConverter.cs" />
<Compile Include="Converters\FalseToVisibleConverter.cs" />
@@ -126,6 +133,10 @@
<Compile Include="Dialogs\AboutDialog.xaml.cs">
<DependentUpon>AboutDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Services\AcceleratorKeyValidator.cs" />
<Compile Include="Services\CommandHistoryService.cs" />
<Compile Include="Utilities\DelayedAction.cs" />
<Compile Include="Utilities\DispatcherExtensions.cs" />
<Compile Include="Utilities\InteractiveSurface.cs" />
<Compile Include="ViewModels\CommandItemViewModel.cs" />
<Compile Include="Dialogs\CustomCommandDialog.xaml.cs">
@@ -134,26 +145,19 @@
<Compile Include="Dialogs\InputDialog.xaml.cs">
<DependentUpon>InputDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Dialogs\ShellProfileSelectionDialog.xaml.cs">
<DependentUpon>ShellProfileSelectionDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Dialogs\SshInfoDialog.xaml.cs">
<DependentUpon>SshInfoDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Dialogs\SshProfileSelectionDialog.xaml.cs">
<DependentUpon>SshProfileSelectionDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Services\ApplicationLanguageService.cs" />
<Compile Include="Services\ClipboardService.cs" />
<Compile Include="Services\FileSystemService.cs" />
<Compile Include="Services\ImageFileSystemService.cs" />
<Compile Include="Services\StartupTaskService.cs" />
<Compile Include="Services\SystemFontService.cs" />
<Compile Include="Utilities\ColorExtensions.cs" />
<Compile Include="Utilities\ContrastHelper.cs" />
<Compile Include="Utilities\DebouncedAction.cs" />
<Compile Include="Utilities\JumpListHelper.cs" />
<Compile Include="ViewModels\CommandProfileProviderViewModel.cs" />
<Compile Include="ViewModels\DelayedHistorySaver.cs" />
<Compile Include="Views\BooleanTemplateSelector.cs" />
<Compile Include="Views\EnvironmentVariablesView.xaml.cs">
<DependentUpon>EnvironmentVariablesView.xaml</DependentUpon>
@@ -168,6 +172,7 @@
<Compile Include="Dialogs\CreateKeyBindingDialog.xaml.cs">
<DependentUpon>CreateKeyBindingDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Views\MenuExtension.cs" />
<Compile Include="Views\OverlayView.xaml.cs">
<DependentUpon>OverlayView.xaml</DependentUpon>
</Compile>
@@ -236,11 +241,14 @@
<PRIResource Include="Strings\fr\Resources.resw" />
<PRIResource Include="Strings\es\Resources.resw" />
<PRIResource Include="Strings\de\Resources.resw" />
<PRIResource Include="Strings\zh-CN\Resources.resw" />
<PRIResource Include="Strings\zh-Hant\Resources.resw" />
<PRIResource Include="Strings\en\Resources.resw" />
</ItemGroup>
<ItemGroup>
<Content Include="Properties\Default.rd.xml" />
<PRIResource Include="Strings\it\Resources.resw" />
<PRIResource Include="Strings\nl\Resources.resw" />
<PRIResource Include="Strings\pl\Resources.resw" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
@@ -259,18 +267,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Dialogs\ShellProfileSelectionDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Dialogs\SshInfoDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Dialogs\SshProfileSelectionDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\EnvironmentVariablesView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@@ -351,31 +351,31 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac">
<Version>4.9.4</Version>
<Version>5.1.2</Version>
</PackageReference>
<PackageReference Include="CommandLineParser">
<Version>2.6.0</Version>
<Version>2.7.82</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.9</Version>
<Version>6.2.10</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.UI">
<Version>5.1.1</Version>
<Version>6.0.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.UI.Controls">
<Version>5.1.1</Version>
<Version>6.0.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.UI.Xaml">
<Version>2.2.190830001</Version>
<Version>2.3.200213001</Version>
</PackageReference>
<PackageReference Include="Microsoft.Xaml.Behaviors.Uwp.Managed">
<Version>2.0.1</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.2</Version>
<Version>12.0.3</Version>
</PackageReference>
<PackageReference Include="Win2D.uwp">
<Version>1.23.0</Version>
<Version>1.24.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -401,6 +401,42 @@
<Name>Windows Desktop Extensions for the UWP</Name>
</SDKReference>
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\ro\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\hi\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\pt-BR\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\ar-iq\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\he\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\az\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\ja\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\ko\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\uk\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\ar\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\id\Resources.resw" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="Strings\zh-Hans\Resources.resw" />
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
@@ -0,0 +1,13 @@
using System;
using Windows.System;
using FluentTerminal.Models.Enums;
namespace FluentTerminal.App.Services
{
public class AcceleratorKeyValidator : IAcceleratorKeyValidator
{
public bool Valid(int key) => Enum.IsDefined(typeof(VirtualKey), key);
public bool Valid(ExtendedVirtualKey key) => Valid((int) key);
}
}
@@ -10,12 +10,27 @@ namespace FluentTerminal.App.Services
private readonly Dictionary<string, string> _languages = new Dictionary<string, string>
{
[""] = "System default",
["zh-Hans-CN"] = "Chinese (Traditional)",
["ar"] = "العربية",
["ar-IQ"] = "اللهجة العراقية",
["az"] = "azərbaycan dili",
["de"] = "Deutsch",
["en"] = "English",
["es"] = "Español",
["fr"] = "Français",
["ru"] = "Pусский"
["he"] = "עברית",
["hi"] = "हिन्दुस्तानी",
["id"] = "Bahasa Indonesia",
["it"] = "Italiano",
["ja"] = "日本語",
["ko"] = "한국어",
["nl"] = "Nederlands",
["pl"] = "Polski",
["pt-BR"] = "Português-Brasil",
["ro"] = "Română",
["ru"] = "Pусский",
["uk"] = "Українська",
["zh-Hans"] = "繁體中文",
["zh-Hant"] = "简体中文"
};
public IEnumerable<string> Languages => _languages.Values;
@@ -32,6 +47,10 @@ namespace FluentTerminal.App.Services
public string GetCurrentLanguage()
{
if (!_languages.ContainsKey(ApplicationLanguages.PrimaryLanguageOverride))
{
ApplicationLanguages.PrimaryLanguageOverride = "en";
}
return _languages[ApplicationLanguages.PrimaryLanguageOverride];
}
}
Loaded 100 of 317 files, more files were not shown because too many files have changed in this diff. Show more