Compare commits

...
Author SHA1 Message Date
Felix Seidl d25196e525 Merge branch 'master' into fix-quicklaunch-new-window 2020-03-25 17:40:09 +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 00ed1560ef fix crash on quick launch to new window 2020-02-29 16:10:28 +01:00
Felix Seidl ae6e61d6fa Rework themes settings 2020-02-29 15:30:46 +01:00
Felix Seidl 5ae7151916 Added handler for xterm.js onBinary (supposed to handle legacy mouse reports) 2020-02-29 13:24:09 +01:00
Felix Seidl 0e2821eeca Added option to use non opaque background
fixes #279
2020-02-29 11:46:29 +01:00
Felix Seidl 3ef374f79d Remove obsolete project settings 2020-02-25 18:48:18 +01:00
Felix Seidl 9413cdc500 Update dependencies, fix solution config 2020-02-25 18:22:51 +01:00
Felix Seidl 80d57342f9 use unicode 11 2020-02-25 18:22:07 +01:00
Felix Seidl 089ec5dc54 Update translations 2020-02-25 18:21:49 +01:00
Felix Seidl a9ec669a27 add new unicode addon 2020-02-24 18:55:28 +01:00
Aleksandar Pesic 7ed503390d Improved TabBar. (#668) 2020-02-24 11:47:04 +01:00
Felix Seidl 33bc06a366 Update xterm.js, use official serialize addon
#fixes #652
2020-02-10 19:20:55 +01:00
Aleksandar Pesic 9c6a7ea2e2 Fixed a bug with profile change in history commands not being preserved. (#661) 2020-02-06 08:47:51 +01:00
Max Risuhin 3126d7b672 Removes domain name from user name. (#660) 2020-02-05 17:13:19 +01:00
Felix Seidl b5be5e4995 Update translations 2020-02-01 12:07:34 +01:00
Felix Seidl 6a0b4b13f2 Update README.md 2020-01-31 19:03:08 +01:00
Marcus Perryman d3ad7b3797 adjustments that allow CI build step to run locally (#659)
* adjustments that allow CI build step to run locally

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

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

* async/await in FluentTerminal.SystemTray project.

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

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

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

* async/await in FluentTerminal.SystemTray project.

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

* ConfigureAwait in FluentTerminal.App.ViewModels project adjusted.

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

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

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

* Renaming a file.

* Code cosmetics.

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

* Reviewed async pattern throughout FluentTerminal.App project.

* Async patterns in TrayProcessCommunicationService reviewed.

* Minor changes.

* Replaced ContinueWith with async/await in TrayProcessCommunicationService.

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

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

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

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

* After merge fixes, and some minor changes.

* DelayedAction fixed and some UI bug fixed.

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

* After merge fixes, and some minor changes.

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

* Replacing SemaphoreSlim with lighter TaskCompletionSource.

* Some async/await simplification.

* Resize locking mechanism in XtermTerminalView simplified.

* Resize handling improved.

* Trivial change in a code comment.

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

* Minor refactoring.

* XtermTerminalView initialization simplified.

* Additionally simplifying XtermTerminalView initialization.

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

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

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

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

* Clicking menu items works.

* Fixing an issue with resources.

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

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

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

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

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

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

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

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

* Merging the latest changed.

* Fixing some after-merge issues.

* Minor refactoring.

* Minor refactoring.

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

* Default profile name in the menu item now updates.

* Updating resources from POEditor.

* New tab/window cascading menues implemented.

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

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

* Configurable tab/window cascading menu.

* "Application menu type" settings value added.

* Minor spelling fix.

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

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

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

* Clicking menu items works.

* Fixing an issue with resources.

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

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

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

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

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

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

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

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

* Merging the latest changed.

* Fixing some after-merge issues.

* Minor refactoring.

* Minor refactoring.

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

* Default profile name in the menu item now updates.

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

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

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

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

* Clicking menu items works.

* Fixing an issue with resources.

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

* Menu displayed properly, except for "Settings".

* Menu works, but it doesn't refresh.

* Works. But needs to be polished.

* All known issues with the new app menu solved.

* Significantly simplifying app menu binding.

* Removing some obsolete classes.

* Menu binding mechanism additionally optimized for performances.

* Removing unused translations.

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

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

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

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

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

* Removing some not needed code.

* Attempt closing ContentDialog programaticaly.

* Fixing a bug caused by new changes.

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

* Removing some not needed code.

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

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

* Trivial change.

* Another trivial change in MOSH_README.

* And another trivial...

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

* New mosh from dialog.

* CommandHistoryService implemented.

* Backward compatibility for command history implemented.

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

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

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

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

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

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

* Need to implement custom event handler for hover

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

* Removed clearSelection from leaveCallback

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

* Add the browse command to theme settings page

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

* Try to show the image that we download

* Change the save image method in file service

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

* Reorganize the UI for image import

* Add background brush converter

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

* Use the File object insted of a image path

* Add background style to content and toolbar

* Create the ImageFile object

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

* Remove commented code

* Remove the base64 string of the image file

* Disable if it's a preinstalled theme

* Add new event when background image changed

* Make the RemoveImportedImage method async

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

* Add the delete background image command

* Remove unused converters that we created

* Save background image in temporary folder

* Remove the temporary images and save the last one

* Implement the cancel and delete background

* Show the background image in terminal

* Split the image file methods in another interface

* Change the name of image saving method

* Prepare the object and methods to export encoded image

* Add import method to deserialize correctly the theme

* Remove the private from the set accessor

* Implememnt the import method

* Implement the conversion base64 image to real image

* Create the background image and import it

* Fix some actions bug

* Get the new image file name after import

* Fix some bugs when the image is deleted

* Move the background image in terminal view

* Translate the background image text

* Add the delete background command

* Fix the background image deletion if we click on cancel

* Add background opacity property to terminal view model

* Need to create a theme changed event

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

* Remove the background from the theme settings page

* Fix error of last edited image

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

* Push tab leak fix.

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

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

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

* ApplicationSettingsChanged event replaced by MVVMLight Messenger implementation.

* CurrentThemeChanged event replaced by MVVMLight Messenger implementation.

* SettingsService.KeyBindingsChanged event replaced with MVVMLight Messenger implementation.

* Further simplifying KeyBindingsChangedMessage propagation.

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

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

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

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

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

Add contributors button (Shields.io button)

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

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

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

French can use the app ;')

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

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

* Delayed command history saving improved.

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

* Opening auto-suggest list on down arrow implemented.

* Minor change.

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

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

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

* Adding "Quick Launch" to menu.

* Auto-complete, initial version.

* Builds, but not finished.

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

* Minor changes.

* Refactroing request/response identifiers.

* Improving command history.

* Minor refactoring.

* Deleting history by using "Del" key.

* Handling mosh special case in quick commands.

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

* New resource strings.

* Fixing quick command links.

* Fixing after-merging bugs.

* Improving filtering.

* Finishing filtering.

* Changing placeholder text.

* Minor refactoring.

* Updating resources.

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

* Turn on AlwaysShowTabs by default.

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

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

* Simplifying a bit.

* SSH view models simplfication.

* Refactoring duplicated code.

* Refactoring duplicated code.

* Renaming a resource file.

* Cleanup.

* Fixing an issue with TemplateSelectors.

* QuickSshDialog created.

* Minor fix.

* Wiring up QuickSshDialog.

* Minor fix.

* Fixed an issue with shared resources.

* Single-text-box dialog works.

* Improving "Quick SSH" look.

* Improving data validation workflow to avoid double validations.

* Refactoring view models and templates to avoid duplicated code.

* Minor fix.

* Fixing a bug with mosh.

* Cleanup.

* Cleanup.

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

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

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

* Implemented quick-ssh shortcuts.

* Fixing quick SSH command validation.

* Minor change.

* Minor change.

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

* Quick SSH profiles.

* Fixing a minor bug with custom links.

* Fixing remote profiles look and feel.

* Minor improvements.

* Fixing a minor bug.

* Minor changes based on Felix' comments.

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

* File checking infrastructure prepared.

* Implementing identity file validation.

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

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

* mosh.exe recompiled.

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

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

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

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

* Implementing "Set Default" on SSH Profiles.

* Simplifying profile view models.

* Fixing null themes issue.

* Changing line ending of a sinfle file to CRLF.

* Changing file to LF, to avoid false differences.

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

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

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

* Rationalise default SSH keybindings

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

* Reorder command enum

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

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

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

* Fixing async terminal adds.

* Improving behavior when new window is requested.

* Minor changes.

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

In this change:

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

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

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

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

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

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

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

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

Change-Id: Iadb468de5f724396db9a2de6bf3c3ba0d4d7ac2f

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

* Implemented opening links in the last active window.

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

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

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

* SSH handling changed to improve links.

* Creating shortcuts from SSH dialog.

* Trivial Mosh checkbox label fix

It's "Mosh" not "MOSH".

* Obtain mosh key from ssh connection (#40)

* Prepopulate SSH username with Windows username (#50)

* Undoing Max's mosh authentication changes.

* Adding the new mosh wrapper.

* Request user name from tray process (#51)

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

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

* Mosh works like charm.

* Mosh wrapper improved to print an error.

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

* Changing mosh wrapper again to return error codes.

* Slightly optimizing getting the username.

* Saving links improved.

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

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

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

* Simplifying SSH dialog.

* Fixing some leftovers after merging.

* Fixing some after-merge issues.

* Avoiding some unintentional upstream changes.

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

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

* New feature: JumpList

* Revert "FluentTerminal.SystemTray"

This reverts commit 5c3122ac394bea0a9e484e314983ad267683d714.

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

* Revert "FluentTerminal.SystemTray"

This reverts commit 5c3122ac394bea0a9e484e314983ad267683d714.

* Multi-language support: Add zh-CN

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

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

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

* Added F11 to toggle fullscreen

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

* removed F11

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

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

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

* using Interactivity

* trying to bind by reference

* fix bindings

* remove unused packages

* Move getter/setter to show method

* Convert to AutoProperty

* Make text copied notification configurable

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

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

* SSH implemented.

* Implementing SSH by using the existing app infrastructure.

* Refactored to new command instead of a profile

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

* Fix cancelling profile edit showing default line translation

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

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

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

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

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

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

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

* parse arguments with commandlineparser nuget

* Run commands with no underlying shell process

* Create RunVerb, cleanup terminal creation

* Fix window close bug

* Use working directory of caller if none specified

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

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

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

* Apply color to single tab

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

* fix TerminalTheme Equals()

* Fix new output indicator

* Ut8 and some fixes

* fix test

* fixed infinite loop on shell exit
2019-03-06 20:06:19 +01:00
Eric Cornelissen b85be454ce Improve settings window (#213)
* Update settings window layout with consistent margins

Updated the layout of the different views of the settings window so that
there is always margin at the bottom. In some cases this was achieved
using <StackPanels> for clarity and extensibility, grouping e.g. a title
and some radio options together with one unified margin.

Also made some small consistency changes to the .xaml files.

* Add width to Themes text input

* Normalize margins in settings

* Add default display values in Terminal settings

... for Arguments and Working Directory

* Generalize TemplateSelector for empty strings

* Add default display value for profile keybindings

* Update margins and fontsize in theme preview

* Update 'loading...' message in About

... to "Checking for updates..." and a spinner as suggested by
@skipmeister123

* No popup when cancelling profile edit no changes

* Delete profile when new and cancelled

* Open new and cloned themes in edit mode

... and make it so canceling deletes the theme.

* No popup when cancelling theme edit no changes

* Add resources for margin definitions

* Check for null Author in TerminalTheme

* Add resource for header margin definition

* Moved createdBy text to code behind
2019-02-28 21:09:52 +01:00
FelixSe 1f3dd72214 Improved text wrapping on window title 2019-02-28 20:44:00 +01:00
Eric Cornelissen c4fcd986c0 Add keybinding for configurable new window (#216)
* Add keybind to open new configurable window

* Close new configurable window on cancel

* renamed event args
2019-02-28 19:24:18 +01:00
FelixSe ea21d37089 Make InputBuffer more threadsafe 2019-02-25 21:10:39 +01:00
Paul Galbraith 2f133720cc Chocolatey install instructions (#209) 2019-02-19 07:30:43 +01:00
Paul Galbraith 250c90fa02 install option to force context menu integration without user interaction (#204) 2019-02-15 19:22:38 +01:00
Mike 7e2c5d2f45 Line ending translation (#172)
* On paste, the TerminalViewModel uses the ShellProfile to translate line endings.

Default line ending translation style is set to not modify the pasted contents, Next up, adding the radio button to shell profile.

* Added UI elements for choosing line ending style.

Using a converter to map between an enum and radio buttons, and another converter to map from enum to text based on the description of the enum value.

* Added "Paste without newlines" command.

* Adding missing converters to project file.

* A few fixes after cherry-picks.

* Adding missing Command property to PasteWithoutNewlines keybinding.

* Cleaned up some unnecessary property settings.

* fix #114 scrolling issue on presicion touchpads (#171)

* fix #114 scrolling issue on presicion touchpads, bounce scroll & pinch zoom

* rephrased comment

* removed css that enforced autohiding

* simplified js code handling scrollbar size

* Press enter to close an Input dialog (#177)

... As if the primary button is pressed.

* Renew certificate

* Update README.md

* Add option to use bold text in terminals (#183)

* Add option to use bold text in terminals

... as per #182

* Add BoldText to setting defaults

* Remove AppVeyor files

* Update README.md

* Add keybinding to change the (current) tab title (#193)

* Add keybinding for change the (current) tab title

Default binding is Ctrl+Shift+N

* Update change tab title default keybind to Ctrl+Shift+R

* Experimental ConPTY support (#199)

* First steps of ConPty support

* Added resize functionality

* fix cwd, args, hide console window

* Handle shell process exited

* Make ConPTY optional

* Fix automerge errors

* Accepted many XAML formatting changes, and changed command handling to switch.

* Fixed spaxcng on the settings page.

* adapted radio button binding to be in line with the rest of the application
2019-02-13 22:42:23 +01:00
FelixSe 9fed3fb192 Fix automerge errors 2019-02-09 23:06:45 +01:00
Felix 0d85f933c7 Experimental ConPTY support (#199)
* First steps of ConPty support

* Added resize functionality

* fix cwd, args, hide console window

* Handle shell process exited

* Make ConPTY optional
2019-02-08 22:12:29 +01:00
Eric Cornelissen f235ddf0f1 Add keybinding to change the (current) tab title (#193)
* Add keybinding for change the (current) tab title

Default binding is Ctrl+Shift+N

* Update change tab title default keybind to Ctrl+Shift+R
2019-02-08 21:55:23 +01:00
Felix 887cd45b4c Update README.md 2019-01-30 18:32:32 +01:00
FelixSe aac2f220d6 Remove AppVeyor files 2019-01-28 22:00:33 +01:00
Eric Cornelissen 90f15cc1e3 Add option to use bold text in terminals (#183)
* Add option to use bold text in terminals

... as per #182

* Add BoldText to setting defaults
2019-01-28 21:57:36 +01:00
Felix 292b66af4d Update README.md 2019-01-28 19:26:48 +01:00
FelixSe b6d835984c Renew certificate 2019-01-26 12:29:32 +01:00
Eric Cornelissen 369b5ddacf Press enter to close an Input dialog (#177)
... As if the primary button is pressed.
2019-01-18 21:20:56 +01:00
Mike 6be29f49d7 fix #114 scrolling issue on presicion touchpads (#171)
* fix #114 scrolling issue on presicion touchpads, bounce scroll & pinch zoom

* rephrased comment

* removed css that enforced autohiding

* simplified js code handling scrollbar size
2019-01-14 21:47:35 +01:00
FelixSe 8131140cb0 Increase version 2019-01-08 19:51:54 +01:00
FelixSe 304314230c update xterm to 3.10.1 2019-01-08 19:27:47 +01:00
FelixSe 0ce605f122 Fix DialogService tests 2019-01-07 22:46:26 +01:00
FelixSe d4aadd7dd4 Added option to change the title of a tab 2019-01-07 22:35:14 +01:00
FelixSe 3cf3e0a330 Themes can now be cloned 2019-01-07 19:37:21 +01:00
FelixSe ff2cfc36b6 Fix problems with selectAll, clear and Ctrl+C 2019-01-04 18:15:24 +01:00
FelixSe b118c66959 Increase version 2019-01-02 22:15:02 +01:00
FelixSe 131e3536f1 Fix theme not updating for profiles without theme 2018-12-30 22:56:11 +01:00
FelixSe b47b544065 Added option to restore preinstalled profiles 2018-12-30 19:00:24 +01:00
FelixSe b13c40e7e8 Clone profiles 2018-12-30 16:20:56 +01:00
Felix 4e47fcef72 Added option to specify theme in profile (#158) 2018-12-30 15:12:04 +01:00
Mike 8acf618fd4 Per-shell-profile keyboard shortcuts (#85)
* Added new command wrapper, abstracts static and dynamic commands

- All uses of the `Command` are replaced with either `AppCommand`, for a command that is static to the scope of the application, or a reference to an ICommand (specifically, an `EnumCommand<AppCommand>()` instance), which provides the abstract wrapper.
- Functionality seems intact.
- Next step is to have the MainViewModel add the per-shell commands via SettingsService.CreateShellKeyboardShortcuts(), and have the SettingsService.ParseCommandString() function parse the command strings passed around properly.
- Finally, add the keyboard chord input to each shell's settings page.

* First kick at adding a new terminal keyboard command.

- It isn't presented anywhere, but references to keybindings should be using the ICommand key as their identifier for recognizing anything, and when xterm passes back a keyboard command event, the SettingsService is asked to convert the string into an ICommand.

* Renaming ICommand to Command.

It's not longer an interface, so this makes more sense.

* Renaming back to Command, and new class is AbstractCommand.

Going to simplify the PR diff considerably.

* Shortcuts for default shells work. Need to get the shell profile edit UI elements working.

* Added keyboard shortcut UI elements to shell profile pages.

* New and deleted shell profiles propagate to all terminal views.

* A bit of cleanup.

* Camel-casing command text to fix Ctrl+C and copy behaviour.

* Fixed failing test.

The new on disk store uses a .ToString(), not the Enum description, since the command isn't an Enum, it's a class now in general, and those semantics make more sense.

* Adding default value for null keybindings in shell profiles.

* Merge branch 'shell_commands' of https://github.com/Riebart/FluentTerminal into pr/85

Improved default value for keybinding property

* Renamed KeyBinding collection to KeyBindings

* Use strings as Command parameter of KeyBinding

* Added missing Commands to keyBindings of default shellProfiles

* fix build and unit tests after merge with master

* improve UI

* Split OnShellProfileCollectionChanged event
2018-12-29 13:20:24 +01:00
Eric Cornelissen 49e09f3f8b Add "Show in system tray" option (#142)
* Add "Show in system tray" option to the SettingsView

* Implement "Show in system tray" requiring restart

* Add warning to restart program when toggling tray

* Update warning text for toggling tray option
2018-12-26 16:31:28 +01:00
Fauzie 9bf8284d9f Add Padding setting (#155)
* add padding setting

* `=` instead of `:`

Co-Authored-By: fauzie811 <fauzie811@yahoo.com>

* call setPadding in createTerminal function

* Add "px" suffix to padding slider tooltip

Based on this tutorial: 
https://www.codeproject.com/Tips/715438/Slider-Thumb-Tooltip

* term.fit() after setPadding()

* move back term.fit() below term.open()

* Changed max value, added throttling, moved converter
2018-12-26 14:41:16 +01:00
FelixSe 0f8aac81de add throttle for background opacity binding 2018-12-26 14:18:54 +01:00
FelixSe abec73b868 update xterm to 3.9.1 2018-12-25 15:50:07 +01:00
Zed 86486c99f2 fixed open here dir param. (#145) 2018-12-04 07:30:32 +01:00
Abhishek Goenka d9b7052e87 Prerequisite added (#139) 2018-11-19 17:18:44 +01:00
Meir Blachman 0efe105321 added missing test for KeyboardCommandService (#124)
added missing test for KeyboardCommandService for Command.ToggleWindow
2018-11-12 21:15:57 +01:00
Eric Cornelissen ed0dad960d Fix display issue when settings window is too narrow (#115)
Closes #100
2018-11-07 07:05:16 +01:00
Eric Cornelissen 2f58ed63de Add About page to the settings window (#103)
* create "About" page in settings

* add content to About page

* Get latest version from GitHub

* Add NotificationService to servies

* Enable actions on notifications

* Get latest version asynchroniously

* Handle when latest version is not found and clean code

* Remove old UpdateService

* Remove old NotificationService

* Add icon for about tab

* Update About page styling

* Make About menu item a footer menu item

* Update About page layout

Based on suggestions by @felixse

* Instatiate temporary navigation item only once

This seems to solve display issues when opening the "About" page multiple times where the main navigation list is being rerendered for no apparent reason.

* Revert unintended changes

* Added shortcut to about page in hamburger menu

* Move About page properties to the AboutPageViewModel

* Make "loading..." and "Not found" italic to indicate non-default behaviour

* move update check back to systemtray

* Made IUpdateService async
2018-10-28 13:55:36 +01:00
FelixSe cc7e069f11 Increase version 2018-10-20 14:51:50 +02:00
FelixSe 94a0eac58f Fixed tab width, Added option to indicate new output on background tabs 2018-10-20 12:20:41 +02:00
FelixSe 9af0556d6c Update xterm.js to 3.8.0 2018-10-12 07:39:52 +02:00
A.J. Ruckman b14ca2c4ed update winpty dependencies from current winpty master code (#95) 2018-10-09 22:27:26 +02:00
FelixSe 89c1a5a614 ExtendedVirtualKey now supports all OEM keys, and uses the qwerty default for description 2018-10-09 22:20:05 +02:00
Justin Gallagher 3e32f611e8 Add automatic install script (#92) 2018-10-08 22:02:58 +02:00
Mike 07e65ce2bf Clarifying installing and dev environment setup instructions. (#83)
* Clarifying installing vs dev environment setup.

* Adding details on installing the app, as well as dev dependencies.
2018-10-05 10:55:41 +02:00
Mike f2a48487f1 Return an empty string when attempting to paste non-text clipboard buffer (#86)
* Return an empty stirng when attempting to paste non-text clipboard buffer.

Returning null Task reference was causing a crash when attempting to paste a non-text buffer.

* Simplifying returned empty string task.
2018-10-01 21:40:18 +02:00
Mike c9d0cc7368 Tab theme colours (#73) 2018-09-10 07:50:20 +02:00
Mike eecc2e8243 Development environment documentation, and npm package bump (#68)
* Updating documentation and npm packages

- The webpack 3.10 was too old, as webpack-cli and webpack-command, required dependencies for building, need 4.x.x or 4.4.0 respectively.
- `npm-check-updates` (`npm t -g npm-check-updates`) run with `ncu -u` auto-bumped xterm.js to 3.6.0, and webpack to 4.16.5. Unfortunately this also reordered the file, so those changes aren't obvious.
- I also added webpack-cli as an explicit dependency.
- Updates to the readme to detail putting together a build environment.

* Noting that the package should be for sideloading

For dev, and non-published packages, the sideloading option is best.

* Adding dev StoreAssociation XML file for dev building.

* Cleaning up the packaging instructions.

* Changed instruction from rename to copy.

- Copy will not try to clobber the working directory of the template
file.

* Bumping xterm to 3.6.0, adding notes on installing built packages
2018-08-18 20:27:53 +02:00
Mike 5c1896cd3b Added support for Ctrl+<N> to switch to tab N. (#70) 2018-08-18 10:37:51 +02:00
Jakob Hermann Preusker e0cde7fe0f Fixed Issue #63: No scrolling in some pages (#66)
* Fixed Issue #63

* Requested changes to PR #66 done

* Fixed scrollbar position in themes/profiles
2018-08-17 07:48:15 +02:00
FelixSe 2d4d0bc23a Update version 2018-08-01 22:25:38 +02:00
FelixSe 9a5bff3d37 Update screenshots 2018-08-01 21:40:15 +02:00
FelixSe 28f212b7d6 Pin language version for services dll 2018-07-31 21:29:07 +02:00
FelixSe bfad59fa7a Added option to start SystemTray on windows startup 2018-07-31 21:13:29 +02:00
FelixSe f0e8f01d30 Added option to always show tabs 2018-07-28 23:02:42 +02:00
FelixSe 3ffd94e99d Clear selection after selecting with 'select to copy' enabled 2018-07-28 22:28:55 +02:00
FelixSe 1be49d27ab Default profile is now saved in local settings 2018-07-28 22:10:41 +02:00
FelixSe 4f43f64783 Fixed a bug that preventend toggle window from hiding the terminal 2018-07-20 23:38:46 +02:00
FelixSe e7997a5272 increase version 2018-07-19 23:52:45 +02:00
FelixSe 507138364e Improved handling of missing profiles/themes 2018-07-19 23:28:57 +02:00
FelixSe 0a4a1baeba increase version 2018-07-19 21:59:19 +02:00
FelixSe 14d111c4de Added mouse settings for copy&paste 2018-07-19 21:22:47 +02:00
FelixSe db73d7e1cb Fixed orphaned terminals on window close 2018-07-19 18:54:52 +02:00
FelixSe 825e9883d7 increase version 2018-07-19 12:39:04 +02:00
Felix b2866c34df Update README.md 2018-07-19 12:08:34 +02:00
Felix 58e3ab9115 Use AppService for Commnunication instead of WebSocket (#55)
* Use AppService for Commnunication instead of WebSocket

* Code cleanup

* Some polishing

* Added some tests
2018-07-19 12:05:02 +02:00
Felix 2b66b3c87f Update README.md 2018-07-18 21:14:11 +02:00
FelixSe 4505504732 Refactored paste to be compatible with xterm 3.5 2018-07-17 21:55:10 +02:00
FelixSe 567da9b0a3 Update xterm to 3.5.1 2018-07-17 20:44:45 +02:00
FelixSe 7ccc36b121 Added option to display tabs on the bottom of the window 2018-07-08 20:47:26 +02:00
Felix 559bf9d394 Improved scrolling behavior of the tabs bar (#54) 2018-07-08 18:28:16 +02:00
FelixSe cee34dfb63 Show title of selected tab in windows task bar 2018-07-06 23:56:24 +02:00
Felix 55995bd7a9 Update README.md 2018-07-02 22:43:28 +02:00
Felix 36edfddf74 Tests for Services (#52) 2018-07-01 13:54:14 +02:00
FelixSe 188046e60a Fixed invisible text when background color was used as text color (e.g. micro editor) 2018-06-25 21:48:55 +02:00
FelixSe 5338f3cae8 fix reversed blue/red colors in ubuntu theme 2018-06-25 21:40:40 +02:00
Felix 624146ecd7 Update README.md 2018-06-25 07:46:31 +02:00
Mike 8f711d1a9e Update readme to address issues with loopback exemption (#51)
* Update README for installation instructions

Changed from requiring developer mode to demonstrate to install when sideloading the app. Including the Powershell snippet necessary to enable the loopback exemption for the package family that is FluentTerminal.

* Updated notes on developer mode

* Linking to issue 43 directly.
2018-06-24 22:45:20 +02:00
Felix b50090060b Update README.md 2018-06-24 12:59:26 +02:00
DF 7db2321a82 Add notification for new version (#46)
* Add notification for new version

* fix spelling error

* Fix review comments
2018-06-23 17:30:47 +02:00
cxfksword def3aa3683 Fix toggle window exception (#49) 2018-06-21 22:27:12 +02:00
FelixSe 77d9a42c36 Adapted KeyBoardCommandServiceTests to not use ToggleWindow command 2018-06-21 22:04:08 +02:00
cxfksword d395085064 Add support grave accent as hotkey (#48) 2018-06-21 21:29:48 +02:00
Felix 56e5d60dae Update README.md 2018-06-17 15:53:37 +02:00
FelixSe 780cbf64b1 Added AppVeyor CI 2018-06-17 15:52:31 +02:00
FelixSe 6b25d78ce6 Remove unused nuget package 2018-06-17 14:49:22 +02:00
FelixSe 53726e0e68 Moved ViewModels into a separate asembly 2018-06-17 14:34:20 +02:00
FelixSe 5e11e5d58b Move existing tests to xUnit test project 2018-06-16 23:33:11 +02:00
FelixSe 3e0417ce47 Move services to a separate assembly 2018-06-16 22:50:27 +02:00
Felix 417bc843b0 Added missing '#' in ubuntu theme 2018-06-16 10:00:54 +02:00
Felix 6d6bdcca3e Merge pull request #41 from Linard11/master
Add WSL as default profile & terminal colors used in Ubuntu Bash
2018-06-15 11:17:46 +02:00
Linard Hug b4bfe39586 Fix typo and cursor selection color 2018-06-15 10:35:16 +02:00
Linard Hug 940cb6f38b Add 'Ubuntu Bash' as default theme 2018-06-15 02:23:29 +02:00
Linard Hug 59c6a39458 Add WSL as default profile 2018-06-15 02:23:04 +02:00
FelixSe 05ce19ec8c Increase version number 2018-06-12 22:59:05 +02:00
FelixSe 7b2bd59365 Improve clear and selectAll behaviour 2018-06-12 22:39:44 +02:00
FelixSe 6a67883777 Revert updating Win2D.uwp 2018-06-12 22:22:23 +02:00
FelixSe 847e1c045a Added option to set scrollback limit, comands to select all and clear buffer, cleaned up keyBindings UI code 2018-06-11 23:11:38 +02:00
FelixSe e918c5492e Update dependencies 2018-06-11 22:04:03 +02:00
FelixSe 1aba3d4926 Fixed text margin in tabs and added option to underline the selected tab 2018-06-10 20:54:51 +02:00
FelixSe 5ad51b6bf7 Added 'Tomorrow' as default theme to showcase light themes 2018-06-10 20:27:28 +02:00
FelixSe 72cfe1168f Export themes 2018-06-10 20:17:46 +02:00
FelixSe 5a96a0387b Set application theme based on the terminal's background color 2018-06-10 15:04:41 +02:00
FelixSe 00a5b6377d Fix selection color in imported themes 2018-06-10 12:14:25 +02:00
FelixSe f610aa8a0f Import iTerm themes 2018-06-09 19:34:32 +02:00
FelixSe c32342bca8 Remember window size and position 2018-06-08 21:42:12 +02:00
FelixSe 84af36f6bc Added fullscreen mode 2018-06-07 22:18:43 +02:00
FelixSe f56465f54a Improve storage of keyBindings 2018-06-07 21:57:45 +02:00
FelixSe 2527657261 Increase version number 2018-05-31 23:51:59 +02:00
FelixSe 5414987ab6 made background opacity global 2018-05-31 22:30:07 +02:00
FelixSe 327f0d5dd5 Improve titlebar color handling for settings window 2018-05-31 21:10:02 +02:00
FelixSe bd64aacbab Add option to set scrollbar style 2018-05-30 21:09:06 +02:00
FelixSe c79b513470 Fixed bug that tied name and arguments in profile editor together 2018-05-30 19:46:19 +02:00
FelixSe bd3ca90514 Fix crash in SettingsService, increase version 2018-05-29 19:09:17 +02:00
Felix 88f88698a2 Update README.md 2018-05-28 22:23:27 +02:00
FelixSe dcdf8d1854 Added search function 2018-05-27 20:22:21 +02:00
FelixSe fb717b3750 Fix crashes on delete or restore keybindings 2018-05-27 13:49:53 +02:00
FelixSe aff7b636f1 Add option to select shell profile while creating a new tab (ctrl+shift+t by default) 2018-05-27 13:35:02 +02:00
FelixSe 99a7de25d3 Close tab/window when shell process has exited 2018-05-26 22:32:15 +02:00
FelixSe 28d950ed9a Support multiple shell profiles 2018-05-26 22:05:49 +02:00
Felix 1490470e91 Merge pull request #22 from skipmeister123/master
Added "Homebrew" theme
2018-05-24 21:52:38 +02:00
Hans Kokx 236e0ff2e1 Added "Homebrew" theme 2018-05-24 15:43:24 -04:00
Felix 5cb9158077 Update README.md 2018-05-23 21:47:16 +02:00
FelixSe 941c7c1948 update to xterm 3.4.0 and use dynamic charAtlas 2018-05-20 12:16:03 +02:00
Felix 45e096c033 Update README.md 2018-05-06 19:49:08 +02:00
FelixSe d37da0ef31 fixed last ambiguous xcopy 2018-05-06 18:52:29 +02:00
FelixSe 49abf56ec6 fixed another ambiguous xcopy 2018-05-02 22:42:22 +02:00
FelixSe 618a2485e1 fix postbuild event when target directory is not yet created 2018-05-02 22:27:02 +02:00
felixse cdeeeb6fa8 Make hintpath in Models.csproj use a variable for program files (x86) 2018-05-02 11:02:00 +02:00
felixse be833cc174 Make hintpaths in SystemTray.csproj use a variable for program files (x86) 2018-05-02 10:40:22 +02:00
FelixSe 39955bcb3e Started writing some unit tests 2018-05-01 21:22:34 +02:00
FelixSe e9b76a6867 Code cleanup 2018-05-01 13:10:47 +02:00
FelixSe 89bce37dbd Fix bug while adding secondary keybindings 2018-05-01 13:10:41 +02:00
FelixSe 82a195c2f2 Use AppService instead of WebAPI for inter process communication 2018-04-29 15:52:57 +02:00
FelixSe c48889cbbc Execute copy only when selection is not empty, this allows for ctrl + c to be used as the default copy keybinding 2018-04-22 13:12:50 +02:00
FelixSe dd9d5d2e72 Extend key enum, improve displaying 2018-04-22 12:45:56 +02:00
FelixSe 7f7f15ea03 Support meta key for keybindings 2018-04-22 12:45:30 +02:00
FelixSe f502303426 Update xterm to 3.3.0 2018-04-15 16:23:24 +02:00
FelixSe 2f2ed06d58 Refactored App startup 2018-04-15 16:13:20 +02:00
FelixSe 809167a872 removed unnecessary quotes in context menu integration script 2018-04-15 16:11:26 +02:00
FelixSe 5d3fd25098 remove unused field 2018-04-15 16:10:41 +02:00
FelixSe 6674fd0ad9 Fix for open Fluent Terminal here failing if no other terminal window open 2018-03-18 14:07:34 +01:00
FelixSe f874cb070d Fix race condition while retrieving the systemtrays API port 2018-03-18 13:37:20 +01:00
FelixSe 40230bfce8 Rename tray command to 'new terminal' since it does not necessarily create a new window 2018-03-14 22:20:04 +01:00
FelixSe 3afa2b5f25 Remove App.Instance singleton 2018-03-11 00:26:23 +01:00
FelixSe 94ba1b8324 Update xterm.js to 3.2.0 2018-03-10 23:33:41 +01:00
FelixSe 6dfacdfebd Remove almost unused AppService in favor of using flute.exe 2018-03-10 00:06:25 +01:00
FelixSe 1ca7371103 No longer depending on a static port for communication with Tray process 2018-03-06 21:45:24 +01:00
Felix 1cb388d4af Update README.md 2018-03-05 21:16:56 +01:00
FelixSe 3bdd96c388 Screenshots look better without cursors.. 2018-03-05 21:02:10 +01:00
FelixSe 5bc5bf224f Add Screenshots 2018-03-05 20:56:45 +01:00
FelixSe 01815db1fe Fix typo, set correct c# version in csproj, increase version 2018-03-05 19:26:33 +01:00
FelixSe 80cf43d3bf Store client in app data instead of serving it 2018-03-04 22:26:30 +01:00
FelixSe 050f162c08 Split SettingsPageViewModel into one per page, fix buggy radio buttons 2018-03-04 14:40:05 +01:00
FelixSe f5e03c9856 Register converters only once 2018-03-04 13:22:27 +01:00
FelixSe f763798683 Added option to create new terminals (e.g from context menu) as tabs instead of windows 2018-03-04 13:07:32 +01:00
FelixSe 46120e3431 Make toggle window hotkey configurable 2018-03-03 11:44:46 +01:00
FelixSe 829077883b Improved layout for keybindings settings page 2018-02-26 22:00:58 +01:00
FelixSe 8c928faa09 Some cosmetic changes 2018-02-25 22:31:52 +01:00
FelixSe 1654e9f1f5 Propagate changed keyBindings immediately 2018-02-25 21:52:17 +01:00
FelixSe f542331192 Editable keybindings 2018-02-25 15:40:36 +01:00
FelixSe 6830adb788 Increase version, add generated files to .gitignore 2018-02-22 22:33:18 +01:00
FelixSe 02e015371c Supress webview native context menu 2018-02-22 22:07:21 +01:00
FelixSe 80bdfa012c Show notification if registering hotkey for toggle window fails 2018-02-20 22:04:23 +01:00
FelixSe 70326e9585 Add missing whitespace in product name 2018-02-20 22:04:20 +01:00
FelixSe c0e1415b6e Use shell executable name as tab title 2018-02-20 21:56:51 +01:00
FelixSe 13a3402b58 Improve error message for failed shell startup 2018-02-20 18:41:34 +01:00
FelixSe 73703412f8 Fix for shells no longer getting cleaned up on window close 2018-02-20 18:18:45 +01:00
FelixSe ab226d95d4 Added option to confirm before closing tab/window 2018-02-19 23:34:22 +01:00
FelixSe de7a76bb3b Add icons to settings pages 2018-02-19 21:01:09 +01:00
FelixSe 379f58b4b8 Rename open to show in tray icon 2018-02-19 20:38:37 +01:00
FelixSe 3a04cd3db2 Close tabs with middle click 2018-02-19 20:37:25 +01:00
FelixSe df0d3f080b Use non acrylic accent color for resize overlay 2018-02-19 19:34:54 +01:00
FelixSe 04131d271a Improve size of titlebar button icons 2018-02-19 19:31:31 +01:00
FelixSe d66fe26634 Global hotkey for show/hide window 2018-02-18 23:10:05 +01:00
FelixSe 8870f0621c Keyboard shortcuts 2018-02-18 21:08:22 +01:00
FelixSe e5250a7ae8 Reorderable tabs for windows 2018-02-17 17:32:48 +01:00
FelixSe b2b6cedf2c Increase version to 0.1.5.0 2018-02-16 17:37:54 +01:00
Felix e777c2c5ac Update README.md 2018-02-16 17:31:46 +01:00
FelixSe 7dcd8e1fbb Make background opacity configurable 2018-02-15 23:00:30 +01:00
FelixSe 37518cf88b Terminal Settings 2018-02-15 22:26:55 +01:00
FelixSe ca0e22b8d0 Reenable settings window title bar when navigation view is collapsed 2018-02-15 16:50:06 +01:00
FelixSe 074f5f9c7e Wrap dialogs in service 2018-02-14 18:00:00 +01:00
FelixSe 0b40963c82 change theme also on existing windows 2018-02-14 11:56:52 +01:00
FelixSe 0b6b8a9968 Fix selection color for xterm.js theme 2018-02-14 10:04:20 +01:00
FelixSe 47613454c7 Switch to official xterm.js 2018-02-14 10:04:05 +01:00
FelixSe 96d063b4fd Theme settings 2018-02-13 23:36:23 +01:00
FelixSe fec44fb0b5 fix context menu integrations for directories that contain whitespaces 2018-02-06 19:12:39 +01:00
FelixSe 58e7de5271 fix explorer integration path for installations where HomeDrive is not C: 2018-02-06 19:09:20 +01:00
FelixSe 466b7a7503 Display default window title again when terminal is resetting its title 2018-02-05 23:30:04 +01:00
FelixSe 50f9c84420 Support specifying the startup directory via command line and provide basic Explorer context menu integration 2018-02-05 23:23:03 +01:00
FelixSe f07dae98ee Escape shell executable path in commandLine 2018-02-04 00:12:44 +01:00
FelixSe 1cc74f79b4 Fix shell arguments 2018-02-03 23:34:07 +01:00
FelixSe 60c4f0518d Recover missing Client files 2018-02-03 23:20:53 +01:00
FelixSe f9fb05b194 Removed generated files from repository and added to .gitignore 2018-02-03 22:45:17 +01:00
FelixSe d933f60b8b Merge branch 'master' of https://github.com/felixse/FluentTerminal 2018-02-03 22:40:33 +01:00
FelixSe 45bba6e2eb Use hyper.js xterm fork for now instead of including it in this repository 2018-02-03 22:39:32 +01:00
FelixSe 210aeedc25 Update winpty to 0.4.3 2018-02-03 22:04:22 +01:00
Felix acfb8e8883 Update README.md 2018-02-03 21:46:00 +01:00
FelixSe 9b61d11f6a Added settings window to configure which shell to use 2018-02-03 21:25:34 +01:00
FelixSe 49ee8489d6 finished resize overlay 2018-01-31 21:18:13 +01:00
FelixSe 7b485ce407 setup dependency injection 2018-01-31 19:37:41 +01:00
FelixSe 66465b525f add MVVM infrastructure 2018-01-31 19:26:34 +01:00
FelixSe 9c5d8c27fd Move logic from web client to app to prepare configuration 2018-01-29 22:26:37 +01:00
FelixSe 4ae3be544b Resize terminal on window resize 2018-01-28 22:06:57 +01:00
FelixSe e32e0398ee Remove unnecessary build step 2018-01-28 22:05:38 +01:00
FelixSe fab22f2479 cleanup app.xaml.cs 2018-01-28 21:50:20 +01:00
Felix b2151c69ab Merge pull request #1 from vitorgrs/master
Remove splashscreen
2018-01-25 22:45:40 +01:00
Vitor Mikaelson f5fba8517a Remove splashscreen 2018-01-25 20:51:02 +00:00
760 changed files with 54263 additions and 52216 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Git will handle the files in whatever way it thinks is best.
# Overwrites global core.autocrlf git setting
* text=auto
+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: |
npm install
npm run build
- name: Setup MSBuild.exe
uses: warrenbuckley/Setup-MSBuild@v1
- name: MSBuild
run: msbuild FluentTerminal.sln -t:build -p:Configuration=Debug -p:Platform=x64 -m -restore
+5
View File
@@ -1,5 +1,8 @@
# Project specific
FluentTerminal.App/Win32
FluentTerminal.App/Client/
FluentTerminal.Client/dist
/.sonarqube
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
@@ -289,3 +292,5 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
!FluentTerminal.App/MoshExecutables/*
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
reg add "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%1\"" /f
reg add "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
reg add "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here\command" /d "\"%LOCALAPPDATA%\Microsoft\WindowsApps\flute.exe\" new \"%%V\"" /f
@@ -0,0 +1,3 @@
reg delete "HKCU\Software\Classes\Directory\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\Directory\Background\shell\Open Fluent Terminal here" /f
reg delete "HKCU\Software\Classes\LibraryFolder\Background\shell\Open Fluent Terminal here" /f
@@ -0,0 +1,157 @@
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.Models.Enums;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class DefaultValueProviderTests
{
public static IEnumerable<object[]> Commands => System.Enum.GetValues(typeof(Command)).Cast<Command>().Select(x => new object[] { x });
[Fact]
public void GetDefaultApplicationSettings_Default_DoesNotReturnNull()
{
var defaultValueProvider = new DefaultValueProvider();
var applicationSettings = defaultValueProvider.GetDefaultApplicationSettings();
applicationSettings.Should().NotBeNull();
}
[Theory]
[MemberData(nameof(Commands))]
public void GetDefaultKeyBindings_AllCommands_ReturnsANonEmptyList(Command command)
{
var defaultValueProvider = new DefaultValueProvider();
var keyBindings = defaultValueProvider.GetDefaultKeyBindings(command);
keyBindings.Should().NotBeNullOrEmpty();
}
[Fact]
public void GetDefaultShellProfileId_Default_DoesNotReturnEmptyGuid()
{
var defaultValueProvider = new DefaultValueProvider();
var defaultShellProfileId = defaultValueProvider.GetDefaultShellProfileId();
defaultShellProfileId.Should().NotBeEmpty();
}
[Fact]
public void GetDefaultShellProfileId_Default_IdIsOneOfThePreInstalledShellProfiles()
{
var defaultValueProvider = new DefaultValueProvider();
var shellProfiles = defaultValueProvider.GetPreinstalledShellProfiles();
var defaultShellProfileId = defaultValueProvider.GetDefaultShellProfileId();
shellProfiles.Select(x => x.Id).Should().Contain(defaultShellProfileId);
}
[Fact]
public void GetDefaultTerminalOptions_Default_DoesNotReturnNull()
{
var defaultValueProvider = new DefaultValueProvider();
var defaultTerminalOptions = defaultValueProvider.GetDefaultTerminalOptions();
defaultTerminalOptions.Should().NotBeNull();
}
[Fact]
public void GetDefaultThemeId_Default_DoesNotReturnEmptyGuid()
{
var defaultValueProvider = new DefaultValueProvider();
var defaultThemeId = defaultValueProvider.GetDefaultThemeId();
defaultThemeId.Should().NotBeEmpty();
}
[Fact]
public void GetDefaultThemeId_Default_IdIsOneOfThePreInstalledThemes()
{
var defaultValueProvider = new DefaultValueProvider();
var themes = defaultValueProvider.GetPreInstalledThemes();
var defaultThemeId = defaultValueProvider.GetDefaultThemeId();
themes.Select(x => x.Id).Should().Contain(defaultThemeId);
}
[Fact]
public void GetPreinstalledShellProfiles_Default_ReturnsANonEmptyList()
{
var defaultValueProvider = new DefaultValueProvider();
var shellProfiles = defaultValueProvider.GetPreinstalledShellProfiles();
shellProfiles.Should().NotBeNullOrEmpty();
}
[Fact]
public void GetPreinstalledShellProfiles_Default_HaveUniqueIds()
{
var defaultValueProvider = new DefaultValueProvider();
var shellProfiles = defaultValueProvider.GetPreinstalledShellProfiles();
shellProfiles.Select(x => x.Id).Should().OnlyHaveUniqueItems();
}
[Fact]
public void GetPreinstalledShellProfiles_Default_HavePreInstalledSetToTrue()
{
var defaultValueProvider = new DefaultValueProvider();
var shellProfiles = defaultValueProvider.GetPreinstalledShellProfiles();
shellProfiles.Select(x => x.PreInstalled).Should().AllBeEquivalentTo(true);
}
[Fact]
public void GetPreinstalledShellProfiles_Default_HaveNonEmptyLocations()
{
var defaultValueProvider = new DefaultValueProvider();
var shellProfiles = defaultValueProvider.GetPreinstalledShellProfiles();
shellProfiles.Select(x => x.Location).Should().NotBeNullOrEmpty();
}
[Fact]
public void GetPreInstalledThemes_Default_ReturnsANonEmptyList()
{
var defaultValueProvider = new DefaultValueProvider();
var themes = defaultValueProvider.GetPreInstalledThemes();
themes.Should().NotBeNullOrEmpty();
}
[Fact]
public void GetPreinstalledThemes_Default_HaveUniqueIds()
{
var defaultValueProvider = new DefaultValueProvider();
var themes = defaultValueProvider.GetPreInstalledThemes();
themes.Select(x => x.Id).Should().OnlyHaveUniqueItems();
}
[Fact]
public void GetPreinstalledThemes_Default_HavePreInstalledSetToTrue()
{
var defaultValueProvider = new DefaultValueProvider();
var themes = defaultValueProvider.GetPreInstalledThemes();
themes.Select(x => x.PreInstalled).Should().AllBeEquivalentTo(true);
}
}
}
@@ -0,0 +1,98 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.App.Services.Implementation;
using Moq;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class DialogServiceTests
{
private readonly Fixture _fixture;
public DialogServiceTests()
{
_fixture = new Fixture();
}
[Fact]
public void ShowCreateKeyBindingDialog_Default_UsesCreateKeyBindingDialog()
{
var createKeyBindingDialog = new Mock<ICreateKeyBindingDialog>();
var dialogService = new DialogService(Mock.Of<IMessageDialog>, () => createKeyBindingDialog.Object, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowCreateKeyBindingDialog();
createKeyBindingDialog.Verify(x => x.CreateKeyBinding(), Times.Once);
}
[Fact]
public void ShowMessageDialogAsnyc_TitleIsEmpty_ThrowsArgumentNullException()
{
var title = string.Empty;
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
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.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("title");
}
[Fact]
public void ShowMessageDialogAsnyc_ContentIsEmpty_ThrowsArgumentNullException()
{
var title = _fixture.Create<string>();
var content = string.Empty;
var buttons = _fixture.CreateMany<DialogButton>(2);
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.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
showMessageDialogAsnyc.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("content");
}
[Fact]
public void ShowMessageDialogAsnyc_NoButtonsPassed_ThrowsArgumentException()
{
var title = _fixture.Create<string>();
var content = _fixture.Create<string>();
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.ShowMessageDialogAsync(title, content);
showMessageDialogAsnyc.Should().Throw<ArgumentException>().And.ParamName.Should().Be("buttons");
}
[Fact]
public void ShowMessageDialogAsnyc_Default_UsesMessageDialog()
{
var title = _fixture.Create<string>();
var content = _fixture.Create<string>();
var buttons = _fixture.CreateMany<DialogButton>(2);
var messageDialog = new Mock<IMessageDialog>();
var dialogService = new DialogService(() => messageDialog.Object, Mock.Of<ICreateKeyBindingDialog>, Mock.Of<IInputDialog>, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowMessageDialogAsync(title, content, buttons.ElementAt(0), buttons.ElementAt(1));
messageDialog.Verify(x => x.ShowAsync(), Times.Once);
}
[Fact]
public void ShowInputDialogAsync_UsesIInputDialogSetTitle()
{
var title = "title";
var inputDialog = new Mock<IInputDialog>();
var dialogService = new DialogService(Mock.Of<IMessageDialog>, Mock.Of<ICreateKeyBindingDialog>, () => inputDialog.Object, Mock.Of<ISshConnectionInfoDialog>, Mock.Of<ICustomCommandDialog>, Mock.Of<IAboutDialog>);
dialogService.ShowInputDialogAsync(title);
inputDialog.Verify(x => x.SetTitle(title), Times.Once);
inputDialog.Verify(x => x.GetInput(), Times.Once);
}
}
}
@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<None Remove="TestData\AdventureTime.itermcolors" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="FluentAssertions" Version="5.10.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="TestData\AdventureTime.itermcolors">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.FoundationContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,80 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.Models;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class FluentTerminalThemeParserTests
{
private readonly Fixture _fixture;
public FluentTerminalThemeParserTests()
{
_fixture = new Fixture();
}
[Fact]
public async Task Parse_ValidFileStream_ReturnsTheme()
{
var theme = _fixture.Create<TerminalTheme>();
var serialized = JsonConvert.SerializeObject(theme);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized));
var parser = new FluentTerminalThemeParser();
var parsedTheme = await parser.Parse(theme.Name, stream);
parsedTheme.Name.Should().Be(theme.Name);
parsedTheme.Author.Should().Be(theme.Author);
parsedTheme.PreInstalled.Should().Be(false);
parsedTheme.Colors.Should().BeEquivalentTo(theme.Colors);
}
[Fact]
public void Parse_InvalidFileStream_ThrowsException()
{
var fileName = _fixture.Create<string>();
var serialized = _fixture.Create<string>();
var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized));
var parser = new FluentTerminalThemeParser();
Func<Task<TerminalTheme>> parse = () => parser.Parse(fileName, stream);
parse.Should().Throw<Exception>();
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Parse_InvalidFileName_ThrowsArgumentNullException(string fileName)
{
var theme = _fixture.Create<TerminalTheme>();
var serialized = JsonConvert.SerializeObject(theme);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized));
var parser = new FluentTerminalThemeParser();
Func<Task<TerminalTheme>> parse = () => parser.Parse(fileName, stream);
parse.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("fileName");
}
[Fact]
public void Parse_FileStreamIsNull_ThrowsArgumentNullException()
{
var fileName = _fixture.Create<string>();
Stream stream = null;
var parser = new FluentTerminalThemeParser();
Func<Task<TerminalTheme>> parse = () => parser.Parse(fileName, stream);
parse.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("fileContent");
}
}
}
@@ -0,0 +1,91 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.Models;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class ITermThemeParserTests
{
private readonly Fixture _fixture;
public ITermThemeParserTests()
{
_fixture = new Fixture();
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Parse_InvalidFileName_ThrowsArgumentNullException(string fileName)
{
var stream = typeof(ITermThemeParserTests).Assembly.GetManifestResourceStream("FluentTerminal.App.Services.Test.TestData.AdventureTime.itermcolors");
var parser = new ITermThemeParser();
Func<Task<TerminalTheme>> parse = () => parser.Parse(fileName, stream);
parse.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("fileName");
}
[Fact]
public void Parse_FileStreamIsNull_ThrowsArgumentNullException()
{
var fileName = _fixture.Create<string>();
Stream stream = null;
var parser = new ITermThemeParser();
Func<Task<TerminalTheme>> parse = () => parser.Parse(fileName, stream);
parse.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("fileContent");
}
[Fact]
public void Parse_InvalidFileStream_ThrowsException()
{
var fileName = _fixture.Create<string>();
var serialized = _fixture.Create<string>();
var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized));
var parser = new ITermThemeParser();
Func<Task<TerminalTheme>> parse = () => parser.Parse(fileName, stream);
parse.Should().Throw<Exception>();
}
[Fact]
public async Task Parse_ValidThemeFile_ParsesCorrectly()
{
var fileName = _fixture.Create<string>();
var stream = typeof(ITermThemeParserTests).Assembly.GetManifestResourceStream("FluentTerminal.App.Services.Test.TestData.AdventureTime.itermcolors");
var parser = new ITermThemeParser();
var theme = await parser.Parse(fileName, stream);
theme.Colors.Background.ToLower().Should().Be("#1f1d45");
theme.Colors.Cursor.ToLower().Should().Be("#efbf38");
theme.Colors.Foreground.ToLower().Should().Be("#f8dcc0");
theme.Colors.Black.ToLower().Should().Be("#050404");
theme.Colors.Red.ToLower().Should().Be("#bd0013");
theme.Colors.Green.ToLower().Should().Be("#4ab118");
theme.Colors.Yellow.ToLower().Should().Be("#e7741e");
theme.Colors.Blue.ToLower().Should().Be("#0f4ac6");
theme.Colors.Magenta.ToLower().Should().Be("#665993");
theme.Colors.Cyan.ToLower().Should().Be("#70a598");
theme.Colors.White.ToLower().Should().Be("#f8dcc0");
theme.Colors.BrightBlack.ToLower().Should().Be("#4e7cbf");
theme.Colors.BrightRed.ToLower().Should().Be("#fc5f5a");
theme.Colors.BrightGreen.ToLower().Should().Be("#9eff6e");
theme.Colors.BrightYellow.ToLower().Should().Be("#efc11a");
theme.Colors.BrightBlue.ToLower().Should().Be("#1997c6");
theme.Colors.BrightMagenta.ToLower().Should().Be("#9b5953");
theme.Colors.BrightCyan.ToLower().Should().Be("#c8faf4");
theme.Colors.BrightWhite.ToLower().Should().Be("#f6f5fb");
}
}
}
@@ -0,0 +1,115 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class KeyboardCommandServiceTests
{
private readonly Fixture _fixture;
public KeyboardCommandServiceTests()
{
_fixture = new Fixture();
}
[Fact]
public void RegisterCommandHandler_HandlerIsNull_ThrowsArgumentNullException()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
Action handler = null;
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.RegisterCommandHandler(command, handler);
invoke.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("handler");
}
[Fact]
public void RegisterCommandHandler_CommandAlreadyRegisted_ThrowsInvalidOperationException()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var handler = _fixture.Create<Action>();
var keyboardCommandService = new KeyboardCommandService();
keyboardCommandService.RegisterCommandHandler(command, handler);
Action invoke = () => keyboardCommandService.RegisterCommandHandler(command, handler);
invoke.Should().Throw<InvalidOperationException>();
}
[Fact]
public void DeregisterCommandHandler_CommandIsNull_ThrowsArgumentNullException()
{
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(null);
invoke.Should().Throw<ArgumentNullException>();
}
[Fact]
public void DeregisterCommandHandler_CommandRegistered_HandlerGetsRemoved()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var handler = _fixture.Create<Action>();
var keyboardCommandService = new KeyboardCommandService();
keyboardCommandService.RegisterCommandHandler(command, handler);
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact]
public void DeregisterCommandHandler_CommandNotRegistered_ShouldNotThrow()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.DeregisterCommandHandler(command);
invoke.Should().NotThrow();
}
[Fact]
public void SendCommand_CommandIsRegistered_HandlerGetsInvoked()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var handlerCalled = false;
var keyboardCommandService = new KeyboardCommandService();
keyboardCommandService.RegisterCommandHandler(command, () => handlerCalled = true);
keyboardCommandService.SendCommand(command);
handlerCalled.Should().BeTrue();
}
[Fact]
public void SendCommand_CommandIsNotRegisted_KeyNotFoundExceptionIsThrown()
{
var command = ((Command)new Random().Next(1, Enum.GetValues(typeof(Command)).Length)).ToString();
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.SendCommand(command);
invoke.Should().Throw<KeyNotFoundException>();
}
[Fact]
public void SendCommand_CommandIsToggleWindow_ShouldNotThrow()
{
var command = nameof(Command.ToggleWindow);
var keyboardCommandService = new KeyboardCommandService();
Action invoke = () => keyboardCommandService.SendCommand(command);
invoke.Should().NotThrow();
}
}
}
@@ -0,0 +1,793 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Moq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class SettingsServiceTests
{
private readonly Fixture _fixture;
public SettingsServiceTests()
{
_fixture = new Fixture();
}
[Fact]
public void Constructor_Default_WritesAllPreinstalledThemesInThemeContainer()
{
var themes = _fixture.CreateMany<TerminalTheme>(3);
var defaultValueProvider = new Mock<IDefaultValueProvider>();
defaultValueProvider.Setup(x => x.GetPreInstalledThemes()).Returns(themes);
var themesContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
Themes = themesContainer.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
new SettingsService(defaultValueProvider.Object, applicationDataContainers);
defaultValueProvider.Verify(x => x.GetPreInstalledThemes(), Times.Once);
foreach (var theme in themes)
{
themesContainer.Verify(x => x.WriteValueAsJson(theme.Id.ToString(), theme), Times.Once);
}
}
[Fact]
public void Constructor_Default_WritesAllPreinstalledShellProfilesInShellProfilesContainer()
{
var shellProfiles = _fixture.CreateMany<ShellProfile>(3);
var defaultValueProvider = new Mock<IDefaultValueProvider>();
defaultValueProvider.Setup(x => x.GetPreinstalledShellProfiles()).Returns(shellProfiles);
var shellProfilesContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
new SettingsService(defaultValueProvider.Object, applicationDataContainers);
defaultValueProvider.Verify(x => x.GetPreinstalledShellProfiles(), Times.Once);
foreach (var shellProfile in shellProfiles)
{
shellProfilesContainer.Verify(x => x.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile), Times.Once);
}
}
[Fact]
public void GetCurrentThemeId_ValueExistsInRoamingSettings_ReturnsCurrentThemeId()
{
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var currentThemeId = (object)_fixture.Create<Guid>();
roamingSettings.Setup(x => x.TryGetValue(SettingsService.CurrentThemeKey, out currentThemeId)).Returns(true);
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
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);
var result = settingsService.GetCurrentThemeId();
result.Should().Be((Guid)currentThemeId);
roamingSettings.Verify(x => x.TryGetValue(SettingsService.CurrentThemeKey, out currentThemeId), Times.Once);
}
[Fact]
public void GetCurrentThemeId_ValueDoesNotExistInRoamingSettings_ReturnsDefaultThemeIdFromDefaultValueProvider()
{
var defaultValueProvider = new Mock<IDefaultValueProvider>();
var defaultThemeId = _fixture.Create<Guid>();
defaultValueProvider.Setup(x => x.GetDefaultThemeId()).Returns(defaultThemeId);
var roamingSettings = new Mock<IApplicationDataContainer>();
var currentThemeId = (object)_fixture.Create<Guid>();
roamingSettings.Setup(x => x.TryGetValue(SettingsService.CurrentThemeKey, out currentThemeId)).Returns(false);
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
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);
var result = settingsService.GetCurrentThemeId();
result.Should().Be(defaultThemeId);
defaultValueProvider.Verify(x => x.GetDefaultThemeId(), Times.Once);
roamingSettings.Verify(x => x.TryGetValue(SettingsService.CurrentThemeKey, out currentThemeId), Times.Once);
}
[Fact]
public void SaveCurrentThemeId_Default_SetsValueOnRoamingSettings()
{
var currentThemeId = _fixture.Create<Guid>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
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);
settingsService.SaveCurrentThemeId(currentThemeId);
roamingSettings.Verify(x => x.SetValue(SettingsService.CurrentThemeKey, currentThemeId), Times.Once);
}
[Fact]
public void SaveCurrentThemeId_Default_InvokesCurrentThemeChangedEvent()
{
var currentThemeId = _fixture.Create<Guid>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var currentThemeChangedEventInvoked = false;
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
settingsService.SaveCurrentThemeId(currentThemeId);
currentThemeChangedEventInvoked.Should().BeTrue();
}
[Fact]
public void SaveTheme_Default_WritesToThemesContainer()
{
var theme = _fixture.Create<TerminalTheme>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var themesContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
Themes = themesContainer.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.SaveTheme(theme);
themesContainer.Verify(x => x.WriteValueAsJson(theme.Id.ToString(), theme), Times.Once);
}
[Fact]
public void SaveTheme_ThemeIsCurrentTheme_InvokesCurrentThemeChangedEvent()
{
var theme = _fixture.Create<TerminalTheme>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var themesContainer = new Mock<IApplicationDataContainer>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var currentThemeId = (object)theme.Id;
roamingSettings.Setup(x => x.TryGetValue(SettingsService.CurrentThemeKey, out currentThemeId)).Returns(true);
var currentThemeChangedEventInvoked = false;
var applicationDataContainers = new ApplicationDataContainers
{
Themes = themesContainer.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = roamingSettings.Object,
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<CurrentThemeChangedMessage>(this,
message => currentThemeChangedEventInvoked = true);
settingsService.SaveTheme(theme);
currentThemeChangedEventInvoked.Should().BeTrue();
}
[Fact]
public void DeleteTheme_Default_CallsDeleteOnThemesContainer()
{
var themeId = _fixture.Create<Guid>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var themesContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
Themes = themesContainer.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.DeleteTheme(themeId);
themesContainer.Verify(x => x.Delete(themeId.ToString()), Times.Once);
}
[Fact]
public void GetThemes_Default_CallsGetAllOnThemesContainer()
{
var themes = _fixture.CreateMany<TerminalTheme>(3);
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var themesContainer = new Mock<IApplicationDataContainer>();
themesContainer.Setup(x => x.GetAll()).Returns(themes.Select(JsonConvert.SerializeObject));
var applicationDataContainers = new ApplicationDataContainers
{
Themes = themesContainer.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetThemes();
result.Should().BeEquivalentTo(themes);
themesContainer.Verify(x => x.GetAll(), Times.Once);
}
[Fact]
public void GetTheme_ValidThemeId_ReturnsThemeFromThemesContainer()
{
var theme = _fixture.Create<TerminalTheme>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var themesContainer = new Mock<IApplicationDataContainer>();
themesContainer.Setup(x => x.ReadValueFromJson(theme.Id.ToString(), default(TerminalTheme))).Returns(theme);
var applicationDataContainers = new ApplicationDataContainers
{
Themes = themesContainer.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetTheme(theme.Id);
result.Should().BeEquivalentTo(theme);
themesContainer.Verify(x => x.ReadValueFromJson(theme.Id.ToString(), default(TerminalTheme)), Times.Once);
}
[Fact]
public void GetTheme_UnknownThemeId_ReturnsNull()
{
var themeId = _fixture.Create<Guid>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var themesContainer = new Mock<IApplicationDataContainer>();
themesContainer.Setup(x => x.ReadValueFromJson(themeId.ToString(), default(TerminalTheme))).Returns(value: null);
var applicationDataContainers = new ApplicationDataContainers
{
Themes = themesContainer.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetTheme(themeId);
result.Should().BeNull();
themesContainer.Verify(x => x.ReadValueFromJson(themeId.ToString(), default(TerminalTheme)), Times.Once);
}
[Fact]
public void GetTerminalOptions_Default_ReturnsTerminalOptionsFromRoamingSettings()
{
var terminalOptions = _fixture.Create<TerminalOptions>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
roamingSettings.Setup(x => x.ReadValueFromJson(nameof(TerminalOptions), It.IsAny<TerminalOptions>())).Returns(terminalOptions);
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetTerminalOptions();
result.Should().BeEquivalentTo(terminalOptions);
roamingSettings.Verify(x => x.ReadValueFromJson(nameof(TerminalOptions), It.IsAny<TerminalOptions>()), Times.Once);
}
[Fact]
public void SaveTerminalOptions_Default_WritesToRoamingSettings()
{
var terminalOptions = _fixture.Create<TerminalOptions>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.SaveTerminalOptions(terminalOptions);
roamingSettings.Verify(x => x.WriteValueAsJson(nameof(TerminalOptions), terminalOptions), Times.Once);
}
[Fact]
public void SaveTerminalOptions_Default_InvokesTerminalOptionsChangedEvent()
{
var terminalOptions = _fixture.Create<TerminalOptions>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var terminalOptionsChangedEventInvoked = false;
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<TerminalOptionsChangedMessage>(this,
message => terminalOptionsChangedEventInvoked = true);
settingsService.SaveTerminalOptions(terminalOptions);
terminalOptionsChangedEventInvoked.Should().BeTrue();
}
[Fact]
public void GetApplicationSettings_Default_ReturnsApplicationSettingsFromRoamingSettings()
{
var applicationSettings = _fixture.Create<ApplicationSettings>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
roamingSettings.Setup(x => x.ReadValueFromJson(nameof(ApplicationSettings), It.IsAny<ApplicationSettings>())).Returns(applicationSettings);
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetApplicationSettings();
result.Should().BeEquivalentTo(applicationSettings);
roamingSettings.Verify(x => x.ReadValueFromJson(nameof(ApplicationSettings), It.IsAny<ApplicationSettings>()), Times.Once);
}
[Fact]
public void SaveApplicationSettings_Default_WritesToRoamingSettings()
{
var applicationSettings = _fixture.Create<ApplicationSettings>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.SaveApplicationSettings(applicationSettings);
roamingSettings.Verify(x => x.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings), Times.Once);
}
[Fact]
public void SaveApplicationSettings_Default_InvokesApplicationSettingsChangedEvent()
{
var applicationSettings = _fixture.Create<ApplicationSettings>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var applicationSettingsChangedEventInvoked = false;
var applicationDataContainers = new ApplicationDataContainers
{
RoamingSettings = roamingSettings.Object,
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<ApplicationSettingsChangedMessage>(this,
a => applicationSettingsChangedEventInvoked = true);
settingsService.SaveApplicationSettings(applicationSettings);
applicationSettingsChangedEventInvoked.Should().BeTrue();
}
[Fact]
public void GetKeyBindings_KeyBindingsInKeyBindingsContainer_ReturnsKeyBindingsFromKeyBindingsContainer()
{
var commands = _fixture.CreateMany<Command>(5);
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
foreach (var command in commands)
{
keyBindingsContainer.Setup(x => x.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null)).Returns(new Collection<KeyBinding> { });
}
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetCommandKeyBindings();
foreach (var command in commands)
{
keyBindingsContainer.Verify(x => x.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null), Times.Once);
}
}
[Fact]
public void GetKeyBindings_KeyBindingsNotInKeyBindingsContainer_ReturnsKeyBindingsFromDefaultValueProvider()
{
var commands = _fixture.CreateMany<Command>(5);
var defaultValueProvider = new Mock<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
var result = settingsService.GetCommandKeyBindings();
foreach (Command command in Enum.GetValues(typeof(Command)))
{
defaultValueProvider.Verify(x => x.GetDefaultKeyBindings(command), Times.Once);
}
}
[Fact]
public void SaveKeyBindings_Default_WritesToKeyBindingsContainer()
{
var command = _fixture.Create<Command>();
var keyBindings = _fixture.Create<ICollection<KeyBinding>>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
keyBindingsContainer.Verify(x => x.WriteValueAsJson(command.ToString(), keyBindings), Times.Once);
}
[Fact]
public void SaveKeyBindings_Default_InvokesKeyBindingsChangedEvent()
{
var command = _fixture.Create<Command>();
var keyBindings = _fixture.Create<Collection<KeyBinding>>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
var keyBindingsChangedEventInvoked = false;
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
settingsService.SaveKeyBindings(command.ToString(), keyBindings);
keyBindingsChangedEventInvoked.Should().BeTrue();
}
[Fact]
public void ResetKeyBindings_Default_WritesToKeyBindingsContainerFromDefaultValueProvider()
{
var defaultValueProvider = new Mock<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
settingsService.ResetKeyBindings();
foreach (Command command in Enum.GetValues(typeof(Command)))
{
keyBindingsContainer.Verify(x => x.WriteValueAsJson(command.ToString(), It.IsAny<ICollection<KeyBinding>>()), Times.Once);
defaultValueProvider.Verify(x => x.GetDefaultKeyBindings(command), Times.Once);
}
}
[Fact]
public void ResetKeyBindings_Default_InvokesKeyBindingsChangedEvent()
{
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var keyBindingsContainer = new Mock<IApplicationDataContainer>();
var keyBindingsChangedEventInvoked = false;
var applicationDataContainers = new ApplicationDataContainers
{
KeyBindings = keyBindingsContainer.Object,
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>(),
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
Messenger.Default.Register<KeyBindingsChangedMessage>(this,
message => keyBindingsChangedEventInvoked = true);
settingsService.ResetKeyBindings();
keyBindingsChangedEventInvoked.Should().BeTrue();
}
[Fact]
public void GetDefaultShellProfileId_ValueExistsInRoamingSettings_ReturnsDefaultShellProfileId()
{
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var localSettings = new Mock<IApplicationDataContainer>();
var defaultShellProfileId = (object)_fixture.Create<Guid>();
localSettings.Setup(x => x.TryGetValue(SettingsService.DefaultShellProfileKey, out defaultShellProfileId)).Returns(true);
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = localSettings.Object,
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);
var result = settingsService.GetDefaultShellProfileId();
result.Should().Be((Guid)defaultShellProfileId);
localSettings.Verify(x => x.TryGetValue(SettingsService.DefaultShellProfileKey, out defaultShellProfileId), Times.Once);
}
[Fact]
public void GetDefaultShellProfileId_ValueDoesNotExistInRoamingSettings_ReturnsDefaultShellProfileIdFromDefaultValueProvider()
{
var defaultValueProvider = new Mock<IDefaultValueProvider>();
var defaultShellProfileId = _fixture.Create<Guid>();
defaultValueProvider.Setup(x => x.GetDefaultShellProfileId()).Returns(defaultShellProfileId);
var localSettings = new Mock<IApplicationDataContainer>();
var currentDefaultShellProfileId = (object)_fixture.Create<Guid>();
localSettings.Setup(x => x.TryGetValue(SettingsService.DefaultShellProfileKey, out currentDefaultShellProfileId)).Returns(false);
var applicationDataContainers = new ApplicationDataContainers
{
LocalSettings = localSettings.Object,
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);
var result = settingsService.GetDefaultShellProfileId();
result.Should().Be(defaultShellProfileId);
defaultValueProvider.Verify(x => x.GetDefaultShellProfileId(), Times.Once);
localSettings.Verify(x => x.TryGetValue(SettingsService.DefaultShellProfileKey, out currentDefaultShellProfileId), Times.Once);
}
[Fact]
public void GetShellProfiles_Default_CallsGetAllOnShellProfilesContainer()
{
var shellProfiles = _fixture.CreateMany<ShellProfile>(3);
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var shellProfilesContainer = new Mock<IApplicationDataContainer>();
shellProfilesContainer.Setup(x => x.GetAll()).Returns(shellProfiles.Select(JsonConvert.SerializeObject));
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
var result = settingsService.GetShellProfiles();
result.Select(p => p.EqualTo(shellProfiles.FirstOrDefault(x => x.Id == p.Id)).Should().BeTrue());
shellProfilesContainer.Verify(x => x.GetAll(), Times.Once);
}
[Fact]
public void SaveShellProfile_Default_WritesToShellProfilesContainer()
{
var shellProfile = _fixture.Create<ShellProfile>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var shellProfilesContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.SaveShellProfile(shellProfile);
shellProfilesContainer.Verify(x => x.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile), Times.Once);
}
[Fact]
public void DeleteShellProfile_Default_CallsDeleteOnShellProfilesContainer()
{
var shellProfileId = _fixture.Create<Guid>();
var defaultValueProvider = Mock.Of<IDefaultValueProvider>();
var shellProfilesContainer = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
Themes = Mock.Of<IApplicationDataContainer>()
};
var settingsService = new SettingsService(defaultValueProvider, applicationDataContainers);
settingsService.DeleteShellProfile(shellProfileId);
shellProfilesContainer.Verify(x => x.Delete(shellProfileId.ToString()), Times.Once);
}
[Fact]
public void GetDefaultShellProfile_ProfileNotFound_DefaultShellProfileIdGetsResetToDefault()
{
var currentShellProfileId = (object)_fixture.Create<Guid>();
var defaultShellProfile = _fixture.Create<ShellProfile>();
var defaultValueProvider = new Mock<IDefaultValueProvider>();
defaultValueProvider.Setup(x => x.GetDefaultShellProfileId()).Returns(defaultShellProfile.Id);
var shellProfilesContainer = new Mock<IApplicationDataContainer>();
var localSettings = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = shellProfilesContainer.Object,
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = localSettings.Object,
Themes = Mock.Of<IApplicationDataContainer>()
};
shellProfilesContainer.Setup(x => x.ReadValueFromJson(currentShellProfileId.ToString(), default(ShellProfile))).Returns(value: null);
shellProfilesContainer.Setup(x => x.ReadValueFromJson(defaultShellProfile.Id.ToString(), default(ShellProfile))).Returns(defaultShellProfile);
localSettings.Setup(x => x.TryGetValue(SettingsService.DefaultShellProfileKey, out currentShellProfileId)).Returns(true);
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
var result = settingsService.GetDefaultShellProfile();
result.Should().Be(defaultShellProfile);
localSettings.Verify(x => x.SetValue(SettingsService.DefaultShellProfileKey, defaultShellProfile.Id), Times.Once);
}
[Fact]
public void GetCurrentTheme_ThemeNotFound_CurrentThemeIdGetsResetToDefault()
{
var currentThemeId = (object)_fixture.Create<Guid>();
var defaultTheme = _fixture.Create<TerminalTheme>();
var defaultValueProvider = new Mock<IDefaultValueProvider>();
defaultValueProvider.Setup(x => x.GetDefaultThemeId()).Returns(defaultTheme.Id);
var themesContainer = new Mock<IApplicationDataContainer>();
var roamingSettings = new Mock<IApplicationDataContainer>();
var applicationDataContainers = new ApplicationDataContainers
{
ShellProfiles = Mock.Of<IApplicationDataContainer>(),
SshProfiles = Mock.Of<IApplicationDataContainer>(),
KeyBindings = Mock.Of<IApplicationDataContainer>(),
LocalSettings = Mock.Of<IApplicationDataContainer>(),
RoamingSettings = roamingSettings.Object,
Themes = themesContainer.Object
};
themesContainer.Setup(x => x.ReadValueFromJson(currentThemeId.ToString(), default(TerminalTheme))).Returns(value: null);
themesContainer.Setup(x => x.ReadValueFromJson(defaultTheme.Id.ToString(), default(TerminalTheme))).Returns(defaultTheme);
roamingSettings.Setup(x => x.TryGetValue(SettingsService.CurrentThemeKey, out currentThemeId)).Returns(true);
var settingsService = new SettingsService(defaultValueProvider.Object, applicationDataContainers);
var result = settingsService.GetCurrentTheme();
result.Should().Be(defaultTheme);
roamingSettings.Verify(x => x.SetValue(SettingsService.CurrentThemeKey, defaultTheme.Id), Times.Once);
}
}
}
@@ -0,0 +1,213 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Ansi 0 Color</key>
<dict>
<key>Blue Component</key>
<real>0.016337040811777115</real>
<key>Green Component</key>
<real>0.016232240945100784</real>
<key>Red Component</key>
<real>0.020433688536286354</real>
</dict>
<key>Ansi 1 Color</key>
<dict>
<key>Blue Component</key>
<real>0.075083047151565552</real>
<key>Green Component</key>
<real>0.0</real>
<key>Red Component</key>
<real>0.73976802825927734</real>
</dict>
<key>Ansi 10 Color</key>
<dict>
<key>Blue Component</key>
<real>0.43171143531799316</real>
<key>Green Component</key>
<real>1</real>
<key>Red Component</key>
<real>0.61819148063659668</real>
</dict>
<key>Ansi 11 Color</key>
<dict>
<key>Blue Component</key>
<real>0.10242157429456711</real>
<key>Green Component</key>
<real>0.7578997015953064</real>
<key>Red Component</key>
<real>0.93761706352233887</real>
</dict>
<key>Ansi 12 Color</key>
<dict>
<key>Blue Component</key>
<real>0.77801346778869629</real>
<key>Green Component</key>
<real>0.59102588891983032</real>
<key>Red Component</key>
<real>0.096501544117927551</real>
</dict>
<key>Ansi 13 Color</key>
<dict>
<key>Blue Component</key>
<real>0.32394066452980042</real>
<key>Green Component</key>
<real>0.34950375556945801</real>
<key>Red Component</key>
<real>0.60686224699020386</real>
</dict>
<key>Ansi 14 Color</key>
<dict>
<key>Blue Component</key>
<real>0.95550096035003662</real>
<key>Green Component</key>
<real>0.97915208339691162</real>
<key>Red Component</key>
<real>0.78500419855117798</real>
</dict>
<key>Ansi 15 Color</key>
<dict>
<key>Blue Component</key>
<real>0.98438382148742676</real>
<key>Green Component</key>
<real>0.96043074131011963</real>
<key>Red Component</key>
<real>0.96449893712997437</real>
</dict>
<key>Ansi 2 Color</key>
<dict>
<key>Blue Component</key>
<real>0.092498354613780975</real>
<key>Green Component</key>
<real>0.6950421929359436</real>
<key>Red Component</key>
<real>0.28945007920265198</real>
</dict>
<key>Ansi 3 Color</key>
<dict>
<key>Blue Component</key>
<real>0.1164485365152359</real>
<key>Green Component</key>
<real>0.45553147792816162</real>
<key>Red Component</key>
<real>0.90543299913406372</real>
</dict>
<key>Ansi 4 Color</key>
<dict>
<key>Blue Component</key>
<real>0.77829605340957642</real>
<key>Green Component</key>
<real>0.28880593180656433</real>
<key>Red Component</key>
<real>0.059818677604198456</real>
</dict>
<key>Ansi 5 Color</key>
<dict>
<key>Blue Component</key>
<real>0.57602518796920776</real>
<key>Green Component</key>
<real>0.35004982352256775</real>
<key>Red Component</key>
<real>0.40191537141799927</real>
</dict>
<key>Ansi 6 Color</key>
<dict>
<key>Blue Component</key>
<real>0.59465551376342773</real>
<key>Green Component</key>
<real>0.64633101224899292</real>
<key>Red Component</key>
<real>0.43781715631484985</real>
</dict>
<key>Ansi 7 Color</key>
<dict>
<key>Blue Component</key>
<real>0.75428962707519531</real>
<key>Green Component</key>
<real>0.86175870895385742</real>
<key>Red Component</key>
<real>0.97308963537216187</real>
</dict>
<key>Ansi 8 Color</key>
<dict>
<key>Blue Component</key>
<real>0.7491685152053833</real>
<key>Green Component</key>
<real>0.48549851775169373</real>
<key>Red Component</key>
<real>0.30726605653762817</real>
</dict>
<key>Ansi 9 Color</key>
<dict>
<key>Blue Component</key>
<real>0.35247993469238281</real>
<key>Green Component</key>
<real>0.37095117568969727</real>
<key>Red Component</key>
<real>0.98876720666885376</real>
</dict>
<key>Background Color</key>
<dict>
<key>Blue Component</key>
<real>0.27054747939109802</real>
<key>Green Component</key>
<real>0.11274909973144531</real>
<key>Red Component</key>
<real>0.12003477662801743</real>
</dict>
<key>Bold Color</key>
<dict>
<key>Blue Component</key>
<real>0.075083047151565552</real>
<key>Green Component</key>
<real>0.0</real>
<key>Red Component</key>
<real>0.73976802825927734</real>
</dict>
<key>Cursor Color</key>
<dict>
<key>Blue Component</key>
<real>0.21945112943649292</real>
<key>Green Component</key>
<real>0.74987590312957764</real>
<key>Red Component</key>
<real>0.93704277276992798</real>
</dict>
<key>Cursor Text Color</key>
<dict>
<key>Blue Component</key>
<real>0.037519864737987518</real>
<key>Green Component</key>
<real>0.032785907387733459</real>
<key>Red Component</key>
<real>0.02988031692802906</real>
</dict>
<key>Foreground Color</key>
<dict>
<key>Blue Component</key>
<real>0.75428962707519531</real>
<key>Green Component</key>
<real>0.86175870895385742</real>
<key>Red Component</key>
<real>0.97308963537216187</real>
</dict>
<key>Selected Text Color</key>
<dict>
<key>Blue Component</key>
<real>0.767447829246521</real>
<key>Green Component</key>
<real>0.85237473249435425</real>
<key>Red Component</key>
<real>0.95287787914276123</real>
</dict>
<key>Selection Color</key>
<dict>
<key>Blue Component</key>
<real>0.30699285864830017</real>
<key>Green Component</key>
<real>0.41871801018714905</real>
<key>Red Component</key>
<real>0.43789717555046082</real>
</dict>
</dict>
</plist>
@@ -0,0 +1,45 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using Moq;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class ThemeParserFactoryTests
{
private readonly Fixture _fixture;
public ThemeParserFactoryTests()
{
_fixture = new Fixture();
}
[Fact]
public void GetParser_SupportedFileType_ReturnsParser()
{
var supportedFileType = _fixture.Create<string>();
var parser = new Mock<IThemeParser>();
parser.Setup(x => x.SupportedFileTypes).Returns(new string[] { supportedFileType });
var themeParserFactory = new ThemeParserFactory(new IThemeParser[] { parser.Object });
var result = themeParserFactory.GetParser(supportedFileType);
parser.Should().Be(parser);
}
[Fact]
public void GetParser_UnsupportedFileType_ReturnsNull()
{
var unSupportedFileType = _fixture.Create<string>();
var supportedFileType = _fixture.Create<string>();
var parser = new Mock<IThemeParser>();
parser.Setup(x => x.SupportedFileTypes).Returns(new string[] { supportedFileType });
var themeParserFactory = new ThemeParserFactory(new IThemeParser[] { parser.Object });
var result = themeParserFactory.GetParser(unSupportedFileType);
result.Should().BeNull();
}
}
}
@@ -0,0 +1,179 @@
using AutoFixture;
using FluentAssertions;
using FluentTerminal.App.Services.Implementation;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Requests;
using FluentTerminal.Models.Responses;
using Moq;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
using Xunit;
namespace FluentTerminal.App.Services.Test
{
public class TrayProcessCommunicationServiceTests
{
private readonly Fixture _fixture;
public TrayProcessCommunicationServiceTests()
{
_fixture = new Fixture();
}
[Fact]
public async Task CreateTerminal_Default_ReturnsResponseFromAppServiceConnection()
{
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var response = _fixture.Create<CreateTerminalResponse>();
var responseMessage = new ValueSet
{
[MessageKeys.Type] = _fixture.Create<string>(),
[MessageKeys.Content] = JsonConvert.SerializeObject(response)
};
appServiceConnection.Setup(x => x.SendMessageAsync(It.IsAny<ValueSet>())).Returns(Task.FromResult(responseMessage));
var terminalSize = _fixture.Create<TerminalSize>();
var shellProfile = _fixture.Create<ShellProfile>();
var sessionType = _fixture.Create<SessionType>();
var id = _fixture.Create<byte>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
var result = await trayProcessCommunicationService.CreateTerminalAsync(id, terminalSize, shellProfile, sessionType);
result.Should().BeEquivalentTo(response);
}
[Fact]
public async Task ResizeTerminal_Default_SendsResizeTerminalRequest()
{
var terminalId = _fixture.Create<byte>();
var terminalSize = _fixture.Create<TerminalSize>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.ResizeTerminalAsync(terminalId, terminalSize);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.ResizeTerminalRequest)), Times.Once);
}
[Fact]
public async Task Write_Default_SendsWriteDataRequest()
{
var terminalId = _fixture.Create<byte>();
var data = _fixture.Create<byte[]>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.WriteAsync(terminalId, data);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == Constants.TerminalBufferRequestIdentifier)), Times.Once);
}
[Fact]
public async Task CloseTerminal_Default_SendsTerminalExitedRequest()
{
var terminalId = _fixture.Create<byte>();
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var appServiceConnection = new Mock<IAppServiceConnection>();
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
await trayProcessCommunicationService.CloseTerminalAsync(terminalId);
appServiceConnection.Verify(x => x.SendMessageAsync(It.Is<ValueSet>(d => (byte)d[MessageKeys.Type] == (byte)MessageIdentifiers.TerminalExitedRequest)), Times.Once);
}
[Fact]
public void OnMessageReceived_TerminalExitedRequest_InvokesTerminalExitedEvent()
{
var request = new TerminalExitedRequest(_fixture.Create<byte>(), _fixture.Create<int>());
var receivedExitStatus = (TerminalExitStatus) null;
var message = new ValueSet
{
[MessageKeys.Type] = (byte) MessageIdentifiers.TerminalExitedRequest,
[MessageKeys.Content] = JsonConvert.SerializeObject(request)
};
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
var appServiceConnection = new Mock<IAppServiceConnection>();
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
trayProcessCommunicationService.TerminalExited += (s, status) =>
{
receivedExitStatus = status;
};
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
receivedExitStatus.Should().NotBeNull();
receivedExitStatus.TerminalId.Should().Be(request.TerminalId);
receivedExitStatus.ExitCode.Should().Be(request.ExitCode);
}
[Fact]
public void OnMessageReceived_DisplayTerminalOutputRequest_InvokesCorrectOutputHandler()
{
var terminalId = _fixture.Create<byte>();
var output = _fixture.Create<byte[]>();
var receivedOutput = default(byte[]);
var message = new ValueSet
{
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
[MessageKeys.TerminalId] = terminalId,
[MessageKeys.Content] = output
};
var settingsService = new Mock<ISettingsService>();
var keyBindings = _fixture.CreateMany<KeyBinding>(3);
settingsService.Setup(x => x.GetCommandKeyBindings()).Returns(new Dictionary<string, ICollection<KeyBinding>>
{
[nameof(Command.ToggleWindow)] = keyBindings.ToList()
});
var trayProcessCommunicationService = new TrayProcessCommunicationService(settingsService.Object);
var appServiceConnection = new Mock<IAppServiceConnection>();
trayProcessCommunicationService.Initialize(appServiceConnection.Object);
trayProcessCommunicationService.SubscribeForTerminalOutput(terminalId, o =>
{
receivedOutput = o;
});
appServiceConnection.Raise(x => x.MessageReceived += null, null, message);
receivedOutput.Should().BeEquivalentTo(output);
}
}
}
@@ -0,0 +1,64 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Storage;
using FluentTerminal.App.Services.Utilities;
namespace FluentTerminal.App.Services.Adapters
{
public class ApplicationDataContainerAdapter : IApplicationDataContainer
{
private readonly ApplicationDataContainer _applicationDataContainer;
public ApplicationDataContainerAdapter(ApplicationDataContainer applicationDataContainer)
{
_applicationDataContainer = applicationDataContainer;
}
public void Delete(string key)
{
_applicationDataContainer.Values.Remove(key);
}
public void Clear()
{
_applicationDataContainer.Values.Clear();
}
public IEnumerable<object> GetAll()
{
return _applicationDataContainer.Values.Select(x => x.Value);
}
public T ReadValueFromJson<T>(string name, T fallbackValue)
{
if (_applicationDataContainer.Values.TryGetValue(name, out object value))
{
if (EqualityComparer<T>.Default.Equals(fallbackValue, default))
{
fallbackValue = Activator.CreateInstance<T>();
}
JsonConvert.PopulateObject((string)value, fallbackValue);
}
return fallbackValue;
}
public void SetValue(string key, object value)
{
_applicationDataContainer.Values[key] = value;
}
public bool TryGetValue(string key, out object value)
{
return _applicationDataContainer.Values.TryGetValue(key, out value);
}
public void WriteValueAsJson<T>(string name, T value)
{
_applicationDataContainer.Values[name] = JsonConvert.SerializeObject(value,
PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
}
}
}
@@ -0,0 +1,13 @@
namespace FluentTerminal.App.Services
{
public class ApplicationDataContainers
{
public IApplicationDataContainer LocalSettings { get; set; }
public IApplicationDataContainer RoamingSettings { get; set; }
public IApplicationDataContainer Themes { get; set; }
public IApplicationDataContainer KeyBindings { get; set; }
public IApplicationDataContainer ShellProfiles { get; set; }
public IApplicationDataContainer SshProfiles { get; set; }
public IApplicationDataContainer HistoryContainer { get; set; }
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace FluentTerminal.App.Services
{
public static class Constants
{
public const string ThemesContainerName = "Themes";
public const string KeyBindingsContainerName = "KeyBindings";
public const string ShellProfilesContainerName = "ShellProfiles";
public const string SshProfilesContainerName = "SshProfiles";
public const byte TerminalBufferRequestIdentifier = 0;
public const string SshCommandName = "ssh";
public const string MoshCommandName = "mosh";
public const string ExecutedCommandsContainerName = "HistoryContainer";
public const string TerminalViewModelStateId = "TerminalViewModelState";
}
}
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IAboutDialog
{
Task ShowAsync();
}
}
@@ -0,0 +1,10 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ICreateKeyBindingDialog
{
Task<KeyBinding> CreateKeyBinding();
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ICustomCommandDialog
{
Task<ShellProfile> GetCustomCommandAsync(ShellProfile input = null);
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IInputDialog
{
void SetTitle(string title);
Task<string> GetInput();
}
}
@@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface IMessageDialog
{
string Title { get; set; }
string Content { get; set; }
void AddButton(DialogButton button);
Task<DialogButton> ShowAsync();
}
}
@@ -0,0 +1,10 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Dialogs
{
public interface ISshConnectionInfoDialog
{
Task<SshProfile> GetSshConnectionInfoAsync(SshProfile input = null);
}
}
@@ -0,0 +1,7 @@
namespace FluentTerminal.App.Services.EventArgs
{
public class CancelableEventArgs : System.EventArgs
{
public bool Cancelled { get; set; }
}
}
@@ -0,0 +1,11 @@
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.Services.EventArgs
{
public class NewTabRequestedEventArgs : System.EventArgs
{
public DragEventArgs DragEventArgs { get; set; }
public int Position { get; set; }
}
}
@@ -0,0 +1,13 @@
using System;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.EventArgs
{
public class NewWindowRequestedEventArgs : System.EventArgs
{
public ShellProfile Profile { get; }
public NewWindowRequestedEventArgs(ShellProfile profile) =>
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
}
}
@@ -0,0 +1,23 @@
using System;
namespace FluentTerminal.App.Services.Exceptions
{
public class ParseThemeException : Exception
{
public ParseThemeException() : base()
{
}
public ParseThemeException(string message) : base(message)
{
}
public ParseThemeException(string message, Exception innerException) : base(message, innerException)
{
}
protected ParseThemeException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluentTerminal.App.Services.Exceptions
{
public class SaveTextFileException : Exception
{
public SaveTextFileException() : base()
{
}
public SaveTextFileException(string message) : base(message)
{
}
public SaveTextFileException(string message, Exception innerException) : base(message, innerException)
{
}
protected SaveTextFileException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
}
}
}
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="PListNet" Version="3.0.0" />
<PackageReference Include="RestSharp" Version="106.10.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="System.Runtime.InteropServices.WindowsRuntime" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FluentTerminal.Models\FluentTerminal.Models.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.FoundationContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Windows.Foundation.UniversalApiContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.UniversalApiContract\5.0.0.0\Windows.Foundation.UniversalApiContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,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);
}
}
@@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
namespace FluentTerminal.App.Services
{
public interface IAppServiceConnection
{
event EventHandler<ValueSet> MessageReceived;
Task<ValueSet> SendMessageAsync(ValueSet message);
}
}
@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface IApplicationDataContainer
{
T ReadValueFromJson<T>(string name, T fallbackValue);
void WriteValueAsJson<T>(string name, T value);
bool TryGetValue(string key, out object value);
void SetValue(string key, object value);
void Delete(string key);
void Clear();
IEnumerable<object> GetAll();
}
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface IApplicationLanguageService
{
IEnumerable<string> Languages { get; }
void SetLanguage(string language);
string GetCurrentLanguage();
}
}
@@ -0,0 +1,29 @@
using FluentTerminal.App.Services.EventArgs;
using System;
using System.Threading.Tasks;
using Windows.UI.Core;
namespace FluentTerminal.App.Services
{
public delegate Task CloseRequestedHandler(object sender, CancelableEventArgs args);
public interface IApplicationView
{
event CloseRequestedHandler CloseRequested;
event EventHandler Closed;
int Id { get; }
string Title { get; set; }
Task ExecuteOnUiThreadAsync(Action action, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal,
bool enforceNewSchedule = false);
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);
}
}
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IClipboardService
{
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);
}
}
@@ -0,0 +1,26 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface IDefaultValueProvider
{
ApplicationSettings GetDefaultApplicationSettings();
ICollection<KeyBinding> GetDefaultKeyBindings(Command command);
Guid GetDefaultShellProfileId();
IEnumerable<TabTheme> GetDefaultTabThemes();
TerminalOptions GetDefaultTerminalOptions();
Guid GetDefaultThemeId();
IEnumerable<ShellProfile> GetPreinstalledShellProfiles();
IEnumerable<TerminalTheme> GetPreInstalledThemes();
}
}
@@ -0,0 +1,27 @@
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public enum DialogButton
{
// ReSharper disable once InconsistentNaming
OK,
Cancel
}
public interface IDialogService
{
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 ShowAboutDialogAsync();
}
}
@@ -0,0 +1,17 @@
using FluentTerminal.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IFileSystemService
{
Task<File> OpenFileAsync(IEnumerable<string> fileTypes);
Task<string> BrowseForDirectoryAsync();
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);
}
}
@@ -0,0 +1,13 @@
using System;
namespace FluentTerminal.App.Services
{
public interface IKeyboardCommandService
{
void RegisterCommandHandler(string command, Action handler);
void SendCommand(string command);
void DeregisterCommandHandler(string command);
void ClearCommandHandlers();
}
}
@@ -0,0 +1,7 @@
namespace FluentTerminal.App.Services
{
public interface INotificationService
{
void ShowNotification(string title, string content, string url = null);
}
}
@@ -0,0 +1,43 @@
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface ISettingsService
{
void DeleteShellProfile(Guid id);
void DeleteSshProfile(Guid id);
void DeleteTheme(Guid id);
ApplicationSettings GetApplicationSettings();
TerminalTheme GetCurrentTheme();
Guid GetCurrentThemeId();
ShellProfile GetDefaultShellProfile();
Guid GetDefaultShellProfileId();
IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings();
IEnumerable<ShellProfile> GetShellProfiles();
IEnumerable<SshProfile> GetSshProfiles();
/// <summary>
/// Returns union of <see cref="GetShellProfiles"/> and <see cref="GetSshProfiles"/>.
/// </summary>
IEnumerable<ShellProfile> GetAllProfiles();
IEnumerable<TabTheme> GetTabThemes();
TerminalOptions GetTerminalOptions();
TerminalTheme GetTheme(Guid id);
IEnumerable<TerminalTheme> GetThemes();
void ResetKeyBindings();
void SaveApplicationSettings(ApplicationSettings applicationSettings);
void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings);
void SaveCurrentThemeId(Guid id);
void SaveDefaultShellProfileId(Guid id);
void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings);
void SaveShellProfile(ShellProfile shellProfile, bool newShell = false);
void SaveSshProfile(SshProfile sshProfile, bool newShell = false);
void SaveTerminalOptions(TerminalOptions terminalOptions);
void SaveTheme(TerminalTheme theme, bool newTheme = false);
ShellProfile GetShellProfile(Guid id);
SshProfile GetSshProfile(Guid id);
string ExportSettings();
void ImportSettings(string serializedSettings);
}
}
@@ -0,0 +1,9 @@
using FluentTerminal.Models;
namespace FluentTerminal.App.Services
{
public interface IShellProfileMigrationService
{
void Migrate(ShellProfile profile);
}
}
@@ -0,0 +1,12 @@
using FluentTerminal.Models.Enums;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IStartupTaskService
{
Task DisableStartupTaskAsync();
Task<StartupTaskStatus> EnableStartupTaskAsync();
Task<StartupTaskStatus> GetStatusAsync();
}
}
@@ -0,0 +1,10 @@
using FluentTerminal.Models;
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface ISystemFontService
{
IEnumerable<FontInfo> GetSystemFontFamilies();
}
}
@@ -0,0 +1,16 @@
using FluentTerminal.Models;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IThemeParser
{
IEnumerable<string> SupportedFileTypes { get; }
Task<TerminalTheme> Parse(string fileName, Stream fileContent);
Task<ExportedTerminalTheme> Import(string fileName, Stream fileContent);
}
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace FluentTerminal.App.Services
{
public interface IThemeParserFactory
{
IEnumerable<string> SupportedFileTypes { get; }
IThemeParser GetParser(string fileType);
}
}
@@ -0,0 +1,52 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface ITrayProcessCommunicationService
{
event EventHandler<TerminalExitStatus> TerminalExited;
void Initialize(IAppServiceConnection appServiceConnection);
Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType);
Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause);
Task ResizeTerminalAsync(byte id, TerminalSize size);
Task UpdateToggleWindowKeyBindingsAsync();
Task WriteAsync(byte terminalId, byte[] data);
void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback);
void UnsubscribeFromTerminalOutput(byte terminalId);
Task CloseTerminalAsync(byte terminalId);
byte GetNextTerminalId();
Task<string> GetUserNameAsync();
Task SaveTextFileAsync(string path, string content);
Task<string> GetSshConfigDirAsync();
Task<string[]> GetFilesFromSshConfigDirAsync();
Task<bool> CheckFileExistsAsync(string path);
Task MuteTerminalAsync(bool mute);
void UpdateSettings(ApplicationSettings settings);
Task<string> GetCommandPathAsync(string command);
Task QuitApplicationAsync();
}
}
@@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public interface IUpdateService
{
Task CheckForUpdateAsync(bool notifyNoUpdate = false);
Version GetCurrentVersion();
Task<Version> GetLatestVersionAsync();
}
}
@@ -0,0 +1,660 @@
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
using Windows.Foundation.Metadata;
using Windows.Storage;
namespace FluentTerminal.App.Services.Implementation
{
public class DefaultValueProvider : IDefaultValueProvider
{
public ApplicationSettings GetDefaultApplicationSettings()
{
var logDirectoryPath = string.Empty;
try
{
// Accessing ApplicationData.Current during run of unit tests causes
// "System.InvalidOperationException : The process has no package identity" exception.
logDirectoryPath = ApplicationData.Current.LocalCacheFolder.Path;
} catch (InvalidOperationException) { }
return new ApplicationSettings
{
ConfirmClosingTabs = false,
ConfirmClosingWindows = false,
UnderlineSelectedTab = false,
InactiveTabColorMode = InactiveTabColorMode.Background,
NewTerminalLocation = NewTerminalLocation.Tab,
TabWindowCascadingAppMenu = true,
TabsPosition = TabsPosition.Top,
CopyOnSelect = false,
MouseMiddleClickAction = MouseAction.None,
MouseRightClickAction = MouseAction.ContextMenu,
ShowNewOutputIndicator = false,
EnableTrayIcon = true,
ShowCustomTitleInTitlebar = true,
UseMoshByDefault = true,
AutoFallbackToWindowsUsernameInLinks = true,
RTrimCopiedLines = true,
MuteTerminalBeeps = true,
EnableLogging = false,
PrintableOutputOnly = true,
LogDirectoryPath = logDirectoryPath,
UseConPty = true,
ShowTextCopied = false
};
}
public ICollection<KeyBinding> GetDefaultKeyBindings(Command command)
{
switch (command)
{
case Command.ToggleWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ToggleWindow),
Key = (int)ExtendedVirtualKey.Scroll
}
};
case Command.NextTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NextTab),
Ctrl = true,
Key = (int)ExtendedVirtualKey.Tab
}
};
case Command.PreviousTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.PreviousTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Tab
}
};
case Command.SwitchToTerm1:
case Command.SwitchToTerm2:
case Command.SwitchToTerm3:
case Command.SwitchToTerm4:
case Command.SwitchToTerm5:
case Command.SwitchToTerm6:
case Command.SwitchToTerm7:
case Command.SwitchToTerm8:
case Command.SwitchToTerm9:
return new List<KeyBinding>
{
new KeyBinding
{
Command = command.ToString(),
Alt = true,
Key = (int)ExtendedVirtualKey.Number1 + (command - Command.SwitchToTerm1)
}
};
case Command.DuplicateTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.DuplicateTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.D
}
};
case Command.NewTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewSshTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewSshTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.Y
}
};
case Command.NewSshWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewSshWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.Y
}
};
case Command.NewCustomCommandTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewCustomCommandTab),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.T
}
};
case Command.NewCustomCommandWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewCustomCommandWindow),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ChangeTabTitle:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ChangeTabTitle),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.R
}
};
case Command.CloseTab:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.CloseTab),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.W
}
};
case Command.NewWindow:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.NewWindow),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.N
}
};
case Command.ShowSettings:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ShowSettings),
Ctrl = true,
Shift = true,
// 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
}
};
case Command.Copy:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.Copy),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.C
}
};
case Command.Paste:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.Paste),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.V
}
};
case Command.PasteWithoutNewlines:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.PasteWithoutNewlines),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.V
}
};
case Command.Search:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.Search),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.F
}
};
case Command.ToggleFullScreen:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.ToggleFullScreen),
Alt = true,
Key = (int)ExtendedVirtualKey.Enter
},
new KeyBinding
{
Command = nameof(Command.ToggleFullScreen),
Key = (int)ExtendedVirtualKey.F11
}
};
case Command.SelectAll:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.SelectAll),
Ctrl = true,
Shift = true,
Key = (int)ExtendedVirtualKey.A
}
};
case Command.Clear:
return new List<KeyBinding>
{
new KeyBinding
{
Command = nameof(Command.Clear),
Ctrl = true,
Alt = true,
Key = (int)ExtendedVirtualKey.L
}
};
default:
throw new InvalidOperationException($"Default keybindings for Command '{command}' are missing");
}
}
public Guid GetDefaultShellProfileId()
{
return Guid.Parse("813f2298-210a-481a-bdbf-c17bc637a3e2");
}
public IEnumerable<TabTheme> GetDefaultTabThemes()
{
return new[]
{
new TabTheme
{
Id = 0,
Name = I18N.Translate("TabTheme.None"),
BackgroundOpacity = double.NaN,
BackgroundPointerOverOpacity = double.NaN,
BackgroundPressedOpacity = double.NaN,
BackgroundSelectedOpacity = double.NaN,
BackgroundSelectedPointerOverOpacity = double.NaN,
BackgroundSelectedPressedOpacity = double.NaN
},
new TabTheme
{
Id = 1,
Name = I18N.Translate("TabTheme.Red"),
Color = "#E81123"
},
new TabTheme
{
Id = 2,
Name = I18N.Translate("TabTheme.Green"),
Color = "#10893E"
},
new TabTheme
{
Id = 3,
Name = I18N.Translate("TabTheme.Blue"),
Color = "#0078D7"
},
new TabTheme
{
Id = 4,
Name = I18N.Translate("TabTheme.Purple"),
Color = "#881798"
},
new TabTheme
{
Id = 5,
Name = I18N.Translate("TabTheme.Orange"),
Color = "#FF8C00"
},
new TabTheme
{
Id = 6,
Name = I18N.Translate("TabTheme.Teal"),
Color = "#00B7C3"
}
};
}
public TerminalOptions GetDefaultTerminalOptions()
{
return new TerminalOptions
{
BellStyle = BellStyle.None,
CursorBlink = true,
CursorStyle = CursorStyle.Block,
ScrollBarStyle = ScrollBarStyle.Hidden,
FontFamily = "Consolas",
FontSize = 13,
FontWeight = 400,
BackgroundOpacity = 0.8,
Padding = 12,
ScrollBackLimit = 1000,
WordSeparator = " ()[]{}:;|│!&*<>@&quot;&squo;",
UseAcrylicBackground = true
};
}
public Guid GetDefaultThemeId()
{
return Guid.Parse("281e4352-bb50-47b7-a691-2b13830df95e");
}
public IEnumerable<ShellProfile> GetPreinstalledShellProfiles()
{
return new[]
{
new ShellProfile
{
Id = GetDefaultShellProfileId(),
Name = "Powershell",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8), // Windows 10 1903+
UseBuffer = false,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
{
Command = GetDefaultShellProfileId().ToString(),
Ctrl=true,
Alt=true,
Shift=false,
Meta=false,
Key=(int)ExtendedVirtualKey.Number1
}
}
},
new ShellProfile
{
Id = Guid.Parse("ab942a61-7673-4755-9bd8-765aff91d9a3"),
Name = "CMD",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\Windows\System32\cmd.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
UseBuffer = true,
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
{
Command = "ab942a61-7673-4755-9bd8-765aff91d9a3",
Ctrl=true,
Alt=true,
Shift=false,
Meta=false,
Key=(int)ExtendedVirtualKey.Number2
}
}
},
new ShellProfile
{
Id= Guid.Parse("e5785ad6-584f-40cb-bdcd-d5b3b3953e7f"),
Name = "WSL",
MigrationVersion = ShellProfile.CurrentMigrationVersion,
Arguments = string.Empty,
Location = @"C:\windows\system32\wsl.exe",
PreInstalled = true,
WorkingDirectory = string.Empty,
UseConPty = ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7), // Windows 10 1809+
UseBuffer = true, //TODO: Set to false if the buffer causes issues with WSL.
EnvironmentVariables = new Dictionary<string, string>
{
["TERM"] = "xterm-256color"
},
KeyBindings = new []
{
new KeyBinding
{
Command = "e5785ad6-584f-40cb-bdcd-d5b3b3953e7f",
Ctrl=true,
Alt=true,
Shift=false,
Meta=false,
Key=(int)ExtendedVirtualKey.Number3
}
}
}
};
}
public IEnumerable<TerminalTheme> GetPreInstalledThemes()
{
var defaultXterm = new TerminalTheme
{
Id = GetDefaultThemeId(),
Author = "xterm.js",
Name = "Xterm.js Default",
PreInstalled = true,
Colors = new TerminalColors
{
Black = "#2e3436",
Red = "#cc0000",
Green = "#4e9a06",
Yellow = "#c4a000",
Blue = "#3465a4",
Magenta = "#75507b",
Cyan = "#06989a",
White = "#d3d7cf",
BrightBlack = "#555753",
BrightRed = "#ef2929",
BrightGreen = "#8ae234",
BrightYellow = "#fce94f",
BrightBlue = "#729fcf",
BrightMagenta = "#ad7fa8",
BrightCyan = "#34e2e2",
BrightWhite = "#eeeeec",
Foreground = "#ffffff",
Background = "#000000",
Cursor = "#ffffff",
CursorAccent = "#000000",
Selection = "rgba(255, 255, 255, 0.3)"
}
};
var powerShell = new TerminalTheme
{
Id = Guid.Parse("3571ce1b-31ce-4cf7-ae15-e0bff70c3eea"),
Author = "Microsoft",
Name = "PowerShell",
PreInstalled = true,
Colors = new TerminalColors
{
Black = "#000000",
Red = "#800000",
Green = "#008000",
Yellow = "#EEEDF0",
Blue = "#000080",
Magenta = "#012456",
Cyan = "#008080",
White = "#c0c0c0",
BrightBlack = "#808080",
BrightRed = "#ff0000",
BrightGreen = "#00ff00",
BrightYellow = "#ffff00",
BrightBlue = "#0000ff",
BrightMagenta = "#ff00ff",
BrightCyan = "#00ffff",
BrightWhite = "#ffffff",
Foreground = "#ffffff",
Background = "#012456",
Cursor = "#fedba9",
CursorAccent = "#000000",
Selection = "rgba(254, 219, 169, 0.3)"
}
};
var homebrew = new TerminalTheme
{
Id = Guid.Parse("5f034210-067b-40e2-b9c9-6a25eb6fb8e6"),
Author = "Hans Kokx",
Name = "Homebrew",
PreInstalled = true,
Colors = new TerminalColors
{
Black = "#000000",
Red = "#A0160B",
Green = "#00AF21",
Yellow = "#A1A222",
Blue = "#192AB7",
Magenta = "#AA2FAE",
Cyan = "#12B1BC",
White = "#BBB5AF",
BrightBlack = "#747876",
BrightRed = "#E52213",
BrightGreen = "#00D92B",
BrightYellow = "#E6E435",
BrightBlue = "#283EF9",
BrightMagenta = "#EB43E6",
BrightCyan = "#15E7E8",
BrightWhite = "#E9E9E9",
Foreground = "#00D92B",
Background = "#000000",
Cursor = "#00D92B",
CursorAccent = "#000000",
Selection = "#4CFFFFFF"
}
};
var tomorrow = new TerminalTheme
{
Id = Guid.Parse("0cbbf805-fa86-4be5-ac51-75f3054dcc3a"),
Author = "Chris Kempson",
Name = "Tomorrow",
PreInstalled = true,
Colors = new TerminalColors
{
Foreground = "#4D4D4C",
Background = "#FFFFFF",
Cursor = "#4D4D4C",
CursorAccent = "#FFFFFF",
Selection = "rgba(214, 214, 214, 0.5)",
Black = "#000000",
Red = "#C82829",
Green = "#718C00",
Yellow = "#EAB700",
Blue = "#4271AE",
Magenta = "#8959A8",
Cyan = "#3E999F",
White = "#FFFFFF",
BrightBlack = "#000000",
BrightRed = "#C82829",
BrightGreen = "#718C00",
BrightYellow = "#EAB700",
BrightBlue = "#4271AE",
BrightMagenta = "#8959A8",
BrightCyan = "#3E999F",
BrightWhite = "#FFFFFF"
}
};
var ubuntu = new TerminalTheme
{
Id = Guid.Parse("32519543-bf94-4db6-92a1-7bcec3966c82"),
Author = "Canonical",
Name = "Ubuntu Bash",
PreInstalled = true,
Colors = new TerminalColors
{
Foreground = "#EEEEEE",
Background = "#300A24",
Cursor = "#CFF5DB",
CursorAccent = "#000000",
Selection = "#4DABEDC1",
Black = "#300A24",
Red = "#CC0000",
Green = "#4E9A06",
Yellow = "#C4A000",
Blue = "#3465A4",
Magenta = "#75507B",
Cyan = "#06989A",
White = "#D3D7CF",
BrightBlack = "#554E53",
BrightRed = "#EF2929",
BrightGreen = "#8AE234",
BrightYellow = "#FCE94F",
BrightBlue = "#729FCF",
BrightMagenta = "#AD7FA8",
BrightCyan = "#34E2E2",
BrightWhite = "#EEEEEE"
}
};
return new[] { defaultXterm, powerShell, homebrew, tomorrow, ubuntu };
}
}
}
@@ -0,0 +1,87 @@
using FluentTerminal.App.Services.Dialogs;
using FluentTerminal.Models;
using System;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Implementation
{
public class DialogService : IDialogService
{
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<IAboutDialog> _aboutDialogFactory;
public DialogService(Func<IMessageDialog> messageDialogFactory, Func<ICreateKeyBindingDialog> createKeyBindingDialogFactory,
Func<IInputDialog> inputDialogFactory, Func<ISshConnectionInfoDialog> sshConnectionInfoDialogFactory,
Func<ICustomCommandDialog> customCommandDialogFactory,
Func<IAboutDialog> aboutDialogFactory)
{
_messageDialogFactory = messageDialogFactory;
_createKeyBindingDialogFactory = createKeyBindingDialogFactory;
_inputDialogFactory = inputDialogFactory;
_sshConnectionInfoDialogFactory = sshConnectionInfoDialogFactory;
_customCommandDialogFactory = customCommandDialogFactory;
_aboutDialogFactory = aboutDialogFactory;
}
public Task<KeyBinding> ShowCreateKeyBindingDialog()
{
var dialog = _createKeyBindingDialogFactory();
return dialog.CreateKeyBinding();
}
public Task<DialogButton> ShowMessageDialogAsync(string title, string content, params DialogButton[] buttons)
{
if (string.IsNullOrWhiteSpace(title))
{
throw new ArgumentNullException(nameof(title));
}
if (string.IsNullOrWhiteSpace(content))
{
throw new ArgumentNullException(nameof(content));
}
if (buttons.Length == 0)
{
throw new ArgumentException("Must not be empty", nameof(buttons));
}
var dialog = _messageDialogFactory();
dialog.Content = content;
dialog.Title = title;
foreach (var button in buttons)
{
dialog.AddButton(button);
}
return dialog.ShowAsync();
}
public Task<string> ShowInputDialogAsync(string title)
{
var dialog = _inputDialogFactory.Invoke();
dialog.SetTitle(title);
return dialog.GetInput();
}
public Task<SshProfile> ShowSshConnectionInfoDialogAsync(SshProfile input = null) =>
_sshConnectionInfoDialogFactory().GetSshConnectionInfoAsync(input);
public Task<ShellProfile> ShowCustomCommandDialogAsync(ShellProfile input = null) =>
_customCommandDialogFactory().GetCustomCommandAsync(input);
public Task ShowAboutDialogAsync()
{
var dialog = _aboutDialogFactory();
return dialog.ShowAsync();
}
}
}
@@ -0,0 +1,49 @@
using FluentTerminal.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Implementation
{
public class FluentTerminalThemeParser : IThemeParser
{
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".flutecolors" };
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))
{
throw new ArgumentNullException(nameof(fileName));
}
if (fileContent == null)
{
throw new ArgumentNullException(nameof(fileContent));
}
using (var streamReader = new StreamReader(fileContent))
{
var content = await streamReader.ReadToEndAsync().ConfigureAwait(false);
var theme = JsonConvert.DeserializeObject<T>(content);
theme.PreInstalled = false;
theme.Id = Guid.NewGuid();
return theme;
}
}
}
}
@@ -0,0 +1,163 @@
using FluentTerminal.App.Services.Exceptions;
using FluentTerminal.Models;
using PListNet;
using PListNet.Nodes;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
// ReSharper disable InconsistentNaming
namespace FluentTerminal.App.Services.Implementation
{
public class ITermThemeParser : IThemeParser
{
private const byte Opacity30Percent = 77;
public IEnumerable<string> SupportedFileTypes { get; } = new[] { ".itermcolors" };
private static class ITermThemeKeys
{
public const string Ansi0Color = "Ansi 0 Color";
public const string Ansi1Color = "Ansi 1 Color";
public const string Ansi2Color = "Ansi 2 Color";
public const string Ansi3Color = "Ansi 3 Color";
public const string Ansi4Color = "Ansi 4 Color";
public const string Ansi5Color = "Ansi 5 Color";
public const string Ansi6Color = "Ansi 6 Color";
public const string Ansi7Color = "Ansi 7 Color";
public const string Ansi8Color = "Ansi 8 Color";
public const string Ansi9Color = "Ansi 9 Color";
public const string Ansi10Color = "Ansi 10 Color";
public const string Ansi11Color = "Ansi 11 Color";
public const string Ansi12Color = "Ansi 12 Color";
public const string Ansi13Color = "Ansi 13 Color";
public const string Ansi14Color = "Ansi 14 Color";
public const string Ansi15Color = "Ansi 15 Color";
public const string BackgroundColor = "Background 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";
public const string SelectedTextColor = "Selected Text Color";
public const string SelectionColor = "Selection Color";
}
private static class ITermThemeColorKeys
{
public const string BlueComponent = "Blue Component";
public const string GreenComponent = "Green Component";
public const string RedComponent = "Red Component";
}
public Task<TerminalTheme> Parse(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.");
return Task.FromResult(new TerminalTheme
{
Name = Path.GetFileNameWithoutExtension(fileName),
Colors = GetColors(node),
Id = Guid.NewGuid(),
PreInstalled = false
});
}
private TerminalColors GetColors(DictionaryNode themeDictionary)
{
return new TerminalColors
{
Background = GetColorString(themeDictionary[ITermThemeKeys.BackgroundColor]),
Foreground = GetColorString(themeDictionary[ITermThemeKeys.ForegroundColor]),
Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]),
CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]),
Selection = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor], Opacity30Percent),
SelectionForeground = GetColorString(themeDictionary[ITermThemeKeys.SelectedTextColor]),
SelectionBackground = GetColorString(themeDictionary[ITermThemeKeys.SelectionColor]),
Black = GetColorString(themeDictionary[ITermThemeKeys.Ansi0Color]),
Red = GetColorString(themeDictionary[ITermThemeKeys.Ansi1Color]),
Green = GetColorString(themeDictionary[ITermThemeKeys.Ansi2Color]),
Yellow = GetColorString(themeDictionary[ITermThemeKeys.Ansi3Color]),
Blue = GetColorString(themeDictionary[ITermThemeKeys.Ansi4Color]),
Magenta = GetColorString(themeDictionary[ITermThemeKeys.Ansi5Color]),
Cyan = GetColorString(themeDictionary[ITermThemeKeys.Ansi6Color]),
White = GetColorString(themeDictionary[ITermThemeKeys.Ansi7Color]),
BrightBlack = GetColorString(themeDictionary[ITermThemeKeys.Ansi8Color]),
BrightRed = GetColorString(themeDictionary[ITermThemeKeys.Ansi9Color]),
BrightGreen = GetColorString(themeDictionary[ITermThemeKeys.Ansi10Color]),
BrightYellow = GetColorString(themeDictionary[ITermThemeKeys.Ansi11Color]),
BrightBlue = GetColorString(themeDictionary[ITermThemeKeys.Ansi12Color]),
BrightMagenta = GetColorString(themeDictionary[ITermThemeKeys.Ansi13Color]),
BrightCyan = GetColorString(themeDictionary[ITermThemeKeys.Ansi14Color]),
BrightWhite = GetColorString(themeDictionary[ITermThemeKeys.Ansi15Color]),
};
}
private string GetColorString(PNode colorNode, byte alpha = Byte.MaxValue)
{
var dictionaryNode = colorNode as DictionaryNode ?? throw new ParseThemeException("Color node was not a dictionary");
var red = dictionaryNode[ITermThemeColorKeys.RedComponent] as RealNode ?? throw new ParseThemeException("Red node value was not a real number");
var green = dictionaryNode[ITermThemeColorKeys.GreenComponent] as RealNode ?? throw new ParseThemeException("Green node value was not a real number");
var blue = dictionaryNode[ITermThemeColorKeys.BlueComponent] as RealNode ?? throw new ParseThemeException("Blue node value was not a real number");
if (alpha == byte.MaxValue)
{
return $"#{GetByteValue(red):X2}{GetByteValue(green):X2}{GetByteValue(blue):X2}";
}
else
{
return $"rgba({GetByteValue(red):G}, {GetByteValue(green):G}, {GetByteValue(blue):G}, {ToDoubleString(alpha)})";
}
}
private byte GetByteValue(RealNode node)
{
var doubleValue = node.Value * Byte.MaxValue;
return Convert.ToByte(doubleValue);
}
private static string ToDoubleString(byte alpha)
{
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,51 @@
using FluentTerminal.Models.Enums;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services.Implementation
{
public class KeyboardCommandService : IKeyboardCommandService
{
private readonly Dictionary<string, Action> _commandHandlers = new Dictionary<string, Action>();
public void RegisterCommandHandler(string command, Action handler)
{
if (_commandHandlers.ContainsKey(command))
{
throw new InvalidOperationException("command already registered");
}
_commandHandlers[command] = handler ?? throw new ArgumentNullException(nameof(handler));
}
public void DeregisterCommandHandler(string command)
{
if (_commandHandlers.ContainsKey(command))
{
_commandHandlers.Remove(command);
}
}
public void SendCommand(string command)
{
if (_commandHandlers.TryGetValue(command, out Action handler))
{
handler.Invoke();
return;
}
// already registered by SystemTray's ToggleWindowService
if (command == nameof(Command.ToggleWindow))
{
return;
}
throw new KeyNotFoundException($"No handler registered for command: {command}");
}
public void ClearCommandHandlers()
{
_commandHandlers.Clear();
}
}
}
@@ -0,0 +1,94 @@
using System;
using System.Text.RegularExpressions;
using FluentTerminal.Models;
namespace FluentTerminal.App.Services.Implementation
{
// The purpose of this class is to ensure backward compatibility of mosh profiles and command history
// TODO: This class should be probably deleted in few months or so
public static class MoshBackwardCompatibility
{
private static readonly string MoshCommandExe = $"{Constants.MoshCommandName}.exe";
private static readonly Regex OldFormatRx = new Regex(@".+\s(?<ports>\d{1,5}:\d{1,5})$", RegexOptions.Compiled);
private static readonly Regex TargetRx =
new Regex(@"(^|\s+)(?<user>[^\s@;]+)@(?<host>[^\s@:]+)(:(?<port>\d{1,5}))?\s+", RegexOptions.Compiled);
private static bool IsMoshProfile(ShellProfile profile) =>
Constants.MoshCommandName.Equals(profile.Location, StringComparison.OrdinalIgnoreCase) ||
MoshCommandExe.Equals(profile.Location, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Checks if the profile contains obsolete mosh syntax, and changes it if it does.
/// </summary>
/// <param name="profile">The profile. Note that the method doesn't change the input profile at all, but creates and
/// returns its clone if there's something to change.</param>
/// <remarks>
/// The method actually checks if <see cref="ShellProfile.Arguments"/> of the input <paramref name="profile"/> is in
/// obsolete <c>mosh</c> format <c>[ssh_arguments] [target] [mosh_port_range]</c>, and if it is changes it to the new
/// format <c>--ssh="[ssh_arguments]" -p [mosh_ports] [target]</c>.
/// </remarks>
/// <returns>
/// If the method detected that the input profile is obsolete mosh profile, it will create its clone with the updated
/// command line arguments, and return it. Otherwise it will simply return unchanged input profile (the same reference
/// as the input profile). It allows us to simply check if anything is changed by comparing the input and output
/// profiles for reference equality.
/// </returns>
public static ShellProfile FixProfile(ShellProfile profile)
{
// If there aren't any arguments, or if the profile isn't a mosh profile, there's nothing to do. Just return.
if (string.IsNullOrWhiteSpace(profile.Arguments) || !IsMoshProfile(profile)) return profile;
var arguments = profile.Arguments.Trim();
var moshPortsMatch = OldFormatRx.Match(arguments);
if (!moshPortsMatch.Success) return profile;// Arguments don't end with mosh ports (i.e. 60000:61000), so it isn't an obsolete format.
var targetMatches = TargetRx.Matches(arguments);
if (targetMatches.Count != 1) return profile;// Target part (user@host:port) not found or found multiple times. Cannot reliably determine the target, so don't change.
var targetMatch = targetMatches[0];
// Removing [mosh_ports] from the original arguments
arguments = arguments.Substring(0, arguments.Length - moshPortsMatch.Groups["ports"].Length).Trim();
var newProfile = profile.Clone();
// Removing [target] from the original arguments, thus getting "pure" ssh arguments
var sshArguments = targetMatch.Index > 0 ? arguments.Substring(0, targetMatch.Index).Trim() : string.Empty;
if (targetMatch.Index + targetMatch.Length < arguments.Length)
{
sshArguments += $" {arguments.Substring(targetMatch.Index + targetMatch.Length).Trim()}";
}
// If the ssh port is defined in the original target (i.e. user@host:port), which was allowed in the obsolete mosh, extracting the port part into a separate ssh argument
if (targetMatch.Groups["port"].Success && uint.TryParse(targetMatch.Groups["port"].Value, out var port) &&
port != SshProfile.DefaultSshPort)
{
sshArguments += $" -p {targetMatch.Groups["port"].Value}";
}
// Replacing any double quotes in ssh arguments with single quotes, since ssh arguments will be double-quoted as a whole
sshArguments = sshArguments.Replace("\"", "'").Trim();
arguments = $"-p {moshPortsMatch.Groups["ports"].Value}";
if (!string.IsNullOrEmpty(sshArguments))
{
// If there were some ssh arguments, add them as defined in the new format
arguments += $" --ssh=\"ssh {sshArguments}\"";
}
// Finally append the target to the arguments
arguments += $" {targetMatch.Groups["user"]}@{targetMatch.Groups["host"]}";
newProfile.Arguments = arguments;
return newProfile;
}
}
}
@@ -0,0 +1,31 @@
using System.Diagnostics;
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;
namespace FluentTerminal.App.Services.Implementation
{
public class NotificationService : INotificationService
{
public void ShowNotification(string title, string content, string url = null)
{
string xml = $@"<toast>
<visual>
<binding template='ToastGeneric'>
<text>{title}</text>
<text>{content}</text>
</binding>
</visual>
</toast>";
var doc = new XmlDocument();
doc.LoadXml(xml);
var toast = new ToastNotification(doc);
if (url != null)
{
toast.Activated += (n, o) => Process.Start(url);
}
ToastNotificationManager.CreateToastNotifier().Show(toast);
}
}
}
@@ -0,0 +1,412 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Messaging;
namespace FluentTerminal.App.Services.Implementation
{
public class SettingsService : ISettingsService
{
public const string CurrentThemeKey = "CurrentTheme";
public const string DefaultShellProfileKey = "DefaultShellProfile";
public const string DefaultSshProfileKey = "DefaultSshProfile";
private readonly IDefaultValueProvider _defaultValueProvider;
private readonly IApplicationDataContainer _keyBindings;
private readonly IApplicationDataContainer _localSettings;
private readonly IApplicationDataContainer _roamingSettings;
private readonly IApplicationDataContainer _shellProfiles;
private readonly IApplicationDataContainer _sshProfiles;
private readonly IApplicationDataContainer _themes;
public SettingsService(IDefaultValueProvider defaultValueProvider, ApplicationDataContainers containers)
{
_defaultValueProvider = defaultValueProvider;
_localSettings = containers.LocalSettings;
_roamingSettings = containers.RoamingSettings;
_themes = containers.Themes;
_keyBindings = containers.KeyBindings;
_shellProfiles = containers.ShellProfiles;
_sshProfiles = containers.SshProfiles;
foreach (var theme in _defaultValueProvider.GetPreInstalledThemes())
{
if (GetTheme(theme.Id) == null)
{
_themes.WriteValueAsJson(theme.Id.ToString(), theme);
}
}
foreach (var shellProfile in _defaultValueProvider.GetPreinstalledShellProfiles())
{
if (GetShellProfile(shellProfile.Id) == null)
{
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
}
}
}
public string ExportSettings()
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = GetCommandKeyBindings(),
TerminalOptions = GetTerminalOptions(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>()
};
foreach (var theme in GetThemes().Where(x => !x.PreInstalled))
{
config.Themes.Add(theme);
}
foreach (var profile in GetShellProfiles().Where(x => !x.PreInstalled))
{
config.Profiles.Add(profile);
}
foreach (var profile in GetSshProfiles())
{
config.SshProfiles.Add(profile);
}
return JsonConvert.SerializeObject(config, PreserveDictionaryKeyCaseContractResolver.SerializerSettings);
}
public void ImportSettings(string serializedSettings)
{
var config = new
{
App = GetApplicationSettings(),
KeyBindings = new Dictionary<string, ICollection<KeyBinding>>(),
Themes = new List<TerminalTheme>(),
Profiles = new List<ShellProfile>(),
SshProfiles = new List<SshProfile>(),
TerminalOptions = GetTerminalOptions()
};
JsonConvert.PopulateObject(serializedSettings, config);
SaveApplicationSettings(config.App);
// Since we set each command sepaartely, we don't need all existing settings
foreach (var pair in config.KeyBindings)
{
SaveKeyBindings(pair.Key, pair.Value);
}
// Can't create/modify pre-installed themes
foreach (var theme in config.Themes.Where(x => !x.PreInstalled))
{
var existingTheme = GetTheme(theme.Id);
if (existingTheme?.PreInstalled == true)
{
continue;
}
SaveTheme(theme, existingTheme != null);
}
// Can't create pre-installed profiles
foreach (var profile in config.Profiles.Where(x => !x.PreInstalled))
{
var existingProfile = GetShellProfile(profile.Id);
var isNew = existingProfile.EqualTo(default);
// You can only edit certain parts of preinstalled profiles
if (!isNew && existingProfile.PreInstalled)
{
existingProfile.WorkingDirectory = profile.WorkingDirectory;
existingProfile.Arguments = profile.Arguments;
existingProfile.TabThemeId = profile.TabThemeId;
existingProfile.TerminalThemeId = profile.TerminalThemeId;
existingProfile.KeyBindings = profile.KeyBindings;
SaveShellProfile(profile, isNew);
continue;
}
SaveShellProfile(profile, isNew);
}
foreach (var profile in config.SshProfiles)
{
var existingProfile = GetSshProfile(profile.Id);
var isNew = existingProfile.EqualTo(default(SshProfile));
SaveSshProfile(profile, isNew);
}
SaveTerminalOptions(config.TerminalOptions);
}
public void DeleteShellProfile(Guid id)
{
_shellProfiles.Delete(id.ToString());
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
}
public void DeleteSshProfile(Guid id)
{
_sshProfiles.Delete(id.ToString());
Messenger.Default.Send(new ShellProfileDeletedMessage(id));
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
public void DeleteTheme(Guid id)
{
_themes.Delete(id.ToString());
foreach (var profile in GetShellProfiles())
{
if (profile.TerminalThemeId == id)
{
profile.TerminalThemeId = Guid.Empty;
SaveShellProfile(profile);
}
}
Messenger.Default.Send(new ThemeDeletedMessage(id));
}
public ApplicationSettings GetApplicationSettings()
{
return _roamingSettings.ReadValueFromJson(nameof(ApplicationSettings), _defaultValueProvider.GetDefaultApplicationSettings());
}
public TerminalTheme GetCurrentTheme()
{
var id = GetCurrentThemeId();
var theme = GetTheme(id);
if (theme == null)
{
id = _defaultValueProvider.GetDefaultThemeId();
SaveCurrentThemeId(id);
theme = GetTheme(id);
}
return theme;
}
public Guid GetCurrentThemeId()
{
if (_roamingSettings.TryGetValue(CurrentThemeKey, out object value))
{
return (Guid)value;
}
return _defaultValueProvider.GetDefaultThemeId();
}
public ShellProfile GetDefaultShellProfile()
{
var id = GetDefaultShellProfileId();
var profile = _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
if (profile == null)
{
id = _defaultValueProvider.GetDefaultShellProfileId();
SaveDefaultShellProfileId(id);
profile = _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
}
return profile;
}
public ShellProfile GetShellProfile(Guid id)
{
return _shellProfiles.ReadValueFromJson(id.ToString(), default(ShellProfile));
}
public SshProfile GetSshProfile(Guid id)
{
return _sshProfiles.ReadValueFromJson(id.ToString(), default(SshProfile));
}
public Guid GetDefaultShellProfileId()
{
if (_localSettings.TryGetValue(DefaultShellProfileKey, out object value))
{
return (Guid)value;
}
return _defaultValueProvider.GetDefaultShellProfileId();
}
public IDictionary<string, ICollection<KeyBinding>> GetCommandKeyBindings()
{
var keyBindings = new Dictionary<string, ICollection<KeyBinding>>();
foreach (Command command in Enum.GetValues(typeof(Command)))
{
keyBindings.Add(command.ToString(), _keyBindings.ReadValueFromJson<Collection<KeyBinding>>(command.ToString(), null) ?? _defaultValueProvider.GetDefaultKeyBindings(command));
}
return keyBindings;
}
public IEnumerable<ShellProfile> GetShellProfiles()
{
return _shellProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<ShellProfile>((string) x))
.Select(MoshBackwardCompatibilityFixProfile);
}
public IEnumerable<SshProfile> GetSshProfiles()
{
return _sshProfiles.GetAll().Select(x => JsonConvert.DeserializeObject<SshProfile>((string) x))
.Select(MoshBackwardCompatibilityFixProfile).Cast<SshProfile>();
}
public IEnumerable<ShellProfile> GetAllProfiles()
{
return GetShellProfiles().Union(GetSshProfiles());
}
private ShellProfile MoshBackwardCompatibilityFixProfile(ShellProfile profile)
{
var fixedProfile = MoshBackwardCompatibility.FixProfile(profile);
if (ReferenceEquals(fixedProfile, profile))
{
// Nothing changed
return fixedProfile;
}
if (fixedProfile is SshProfile sshProfile)
{
DeleteSshProfile(profile.Id);
SaveSshProfile(sshProfile);
}
else
{
DeleteShellProfile(profile.Id);
SaveShellProfile(fixedProfile);
}
return fixedProfile;
}
public IEnumerable<TabTheme> GetTabThemes()
{
return _defaultValueProvider.GetDefaultTabThemes();
}
public TerminalOptions GetTerminalOptions()
{
return _roamingSettings.ReadValueFromJson(nameof(TerminalOptions), _defaultValueProvider.GetDefaultTerminalOptions());
}
public TerminalTheme GetTheme(Guid id)
{
return _themes.ReadValueFromJson(id.ToString(), default(TerminalTheme));
}
public IEnumerable<TerminalTheme> GetThemes()
{
return _themes.GetAll().Select(x => JsonConvert.DeserializeObject<TerminalTheme>((string)x)).ToList();
}
public void ResetKeyBindings()
{
foreach (Command command in Enum.GetValues(typeof(Command)))
{
_keyBindings.WriteValueAsJson(command.ToString(), _defaultValueProvider.GetDefaultKeyBindings(command));
}
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
public void SaveApplicationSettings(ApplicationSettings applicationSettings)
{
_roamingSettings.WriteValueAsJson(nameof(ApplicationSettings), applicationSettings);
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void NotifyApplicationSettingsChanged(ApplicationSettings applicationSettings)
{
Messenger.Default.Send(new ApplicationSettingsChangedMessage(applicationSettings.Clone()));
}
public void SaveCurrentThemeId(Guid id)
{
_roamingSettings.SetValue(CurrentThemeKey, id);
Messenger.Default.Send(new CurrentThemeChangedMessage(id));
}
public void SaveDefaultShellProfileId(Guid id)
{
_localSettings.SetValue(DefaultShellProfileKey, id);
Messenger.Default.Send(new DefaultShellProfileChangedMessage(id));
}
public void SaveKeyBindings(string command, ICollection<KeyBinding> keyBindings)
{
if (!Enum.TryParse<Command>(command, true, out var enumValue))
{
throw new InvalidOperationException();
}
_keyBindings.WriteValueAsJson(enumValue.ToString(), keyBindings);
Messenger.Default.Send(new KeyBindingsChangedMessage());
}
public void SaveShellProfile(ShellProfile shellProfile, bool newShell = false)
{
_shellProfiles.WriteValueAsJson(shellProfile.Id.ToString(), shellProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
Messenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
Messenger.Default.Send(new ShellProfileAddedMessage(shellProfile));
}
else
{
Messenger.Default.Send(new ShellProfileChangedMessage(shellProfile));
}
}
public void SaveSshProfile(SshProfile sshProfile, bool newShell = false)
{
_sshProfiles.WriteValueAsJson(sshProfile.Id.ToString(), sshProfile);
// When saving the shell profile, we also need to update keybindings for everywhere.
Messenger.Default.Send(new KeyBindingsChangedMessage());
if (newShell)
{
Messenger.Default.Send(new ShellProfileAddedMessage(sshProfile));
}
else
{
Messenger.Default.Send(new ShellProfileChangedMessage(sshProfile));
}
}
public void SaveTerminalOptions(TerminalOptions terminalOptions)
{
_roamingSettings.WriteValueAsJson(nameof(TerminalOptions), terminalOptions);
Messenger.Default.Send(new TerminalOptionsChangedMessage(terminalOptions));
}
public void SaveTheme(TerminalTheme theme, bool newTheme = false)
{
_themes.WriteValueAsJson(theme.Id.ToString(), theme);
if (theme.Id == GetCurrentThemeId())
{
Messenger.Default.Send(new CurrentThemeChangedMessage(theme.Id));
}
if (newTheme)
{
Messenger.Default.Send(new ThemeAddedMessage(theme));
}
}
}
}
@@ -0,0 +1,32 @@
using FluentTerminal.Models;
using System;
using System.Collections.Generic;
namespace FluentTerminal.App.Services.Implementation
{
public class ShellProfileMigrationService : IShellProfileMigrationService
{
private readonly Dictionary<int, Action<ShellProfile>> _migrationSteps = new Dictionary<int, Action<ShellProfile>>
{
[1] = new Action<ShellProfile>(profile =>
{
profile.EnvironmentVariables["TERM"] = "xterm-256color";
})
};
public void Migrate(ShellProfile profile)
{
while(profile.MigrationVersion < ShellProfile.CurrentMigrationVersion)
{
ApplyMigrationStep(profile, profile.MigrationVersion + 1);
}
}
private void ApplyMigrationStep(ShellProfile profile, int targetVersion)
{
_migrationSteps[targetVersion](profile);
profile.MigrationVersion = targetVersion;
}
}
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Linq;
namespace FluentTerminal.App.Services.Implementation
{
public class ThemeParserFactory : IThemeParserFactory
{
private readonly IEnumerable<IThemeParser> _parsers;
public IEnumerable<string> SupportedFileTypes { get; }
public ThemeParserFactory(IEnumerable<IThemeParser> parsers)
{
_parsers = parsers;
SupportedFileTypes = _parsers.SelectMany(p => p.SupportedFileTypes);
}
public IThemeParser GetParser(string fileType)
{
return _parsers.FirstOrDefault(p => p.SupportedFileTypes.Contains(fileType));
}
}
}
@@ -0,0 +1,300 @@
using FluentTerminal.App.Services.Exceptions;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Requests;
using FluentTerminal.Models.Responses;
using Newtonsoft.Json;
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;
public TrayProcessCommunicationService(ISettingsService settingsService)
{
_settingsService = settingsService;
_terminalOutputHandlers = new Dictionary<byte, Action<byte[]>>();
}
public byte GetNextTerminalId()
{
return _nextTerminalId++;
}
public async Task<string> GetUserNameAsync()
{
if (!string.IsNullOrEmpty(_userName))
{
// Returning the username from cache
return _userName;
}
var response = await GetResponseAsync<StringValueResponse>(new GetUserNameRequest()).ConfigureAwait(false);
if (response.Success)
{
_userName = response.Value;
}
return _userName;
}
public async Task SaveTextFileAsync(string path, string 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);
}
}
public async Task<string> GetSshConfigDirAsync()
{
if (!string.IsNullOrEmpty(_sshConfigDir))
{
return _sshConfigDir;
}
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{ IncludeContent = false }).ConfigureAwait(false);
if (response.Success)
{
_sshConfigDir = response.Path;
}
return _sshConfigDir;
}
public async Task<string[]> GetFilesFromSshConfigDirAsync()
{
var response =
await GetResponseAsync<GetSshConfigFolderResponse>(new GetSshConfigFolderRequest
{IncludeContent = true}).ConfigureAwait(false);
if (response.Success)
{
_sshConfigDir = response.Path;
return response.Files;
}
return null;
}
public async Task<bool> CheckFileExistsAsync(string path)
{
var response = await GetResponseAsync<CommonResponse>(new CheckFileExistsRequest {Path = path})
.ConfigureAwait(false);
return response.Success;
}
public Task MuteTerminalAsync(bool mute)
{
return _appServiceConnection.SendMessageAsync(CreateMessage(new MuteTerminalRequest {Mute = mute}));
}
public void UpdateSettings(ApplicationSettings settings)
{
_appServiceConnection.SendMessageAsync(CreateMessage(new UpdateSettingsRequest { Settings = settings }));
}
public async Task<CreateTerminalResponse> CreateTerminalAsync(byte id, TerminalSize size, ShellProfile shellProfile,
SessionType sessionType)
{
var request = new CreateTerminalRequest
{
Id = id,
Size = size,
Profile = shellProfile,
SessionType = sessionType
};
Logger.Instance.Debug("Sending CreateTerminalRequest: {@request}", request);
var response = await GetResponseAsync<CreateTerminalResponse>(request).ConfigureAwait(false);
Logger.Instance.Debug("Received CreateTerminalResponse: {@response}", response);
return response;
}
public Task<PauseTerminalOutputResponse> PauseTerminalOutputAsync(byte id, bool pause)
{
return GetResponseAsync<PauseTerminalOutputResponse>(
new PauseTerminalOutputRequest {Id = id, Pause = pause});
}
public void Initialize(IAppServiceConnection appServiceConnection)
{
_appServiceConnection = appServiceConnection;
_appServiceConnection.MessageReceived += OnMessageReceived;
}
private void OnMessageReceived(object sender, IDictionary<string, object> e)
{
var messageType = (byte)e[MessageKeys.Type];
var messageContent = e[MessageKeys.Content];
switch (messageType)
{
case Constants.TerminalBufferRequestIdentifier:
var terminalId = (byte)e[MessageKeys.TerminalId];
if (_terminalOutputHandlers.ContainsKey(terminalId))
{
_terminalOutputHandlers[terminalId].Invoke((byte[])messageContent);
}
else
{
Logger.Instance.Error("Received output for unknown terminal Id {id}", terminalId);
}
break;
case (byte) MessageIdentifiers.TerminalExitedRequest:
var request = JsonConvert.DeserializeObject<TerminalExitedRequest>((string)messageContent);
Logger.Instance.Debug("Received TerminalExitedRequest: {@request}", request);
TerminalExited?.Invoke(this, request.ToStatus());
break;
default:
Logger.Instance.Error("Received unknown message type: {messageType}", messageType);
break;
}
}
public Task ResizeTerminalAsync(byte id, TerminalSize size)
{
return _appServiceConnection.SendMessageAsync(CreateMessage(new ResizeTerminalRequest
{TerminalId = id, NewSize = size}));
}
public void SubscribeForTerminalOutput(byte terminalId, Action<byte[]> callback)
{
_terminalOutputHandlers[terminalId] = callback;
}
public void UnsubscribeFromTerminalOutput(byte id)
{
if (_terminalOutputHandlers.ContainsKey(id))
{
_terminalOutputHandlers.Remove(id);
}
}
public Task UpdateToggleWindowKeyBindingsAsync()
{
var keyBindings = _settingsService.GetCommandKeyBindings()[nameof(Command.ToggleWindow)];
return _appServiceConnection.SendMessageAsync(CreateMessage(new SetToggleWindowKeyBindingsRequest
{KeyBindings = keyBindings}));
}
public Task WriteAsync(byte id, byte[] data)
{
var message = new ValueSet
{
[MessageKeys.Type] = Constants.TerminalBufferRequestIdentifier,
[MessageKeys.TerminalId] = id,
[MessageKeys.Content] = data
};
return _appServiceConnection.SendMessageAsync(message);
}
public Task CloseTerminalAsync(byte terminalId)
{
var request = new TerminalExitedRequest(terminalId, -1);
Logger.Instance.Debug("Sending TerminalExitedRequest: {@request}", request);
return _appServiceConnection.SendMessageAsync(CreateMessage(request));
}
public async Task<string> GetCommandPathAsync(string command)
{
if (string.IsNullOrWhiteSpace(command))
{
throw new ArgumentException("Input value is null or empty.", nameof(command));
}
command = command.Trim();
var commandLower = command.ToLowerInvariant();
if (CommandPaths.TryGetValue(commandLower, out var path))
{
return path;
}
if (CommandErrors.TryGetValue(commandLower, out var error))
{
throw new Exception(error);
}
var response = await GetResponseAsync<StringValueResponse>(new GetCommandPathRequest {Command = command})
.ConfigureAwait(false);
if (response.Success)
{
CommandPaths[commandLower] = response.Value;
return response.Value;
}
CommandErrors[commandLower] = response.Error;
throw new Exception(response.Error);
}
public Task QuitApplicationAsync()
{
return _appServiceConnection.SendMessageAsync(CreateMessage(new QuitApplicationRequest()));
}
private async Task<T> GetResponseAsync<T>(IMessage request)
{
var messageResponse =
await _appServiceConnection.SendMessageAsync(CreateMessage(request)).ConfigureAwait(false);
return JsonConvert.DeserializeObject<T>((string)messageResponse[MessageKeys.Content]);
}
}
}
@@ -0,0 +1,56 @@
using System;
using Windows.ApplicationModel;
using Newtonsoft.Json;
using RestSharp;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services.Implementation
{
public class UpdateService : IUpdateService
{
private const string apiEndpoint = "https://api.github.com";
private readonly INotificationService _notificationService;
public UpdateService(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task CheckForUpdateAsync(bool notifyNoUpdate = false)
{
var latest = await GetLatestVersionAsync().ConfigureAwait(false);
if (latest > GetCurrentVersion())
{
_notificationService.ShowNotification("Update available",
"Click to open the releases page.", "https://github.com/felixse/FluentTerminal/releases");
}
else if (notifyNoUpdate)
{
_notificationService.ShowNotification("No update available", "You're up to date!");
}
}
public Version GetCurrentVersion()
{
var currentVersion = Package.Current.Id.Version;
return new Version(currentVersion.Major, currentVersion.Minor, currentVersion.Build, currentVersion.Revision);
}
public async Task<Version> GetLatestVersionAsync()
{
var restClient = new RestClient(apiEndpoint);
var restRequest = new RestRequest("/repos/felixse/fluentterminal/releases", Method.GET);
var restResponse = await restClient.ExecuteAsync(restRequest).ConfigureAwait(false);
if (restResponse.IsSuccessful)
{
dynamic restResponseData = JsonConvert.DeserializeObject(restResponse.Content);
string tag = restResponseData[0].tag_name;
var latestVersion = new Version(tag);
return new Version(latestVersion.Major, latestVersion.Minor, latestVersion.Build, latestVersion.Revision);
}
return new Version(0, 0, 0, 0);
}
}
}
+68
View File
@@ -0,0 +1,68 @@
using Serilog;
using Serilog.Events;
using System;
namespace FluentTerminal.App.Services
{
public sealed class Logger
{
public enum LogLevel
{
Verbose = 0,
Debug = 1,
Information = 2,
Warning = 3,
Error = 4,
Fatal = 5
}
public class Configuration
{
public LogLevel LogLevel { get; set; } = LogLevel.Error;
}
public static Logger Instance { get; } = new Logger();
private Logger()
{
}
public void Initialize(string filePath, Configuration configuration)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(filePath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7))
.MinimumLevel.Is((LogEventLevel)configuration.LogLevel)
.CreateLogger();
// Log file is not created during unhandled exception processing.
// Force log file creation with the highest severity initialization record.
Log.Fatal("Initialized");
}
public void Debug(string text, params object[] propertyValues)
{
Log.Debug(text, propertyValues, propertyValues);
}
public void Information(string text, params object[] propertyValues)
{
Log.Information(text, propertyValues);
}
public void Warning(string text, params object[] propertyValues)
{
Log.Warning(text, propertyValues);
}
public void Error(string text, params object[] propertyValues)
{
Log.Error(text, propertyValues);
}
public void Error(Exception exception, string text, params object[] propertyValues)
{
Log.Error(exception, text, propertyValues);
}
}
}
+178
View File
@@ -0,0 +1,178 @@
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using FluentTerminal.Models.Responses;
using System;
using System.Text;
using System.Threading.Tasks;
namespace FluentTerminal.App.Services
{
public class Terminal
{
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private Func<Task<string>> _selectedTextCallback;
private bool _closingFromUi;
private bool _exited;
private readonly bool _requireShellProcessStart;
private string _fallbackTitle;
public Terminal(ITrayProcessCommunicationService trayProcessCommunicationService, byte? terminalId = null)
{
_trayProcessCommunicationService = trayProcessCommunicationService;
_trayProcessCommunicationService.TerminalExited += OnTerminalExited;
Id = terminalId ?? trayProcessCommunicationService.GetNextTerminalId();
_requireShellProcessStart = !terminalId.HasValue;
}
private void OnTerminalExited(object sender, TerminalExitStatus status)
{
if (status.TerminalId != Id)
{
return;
}
_exited = true;
Exited?.Invoke(this, status.ExitCode);
if (_closingFromUi || status.ExitCode <= 0)
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
_trayProcessCommunicationService.TerminalExited -= OnTerminalExited;
}
public event EventHandler<int> Exited;
/// <summary>
/// To be observed by both view and viewmodel
/// </summary>
public event EventHandler Closed;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> KeyboardCommandReceived;
/// <summary>
/// to be observed by view
/// </summary>
public event EventHandler<byte[]> OutputReceived;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<TerminalSize> SizeChanged;
/// <summary>
/// to be observed by viewmodel
/// </summary>
public event EventHandler<string> TitleChanged;
public byte Id { get; }
/// <summary>
/// To be called by either view or viewmodel
/// </summary>
public Task CloseAsync()
{
if (_exited)
{
Closed?.Invoke(this, System.EventArgs.Empty);
return Task.CompletedTask;
}
_closingFromUi = true;
_trayProcessCommunicationService.UnsubscribeFromTerminalOutput(Id);
return _trayProcessCommunicationService.CloseTerminalAsync(Id);
}
/// <summary>
/// to be called by viewmodel
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedText()
{
return _selectedTextCallback?.Invoke();
}
/// <summary>
/// to be called by view
/// </summary>
public void ProcessKeyboardCommand(string command)
{
KeyboardCommandReceived?.Invoke(this, command);
}
/// <summary>
/// To be set by view
/// </summary>
public void RegisterSelectedTextCallback(Func<Task<string>> selectedTextCallback)
{
_selectedTextCallback = selectedTextCallback;
}
/// <summary>
/// To be called by view
/// </summary>
public async Task SetSizeAsync(TerminalSize size)
{
await _trayProcessCommunicationService.ResizeTerminalAsync(Id, size).ConfigureAwait(false);
SizeChanged?.Invoke(this, size);
}
/// <summary>
/// to be called by view
/// </summary>
public void SetTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
title = _fallbackTitle;
}
TitleChanged?.Invoke(this, title);
}
/// <summary>
/// To be called by view when ready
/// </summary>
public async Task<TerminalResponse> StartShellProcessAsync(ShellProfile shellProfile, TerminalSize size, SessionType sessionType, string termState)
{
if (!_requireShellProcessStart && !string.IsNullOrEmpty(termState))
{
OutputReceived?.Invoke(this, Encoding.UTF8.GetBytes(termState));
}
_trayProcessCommunicationService.SubscribeForTerminalOutput(Id, t => OutputReceived?.Invoke(this, t));
if (_requireShellProcessStart)
{
var response = await _trayProcessCommunicationService
.CreateTerminalAsync(Id, size, shellProfile, sessionType).ConfigureAwait(false);
if (response.Success)
{
_fallbackTitle = response.Name;
SetTitle(_fallbackTitle);
}
return response;
}
else
{
return await _trayProcessCommunicationService.PauseTerminalOutputAsync(Id, false);
}
}
public Task Write(byte[] data)
{
return _trayProcessCommunicationService.WriteAsync(Id, data);
}
/// <summary>
/// Tells the ViewModel that this view failed to launch and can be closed.
/// </summary>
public void ReportLauchFailed()
{
Closed?.Invoke(this, System.EventArgs.Empty);
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.ComponentModel;
namespace FluentTerminal.App.Services.Utilities
{
public static class EnumHelper
{
public static string GetEnumDescription(Enum value)
{
var fieldInfo = value.GetType().GetField(value.ToString());
var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes?.Length > 0)
{
return attributes[0].Description;
}
return value.ToString();
}
}
}
@@ -0,0 +1,25 @@
using Windows.ApplicationModel.Resources;
namespace FluentTerminal.App.Services.Utilities
{
/// <summary>
/// Get strings in different languages.
/// Author: Zuozishi
/// </summary>
public static class I18N
{
private static readonly ResourceLoader ResourceLoader = ResourceLoader.GetForViewIndependentUse();
public static string Translate(string resource)
{
return ResourceLoader.GetString(resource.Replace('.', '/'));
}
public static string TranslateWithFallback(string resource, string fallback)
{
var result = Translate(resource);
return string.IsNullOrEmpty(result) ? fallback : result;
}
}
}
@@ -0,0 +1,21 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace FluentTerminal.App.Services.Utilities
{
public class PreserveDictionaryKeyCaseContractResolver : DefaultContractResolver
{
public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
{ ContractResolver = new PreserveDictionaryKeyCaseContractResolver() };
protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
contract.DictionaryKeyResolver = key => key;
return contract;
}
}
}
@@ -0,0 +1,27 @@
using FluentTerminal.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Reflection;
namespace FluentTerminal.App.Services.Utilities
{
public class TerminalThemeContractResolver : PreserveDictionaryKeyCaseContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (property.DeclaringType == typeof(TerminalTheme) && property.PropertyName == nameof(TerminalTheme.PreInstalled))
{
property.ShouldSerialize = (_) => false;
}
if (property.DeclaringType == typeof(TerminalTheme) && property.PropertyName == nameof(TerminalTheme.Id))
{
property.ShouldSerialize = (_) => false;
}
return property;
}
}
}
@@ -0,0 +1,101 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
internal class DelayedHistorySaver : ISessionSuccessTracker
{
private const int DelayMilliseconds = 5000;
private readonly Action _saveAction;
private readonly object _lock = new object();
private CancellationTokenSource _cts;
private bool _sessionStarted;
private bool _outputReceived;
private bool _toSave = true;
internal DelayedHistorySaver(Action saveAction)
{
_saveAction = saveAction;
}
public void SetSuccessfulSessionStart()
{
lock (_lock)
{
if (!_toSave || _sessionStarted)
{
return;
}
_sessionStarted = true;
if (_outputReceived)
{
InitiateSave();
}
}
}
public void SetOutputReceived()
{
lock (_lock)
{
if (!_toSave || _outputReceived)
{
return;
}
_outputReceived = true;
if (_sessionStarted)
{
InitiateSave();
}
}
}
public void SetInvalid()
{
lock (_lock)
{
if (!_toSave || !(_cts is CancellationTokenSource cts))
{
return;
}
_toSave = false;
cts.Cancel();
cts.Dispose();
_cts = null;
}
}
private void InitiateSave()
{
_cts = new CancellationTokenSource();
Task.Delay(DelayMilliseconds, _cts.Token).ContinueWith(t =>
{
lock (_lock)
{
if (_toSave)
{
_saveAction.Invoke();
}
_toSave = false;
_cts?.Dispose();
_cts = null;
}
});
}
}
}
@@ -0,0 +1,25 @@
using GalaSoft.MvvmLight;
using System;
namespace FluentTerminal.App.ViewModels
{
public class EnvironmentVariableViewModel : ViewModelBase
{
private string _name;
private string _value;
public Guid Id { get; } = Guid.NewGuid();
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public string Value
{
get => _value;
set => Set(ref _value, value);
}
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MvvmLightLibs" Version="5.4.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FluentTerminal.App.Services\FluentTerminal.App.Services.csproj" />
<ProjectReference Include="..\FluentTerminal.Models\FluentTerminal.Models.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Windows.Foundation.UniversalApiContract">
<HintPath>$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.16299.0\Windows.Foundation.UniversalApiContract\5.0.0.0\Windows.Foundation.UniversalApiContract.winmd</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
namespace FluentTerminal.App.ViewModels
{
public interface ISessionSuccessTracker
{
void SetSuccessfulSessionStart();
void SetOutputReceived();
void SetInvalid();
}
}
@@ -0,0 +1,20 @@
using System;
using FluentTerminal.Models;
using System.Threading.Tasks;
namespace FluentTerminal.App.ViewModels
{
public interface ITerminalView : IDisposable
{
Task ChangeThemeAsync(TerminalTheme theme);
Task ChangeKeyBindingsAsync();
Task ChangeOptionsAsync(TerminalOptions options);
Task InitializeAsync(TerminalViewModel viewModel);
void DisposalPrepare();
Task FindNextAsync(SearchRequest request);
Task FindPreviousAsync(SearchRequest request);
Task FocusTerminalAsync();
Task<string> SerializeXtermStateAsync();
void Paste(string text);
}
}
@@ -0,0 +1,125 @@
using FluentTerminal.App.ViewModels.Utilities;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Infrastructure
{
public class AsyncCommand : IAsyncCommand
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(
Func<Task> execute,
Func<bool> canExecute = null,
IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute()
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async Task ExecuteAsync()
{
if (CanExecute())
{
try
{
// TODO: If we're using class field _isExecuting, maybe we should introduce some locking?
_isExecuting = true;
await _execute();
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute();
}
void ICommand.Execute(object parameter)
{
ExecuteAsync().FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
public class AsyncCommand<T> : IAsyncCommand<T>
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<T, Task> _execute;
private readonly Func<T, bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(Func<T, Task> execute, Func<T, bool> canExecute = null, IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute(T parameter)
{
return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
}
public async Task ExecuteAsync(T parameter)
{
if (CanExecute(parameter))
{
try
{
_isExecuting = true;
await _execute(parameter);
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute((T)parameter);
}
void ICommand.Execute(object parameter)
{
ExecuteAsync((T)parameter).FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
}
@@ -0,0 +1,20 @@
using System.Threading.Tasks;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels.Infrastructure
{
/// <summary>
/// based on: https://github.com/johnthiriet/AsyncVoid
/// </summary>
public interface IAsyncCommand : ICommand
{
Task ExecuteAsync();
bool CanExecute();
}
public interface IAsyncCommand<in T> : ICommand
{
Task ExecuteAsync(T parameter);
bool CanExecute(T parameter);
}
}
@@ -0,0 +1,9 @@
using System;
namespace FluentTerminal.App.ViewModels.Infrastructure
{
public interface IErrorHandler
{
void HandleError(Exception ex);
}
}
@@ -0,0 +1,820 @@
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.EventArgs;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.Models;
using FluentTerminal.Models.Enums;
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Core;
using FluentTerminal.App.ViewModels.Menu;
using FluentTerminal.Models.Messages;
using GalaSoft.MvvmLight.Command;
using FluentTerminal.App.ViewModels.Infrastructure;
using System.Windows.Input;
namespace FluentTerminal.App.ViewModels
{
public class MainViewModel : ViewModelBase
{
private readonly IClipboardService _clipboardService;
private readonly IDialogService _dialogService;
private readonly IKeyboardCommandService _keyboardCommandService;
private readonly ISettingsService _settingsService;
private readonly ITrayProcessCommunicationService _trayProcessCommunicationService;
private readonly ICommandHistoryService _commandHistoryService;
private readonly IAcceleratorKeyValidator _acceleratorKeyValidator;
private ApplicationSettings _applicationSettings;
private bool _useAcrylicBackground;
private double _backgroundOpacity;
private TerminalViewModel _selectedTerminal;
private TabsPosition _tabsPosition;
private string _windowTitle;
private IDictionary<string, ICollection<KeyBinding>> _keyBindings;
public MainViewModel(ISettingsService settingsService, ITrayProcessCommunicationService trayProcessCommunicationService, IDialogService dialogService, IKeyboardCommandService keyboardCommandService,
IApplicationView applicationView, IClipboardService clipboardService, ICommandHistoryService commandHistoryService, IAcceleratorKeyValidator acceleratorKeyValidator)
{
MessengerInstance.Register<ApplicationSettingsChangedMessage>(this, OnApplicationSettingsChanged);
MessengerInstance.Register<ShellProfileAddedMessage>(this, OnShellProfileAdded);
MessengerInstance.Register<ShellProfileDeletedMessage>(this, OnShellProfileDeleted);
MessengerInstance.Register<ShellProfileChangedMessage>(this, OnShellProfileChanged);
MessengerInstance.Register<DefaultShellProfileChangedMessage>(this, OnDefaultShellProfileChanged);
MessengerInstance.Register<TerminalOptionsChangedMessage>(this, OnTerminalOptionsChanged);
MessengerInstance.Register<CommandHistoryChangedMessage>(this, OnCommandHistoryChanged);
MessengerInstance.Register<KeyBindingsChangedMessage>(this, OnKeyBindingChanged);
_settingsService = settingsService;
_trayProcessCommunicationService = trayProcessCommunicationService;
_dialogService = dialogService;
ApplicationView = applicationView;
_clipboardService = clipboardService;
_keyboardCommandService = keyboardCommandService;
_commandHistoryService = commandHistoryService;
_acceleratorKeyValidator = acceleratorKeyValidator;
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewTab), async () => await AddDefaultProfileAsync(NewTerminalLocation.Tab));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewWindow), async () => await AddDefaultProfileAsync(NewTerminalLocation.Window));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshTab), async () => await AddSshProfileAsync(NewTerminalLocation.Tab));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewSshWindow), async () => await AddSshProfileAsync(NewTerminalLocation.Window));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewCustomCommandTab), async () => await AddQuickLaunchProfileAsync(NewTerminalLocation.Tab));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NewCustomCommandWindow), async () => await AddQuickLaunchProfileAsync(NewTerminalLocation.Window));
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ChangeTabTitle), async () => await SelectedTerminal.EditTitleAsync());
_keyboardCommandService.RegisterCommandHandler(nameof(Command.CloseTab), CloseCurrentTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.DuplicateTab), async () => await AddTabAsync(SelectedTerminal.ShellProfile.Clone()));
// Add all of the commands for switching to a tab of a given ID, if there's one open there
for (int i = 0; i < 9; i++)
{
var switchCmd = Command.SwitchToTerm1 + i;
int tabNumber = i;
// ReSharper disable once InconsistentNaming
void handler() => SelectTabNumber(tabNumber);
_keyboardCommandService.RegisterCommandHandler(switchCmd.ToString(), handler);
}
_keyboardCommandService.RegisterCommandHandler(nameof(Command.NextTab), SelectNextTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.PreviousTab), SelectPreviousTab);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ShowSettings), ShowSettings);
_keyboardCommandService.RegisterCommandHandler(nameof(Command.ToggleFullScreen), ToggleFullScreen);
foreach (ShellProfile profile in _settingsService.GetShellProfiles())
{
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), async () => await AddProfileByGuidAsync(profile.Id));
}
foreach (SshProfile profile in _settingsService.GetSshProfiles())
{
_keyboardCommandService.RegisterCommandHandler(profile.Id.ToString(), async () => await AddProfileByGuidAsync(profile.Id));
}
var currentTheme = _settingsService.GetCurrentTheme();
var options = _settingsService.GetTerminalOptions();
UseAcrylicBackground = options.UseAcrylicBackground;
BackgroundOpacity = options.BackgroundOpacity;
_applicationSettings = _settingsService.GetApplicationSettings();
TabsPosition = _applicationSettings.TabsPosition;
AddDefaultTabCommand = new RelayCommand(async () => await AddDefaultProfileAsync(NewTerminalLocation.Tab));
ApplicationView.CloseRequested += OnCloseRequest;
ApplicationView.Closed += OnClosed;
Terminals.CollectionChanged += OnTerminalsCollectionChanged;
LoadKeyBindings();
_newDefaultTabCommand = new RelayCommand(async () => await AddDefaultProfileAsync(NewTerminalLocation.Tab));
_newDefaultWindowCommand = new RelayCommand(async () => await AddDefaultProfileAsync(NewTerminalLocation.Window));
_newRemoteTabCommand = new RelayCommand(async () => await AddSshProfileAsync(NewTerminalLocation.Tab));
_newRemoteWindowCommand = new RelayCommand(async () => await AddSshProfileAsync(NewTerminalLocation.Window));
_newQuickLaunchTabCommand = new RelayCommand(async () => await AddQuickLaunchProfileAsync(NewTerminalLocation.Tab));
_newQuickLaunchWindowCommand = new RelayCommand(async () => await AddQuickLaunchProfileAsync(NewTerminalLocation.Window));
_settingsCommand = new RelayCommand(ShowSettings);
_aboutCommand = new RelayCommand(async () => await _dialogService.ShowAboutDialogAsync());
_quitCommand = new AsyncCommand(() => _trayProcessCommunicationService.QuitApplicationAsync());
_defaultProfile = _settingsService.GetDefaultShellProfile();
CreateMenuViewModel();
}
private ShellProfile _defaultProfile;
private void UpdateDefaultShellProfile()
{
var defaultProfile = _settingsService.GetDefaultShellProfile();
// We need to rebuild the menu only if default profile Id or Name is changed
var changeMenu = _defaultProfile == null || !_defaultProfile.Id.Equals(defaultProfile.Id) ||
!string.Equals(_defaultProfile.Name, defaultProfile.Name, StringComparison.Ordinal);
_defaultProfile = defaultProfile;
if (changeMenu)
{
ApplicationView.ExecuteOnUiThreadAsync(CreateMenuViewModel, CoreDispatcherPriority.Low, true);
}
}
private void LoadKeyBindings() => _keyBindings = _settingsService.GetCommandKeyBindings();
private void OnClosed(object sender, EventArgs e)
{
MessengerInstance.Unregister(this);
ApplicationView.CloseRequested -= OnCloseRequest;
ApplicationView.Closed -= OnClosed;
Terminals.CollectionChanged -= OnTerminalsCollectionChanged;
_keyboardCommandService.ClearCommandHandlers();
_applicationSettings = null;
AddDefaultTabCommand = null;
Closed?.Invoke(this, e);
}
private void OnShellProfileDeleted(ShellProfileDeletedMessage message)
{
_keyboardCommandService.DeregisterCommandHandler(message.ProfileId.ToString());
UpdateDefaultShellProfile();
}
private void OnShellProfileChanged(ShellProfileChangedMessage message)
{
UpdateDefaultShellProfile();
}
private void OnShellProfileAdded(ShellProfileAddedMessage message)
{
_keyboardCommandService.RegisterCommandHandler(message.ShellProfile.Id.ToString(),
async () => await AddProfileByGuidAsync(message.ShellProfile.Id));
UpdateDefaultShellProfile();
}
private void OnDefaultShellProfileChanged(DefaultShellProfileChangedMessage message)
{
UpdateDefaultShellProfile();
}
private void OnTerminalsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged(nameof(ShowTabsOnTop));
RaisePropertyChanged(nameof(ShowTabsOnBottom));
}
public event EventHandler Closed;
public event EventHandler<NewWindowRequestedEventArgs> NewWindowRequested;
public event EventHandler ShowSettingsRequested;
public event EventHandler ActivatedMv;
public event EventHandler<TerminalViewModel> TabTearedOff;
public void TearOffTab(TerminalViewModel model)
{
TabTearedOff?.Invoke(this, model);
}
public void FocusWindow()
{
ActivatedMv?.Invoke(this, EventArgs.Empty);
}
public RelayCommand AddDefaultTabCommand { get; private set; }
public string WindowTitle
{
get => _windowTitle;
set
{
if (Set(ref _windowTitle, value))
{
ApplicationView.Title = value;
}
}
}
public bool ShowTabsOnTop
{
get => TabsPosition == TabsPosition.Top;
}
public bool ShowTabsOnBottom
{
get => TabsPosition == TabsPosition.Bottom;
}
public bool UseAcrylicBackground
{
get => _useAcrylicBackground;
set => Set(ref _useAcrylicBackground, value);
}
public double BackgroundOpacity
{
get => _backgroundOpacity;
set => Set(ref _backgroundOpacity, value);
}
public TerminalViewModel SelectedTerminal
{
get => _selectedTerminal;
set
{
var oldValue = _selectedTerminal;
if (Set(ref _selectedTerminal, value))
{
if (oldValue != null)
{
oldValue.IsSelected = false;
}
if (_selectedTerminal != null)
{
_selectedTerminal.IsSelected = true;
_selectedTerminal.FocusTerminal();
SetWindowTitle(_selectedTerminal);
}
}
}
}
public TabsPosition TabsPosition
{
get => _tabsPosition;
set => Set(ref _tabsPosition, value);
}
public ObservableCollection<TerminalViewModel> Terminals { get; } = new ObservableCollection<TerminalViewModel>();
public IApplicationView ApplicationView { get; }
#region Launching terminal sessions
public Task AddDefaultProfileAsync(NewTerminalLocation location)
{
var profile = _settingsService.GetDefaultShellProfile();
return AddProfileAsync(profile, location);
}
private async Task AddSshProfileAsync(NewTerminalLocation location)
{
var profile = await _dialogService.ShowSshConnectionInfoDialogAsync().ConfigureAwait(true);
await AddProfileAsync(profile, location).ConfigureAwait(false);
}
private async Task AddQuickLaunchProfileAsync(NewTerminalLocation location)
{
var profile = await _dialogService.ShowCustomCommandDialogAsync().ConfigureAwait(true);
await AddProfileAsync(profile, location).ConfigureAwait(false);
}
public Task AddProfileByGuidAsync(Guid profileId) =>
AddProfileByGuidAsync(profileId, _applicationSettings.NewTerminalLocation);
private async Task AddProfileByGuidAsync(Guid profileId, NewTerminalLocation location)
{
var profile = _settingsService.GetShellProfile(profileId) ?? _settingsService.GetSshProfile(profileId);
await AddProfileAsync(profile, location).ConfigureAwait(false);
}
// For serialization
public Task AddTabAsync(string terminalState, int position)
{
return AddTabAsync(new ShellProfile(), terminalState, position);
}
private Task AddProfileAsync(ShellProfile profile) =>
AddProfileAsync(profile, _applicationSettings.NewTerminalLocation);
private Task AddProfileAsync(ShellProfile profile, NewTerminalLocation location)
{
if (profile == null)
{
return Task.CompletedTask;
}
if (location == NewTerminalLocation.Tab)
{
return AddTabAsync(profile);
}
NewWindowRequested?.Invoke(this, new NewWindowRequestedEventArgs(profile));
return Task.CompletedTask;
}
public Task AddTabAsync(ShellProfile profile) => AddTabAsync(profile, string.Empty, Terminals.Count);
private Task AddTabAsync(ShellProfile profile, string terminalState, int position)
{
profile.Tag = new DelayedHistorySaver(() => _commandHistoryService.MarkUsed(profile));
return ApplicationView.ExecuteOnUiThreadAsync(() =>
{
var terminal = new TerminalViewModel(_settingsService, _trayProcessCommunicationService, _dialogService, _keyboardCommandService,
_applicationSettings, profile, ApplicationView, _clipboardService, terminalState);
terminal.PropertyChanged += Terminal_PropertyChanged;
terminal.Closed += OnTerminalClosed;
terminal.CloseLeftTabsRequested += Terminal_CloseLeftTabsRequested;
terminal.CloseRightTabsRequested += Terminal_CloseRightTabsRequested;
terminal.CloseOtherTabsRequested += Terminal_CloseOtherTabsRequested;
terminal.DuplicateTabRequested += Terminal_DuplicateTabRequested;
Terminals.Insert(Math.Min(position, Terminals.Count), terminal);
SelectedTerminal = terminal;
});
}
#endregion Launching terminal sessions
private void Terminal_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (!(sender is TerminalViewModel terminalViewModel))
{
return;
}
switch (e.PropertyName)
{
case nameof(TerminalViewModel.ShellTitle):
case nameof(TerminalViewModel.TabTitle):
SetWindowTitle(terminalViewModel);
break;
}
}
// ReSharper disable once UnusedMethodReturnValue.Local
private Task SetWindowTitle(TerminalViewModel terminalViewModel)
{
return ApplicationView.ExecuteOnUiThreadAsync(() =>
{
WindowTitle = !_applicationSettings.ShowCustomTitleInTitlebar ||
string.IsNullOrEmpty(terminalViewModel?.TabTitle)
? terminalViewModel?.ShellTitle
: terminalViewModel.TabTitle;
}, CoreDispatcherPriority.Low);
}
private async void Terminal_DuplicateTabRequested(object sender, EventArgs e)
{
if (sender is TerminalViewModel terminal)
{
await AddTabAsync(terminal.ShellProfile.Clone()).ConfigureAwait(false);
}
}
private async void Terminal_CloseOtherTabsRequested(object sender, EventArgs e)
{
if (sender is TerminalViewModel terminal)
{
await Task.WhenAll(Terminals.Where(t => t != terminal).Select(t =>
{
Logger.Instance.Debug("Terminal with Id: {@id} closed.", t.Terminal.Id);
return t.CloseCommand.ExecuteAsync();
})).ConfigureAwait(false);
}
}
private async void Terminal_CloseRightTabsRequested(object sender, EventArgs e)
{
if (sender is TerminalViewModel terminal)
{
var toRemove = Terminals.Skip(Terminals.IndexOf(terminal) + 1).ToList();
await Task.WhenAll(toRemove.Select(t =>
{
Logger.Instance.Debug("Terminal with Id: {@id} closed.", t.Terminal.Id);
return t.CloseCommand.ExecuteAsync();
})).ConfigureAwait(false);
}
}
private async void Terminal_CloseLeftTabsRequested(object sender, EventArgs e)
{
if (sender is TerminalViewModel terminal)
{
var toRemove = Terminals.Take(Terminals.IndexOf(terminal)).ToList();
await Task.WhenAll(toRemove.Select(t =>
{
Logger.Instance.Debug("Terminal with Id: {@id} closed.", t.Terminal.Id);
return t.CloseCommand.ExecuteAsync();
})).ConfigureAwait(false);
}
}
private Task CloseAllTerminalsAsync()
{
return Task.WhenAll(Terminals.Select(terminal =>
{
terminal.PropertyChanged -= Terminal_PropertyChanged;
terminal.Closed -= OnTerminalClosed;
terminal.CloseLeftTabsRequested -= Terminal_CloseLeftTabsRequested;
terminal.CloseRightTabsRequested -= Terminal_CloseRightTabsRequested;
terminal.CloseOtherTabsRequested -= Terminal_CloseOtherTabsRequested;
terminal.DuplicateTabRequested -= Terminal_DuplicateTabRequested;
return terminal.CloseAsync();
}));
}
private void CloseCurrentTab()
{
SelectedTerminal?.CloseCommand.ExecuteAsync();
}
private void OnApplicationSettingsChanged(ApplicationSettingsChangedMessage message)
{
var updateMenu = _applicationSettings != null &&
(_applicationSettings.TabWindowCascadingAppMenu !=
message.ApplicationSettings.TabWindowCascadingAppMenu ||
!message.ApplicationSettings.TabWindowCascadingAppMenu &&
_applicationSettings.NewTerminalLocation !=
message.ApplicationSettings.NewTerminalLocation);
_applicationSettings = message.ApplicationSettings;
SetWindowTitle(SelectedTerminal);
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
TabsPosition = message.ApplicationSettings.TabsPosition;
RaisePropertyChanged(nameof(ShowTabsOnTop));
RaisePropertyChanged(nameof(ShowTabsOnBottom));
});
if (updateMenu)
{
ApplicationView.ExecuteOnUiThreadAsync(CreateMenuViewModel, CoreDispatcherPriority.Low, true);
}
}
private async Task OnCloseRequest(object sender, CancelableEventArgs e)
{
if (_applicationSettings.ConfirmClosingWindows)
{
var result = await _dialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmCloseWindow"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(false);
if (result == DialogButton.OK)
{
await CloseAllTerminalsAsync().ConfigureAwait(false);
}
else
{
e.Cancelled = true;
}
}
else
{
await CloseAllTerminalsAsync().ConfigureAwait(false);
}
}
private void OnTerminalClosed(object sender, EventArgs e)
{
if (!(sender is TerminalViewModel terminal))
{
return;
}
Logger.Instance.Debug("Terminal with Id: {@id} closed.", terminal.Terminal.Id);
terminal.PropertyChanged -= Terminal_PropertyChanged;
terminal.Closed -= OnTerminalClosed;
terminal.CloseLeftTabsRequested -= Terminal_CloseLeftTabsRequested;
terminal.CloseRightTabsRequested -= Terminal_CloseRightTabsRequested;
terminal.CloseOtherTabsRequested -= Terminal_CloseOtherTabsRequested;
terminal.DuplicateTabRequested -= Terminal_DuplicateTabRequested;
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
var wasSelected = _selectedTerminal == terminal;
Terminals.Remove(terminal);
if (Terminals.Count == 0)
{
ApplicationView.TryCloseAsync();
}
else if (wasSelected)
{
SelectedTerminal = Terminals.LastOrDefault(t => t != terminal);
}
});
}
private void OnTerminalOptionsChanged(TerminalOptionsChangedMessage message)
{
ApplicationView.ExecuteOnUiThreadAsync(() =>
{
BackgroundOpacity = message.TerminalOptions.BackgroundOpacity;
UseAcrylicBackground = message.TerminalOptions.UseAcrylicBackground;
}, CoreDispatcherPriority.Low);
}
private void OnKeyBindingChanged(KeyBindingsChangedMessage message)
{
LoadKeyBindings();
ApplicationView.ExecuteOnUiThreadAsync(CreateMenuViewModel, CoreDispatcherPriority.Low, true);
}
private void OnCommandHistoryChanged(CommandHistoryChangedMessage message)
{
ApplicationView.ExecuteOnUiThreadAsync(CreateMenuViewModel, CoreDispatcherPriority.Low, true);
}
private void SelectTabNumber(int tabNumber)
{
if (tabNumber < Terminals.Count)
{
SelectedTerminal = Terminals[tabNumber];
}
}
private void SelectNextTab()
{
var currentIndex = Terminals.IndexOf(SelectedTerminal);
var nextIndex = (currentIndex + 1) % Terminals.Count;
SelectedTerminal = Terminals[nextIndex];
}
private void SelectPreviousTab()
{
var currentIndex = Terminals.IndexOf(SelectedTerminal);
var previousIndex = (currentIndex - 1 + Terminals.Count) % Terminals.Count;
SelectedTerminal = Terminals[previousIndex];
}
public void ShowSettings()
{
ShowSettingsRequested?.Invoke(this, EventArgs.Empty);
}
private void ToggleFullScreen()
{
ApplicationView.ToggleFullScreen();
}
public void OnWindowKeyDown(int key, bool control, bool alt, bool shift, bool meta)
{
var binding = _keyBindings.Values.SelectMany(bs => bs).FirstOrDefault(b =>
b.Key == key && b.Ctrl == control && b.Alt == alt && b.Shift == shift && b.Meta == meta);
if (binding != null)
{
_keyboardCommandService.SendCommand(binding.Command);
}
}
#region App menu
private const int RecentItemsMaxCount = 10;
private MenuViewModel _menuViewModel;
private readonly RelayCommand _newDefaultTabCommand;
private readonly RelayCommand _newDefaultWindowCommand;
private readonly RelayCommand _newRemoteTabCommand;
private readonly RelayCommand _newRemoteWindowCommand;
private readonly RelayCommand _newQuickLaunchTabCommand;
private readonly RelayCommand _newQuickLaunchWindowCommand;
private readonly RelayCommand _settingsCommand;
private readonly RelayCommand _aboutCommand;
private readonly ICommand _quitCommand;
public MenuViewModel AppMenuViewModel
{
get => _menuViewModel;
private set => Set(ref _menuViewModel, value);
}
private void CreateMenuViewModel()
{
var appMenuViewModel = new MenuViewModel();
if (_applicationSettings.TabWindowCascadingAppMenu)
{
var tabItem = new ExpandableMenuItemViewModel(
I18N.TranslateWithFallback("MenuItem_NewTab_Text", "New Tab"),
description: I18N.TranslateWithFallback("MenuItem_NewTab_Text", "Launches a session in a new tab."),
icon: Mdl2Icon.Add());
FillCoreItems(tabItem.SubItems, NewTerminalLocation.Tab);
appMenuViewModel.Items.Add(tabItem);
var windowItem = new ExpandableMenuItemViewModel(
I18N.TranslateWithFallback("MenuItem_NewWindow_Text", "New Window"),
description: I18N.TranslateWithFallback("MenuItem_NewWindow_Description", "Launches a session in a new window."),
icon: Mdl2Icon.NewWindow());
FillCoreItems(windowItem.SubItems, NewTerminalLocation.Window);
appMenuViewModel.Items.Add(windowItem);
}
else
{
FillCoreItems(appMenuViewModel.Items, _applicationSettings.NewTerminalLocation);
}
appMenuViewModel.Items.Add(new ExpandableMenuItemViewModel(I18N.TranslateWithFallback("Recent.Text", "Recent"),
GetRecentMenuItems(), I18N.TranslateWithFallback("Recent_Description", "Recently opened sessions."),
icon: Mdl2Icon.History()));
appMenuViewModel.Items.Add(new SeparatorMenuItemViewModel());
var settingsItem = new MenuItemViewModel(I18N.TranslateWithFallback("Settings.Text", "Settings"),
_settingsCommand, I18N.TranslateWithFallback("Settings_Description", "Opens settings window."),
icon: Mdl2Icon.Settings());
if (_keyBindings.TryGetValue(nameof(Command.ShowSettings), out var keyBindings) &&
keyBindings.FirstOrDefault() is KeyBinding settingsKeyBinding &&
_acceleratorKeyValidator.Valid(settingsKeyBinding.Key))
{
LoadKeyBindingsFromModel(settingsItem, settingsKeyBinding);
}
else
{
settingsItem.KeyBinding = null;
}
appMenuViewModel.Items.Add(settingsItem);
appMenuViewModel.Items.Add(new MenuItemViewModel(I18N.TranslateWithFallback("AboutDialog.Title", "About"), _aboutCommand,
I18N.TranslateWithFallback("About_Description", "Basic info about the app."),
icon: Mdl2Icon.Info()));
appMenuViewModel.Items.Add(new SeparatorMenuItemViewModel());
var quitItem = new MenuItemViewModel(I18N.TranslateWithFallback("Quit.Text", "Quit"), _quitCommand,
I18N.TranslateWithFallback("Quit.Description", "Quit Fluent Terminal"),
icon: Mdl2Icon.Cancel());
appMenuViewModel.Items.Add(quitItem);
if (!appMenuViewModel.EquivalentTo(_menuViewModel))
{
AppMenuViewModel = appMenuViewModel;
}
}
private void FillCoreItems(ObservableCollection<MenuItemViewModelBase> items, NewTerminalLocation location)
{
var tab = location == NewTerminalLocation.Tab;
if (_defaultProfile?.Name is string defaultProfileName)
{
var defaultProfileItem = new MenuItemViewModel(
defaultProfileName, tab ? _newDefaultTabCommand : _newDefaultWindowCommand,
I18N.TranslateWithFallback("MenuItem_DefaultProfile_Description",
"Starts new terminal session based on the default profile."), icon: Mdl2Icon.FavoriteStar());
var defaultProfileCommand = tab ? nameof(Command.NewTab) : nameof(Command.NewWindow);
if (_keyBindings.TryGetValue(defaultProfileCommand, out var kbs) &&
kbs.FirstOrDefault() is KeyBinding tabKeyBindings &&
_acceleratorKeyValidator.Valid(tabKeyBindings.Key))
{
LoadKeyBindingsFromModel(defaultProfileItem, tabKeyBindings);
}
else
{
defaultProfileItem.KeyBinding = null;
}
items.Add(defaultProfileItem);
}
var remoteConnectItem = new MenuItemViewModel(
I18N.TranslateWithFallback("MenuItem_Remote_Text", "Remote Connect..."),
tab ? _newRemoteTabCommand : _newRemoteWindowCommand,
I18N.TranslateWithFallback("MenuItem_Remote_Description",
"Opens a dialog for launching a new SSH or Mosh terminal session."),
icon: Mdl2Icon.Globe());
var command = tab ? nameof(Command.NewSshTab) : nameof(Command.NewSshWindow);
if (_keyBindings.TryGetValue(command, out var keyBindings) &&
keyBindings.FirstOrDefault() is KeyBinding remoteTabKeyBinding &&
_acceleratorKeyValidator.Valid(remoteTabKeyBinding.Key))
{
LoadKeyBindingsFromModel(remoteConnectItem, remoteTabKeyBinding);
}
else
{
remoteConnectItem.KeyBinding = null;
}
items.Add(remoteConnectItem);
var quickLaunchItem = new MenuItemViewModel(
I18N.TranslateWithFallback("MenuItem_QuickLaunch_Text", "Quick Launch..."),
tab ? _newQuickLaunchTabCommand : _newQuickLaunchWindowCommand,
I18N.TranslateWithFallback("MenuItem_QuickLaunch_Description",
"Opens a \"Quick Launch\" dialog for starting a new terminal session."),
icon: Mdl2Icon.Play());
command = tab ? nameof(Command.NewCustomCommandTab) : nameof(Command.NewCustomCommandWindow);
if (_keyBindings.TryGetValue(command, out keyBindings) &&
keyBindings.FirstOrDefault() is KeyBinding quickTabKeyBinding &&
_acceleratorKeyValidator.Valid(quickTabKeyBinding.Key))
{
LoadKeyBindingsFromModel(quickLaunchItem, quickTabKeyBinding);
}
else
{
quickLaunchItem.KeyBinding = null;
}
items.Add(quickLaunchItem);
}
private ObservableCollection<MenuItemViewModel> GetRecentMenuItems() =>
new ObservableCollection<MenuItemViewModel>(_commandHistoryService
.GetHistoryRecentFirst(top: RecentItemsMaxCount).Select(CommandToMenuItem));
private MenuItemViewModel CommandToMenuItem(ExecutedCommand command)
{
var itemCommand = new RelayCommand(async () => await AddProfileAsync(command.ShellProfile),
keepTargetAlive: true);
var keyBinding = command.ShellProfile.KeyBindings?.FirstOrDefault() is KeyBinding kb
? new MenuItemKeyBindingViewModel(kb)
: null;
return new MenuItemViewModel(command.Value, itemCommand, keyBinding: keyBinding);
}
private void LoadKeyBindingsFromModel(MenuItemViewModel menuItemViewModel, KeyBinding keyBinding)
{
if (!_acceleratorKeyValidator.Valid(keyBinding.Key))
{
menuItemViewModel.KeyBinding = null;
return;
}
if (menuItemViewModel.KeyBinding == null)
{
menuItemViewModel.KeyBinding = new MenuItemKeyBindingViewModel(keyBinding);
}
else
{
menuItemViewModel.KeyBinding.Key = keyBinding.Key;
menuItemViewModel.KeyBinding.Ctrl = keyBinding.Ctrl;
menuItemViewModel.KeyBinding.Alt = keyBinding.Alt;
menuItemViewModel.KeyBinding.Shift = keyBinding.Shift;
menuItemViewModel.KeyBinding.Windows = keyBinding.Meta;
}
}
#endregion App menu
}
}
@@ -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;
}
}
}
@@ -0,0 +1,53 @@
using GalaSoft.MvvmLight;
using System;
using Windows.UI.Xaml;
namespace FluentTerminal.App.ViewModels
{
public class OverlayViewModel : ViewModelBase
{
private readonly DispatcherTimer _overlayTimer;
private bool _showOverlay;
private string _overlayContent;
// Important! The constructor has to be called from the UI thread.
public OverlayViewModel()
{
_overlayTimer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 2)};
}
public bool ShowOverlay
{
get => _showOverlay;
set => Set(ref _showOverlay, value);
}
public string OverlayContent
{
get => _overlayContent;
set => Set(ref _overlayContent, value);
}
public void Show(string message)
{
OverlayContent = message;
ShowOverlay = true;
if (_overlayTimer.IsEnabled)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
}
_overlayTimer.Start();
_overlayTimer.Tick += OnResizeOverlayTimerFinished;
}
private void OnResizeOverlayTimerFinished(object sender, object e)
{
_overlayTimer.Stop();
_overlayTimer.Tick -= OnResizeOverlayTimerFinished;
ShowOverlay = false;
}
}
}
@@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.App.ViewModels.Profiles;
using FluentTerminal.App.ViewModels.Settings;
using FluentTerminal.Models;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
namespace FluentTerminal.App.ViewModels
{
/// <summary>
/// Base class for all profile view models. Implements logic for saving, editing, resetting, etc.
/// </summary>
public abstract class ProfileViewModelBase<T> : ViewModelBase where T : ProfileProviderViewModelBase
{
#region Fields
protected readonly IDialogService DialogService;
protected readonly ISettingsService SettingsService;
private List<KeyBinding> _bindingsBeforeEdit;
private Dictionary<string, string> _environmentVariablesBeforeEdit;
private string _nameBeforeEdit;
#endregion Fields
#region Properties
protected bool IsNew { get; private set; }
public Guid Id { get; private set; }
private bool _inEditMode;
public bool InEditMode
{
get => _inEditMode;
set => Set(ref _inEditMode, value);
}
private string _name;
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public KeyBindingsViewModel KeyBindings { get; }
public ObservableCollection<EnvironmentVariableViewModel> EnvironmentVariables { get; } = new ObservableCollection<EnvironmentVariableViewModel>();
public T ProfileVm { get; protected set; }
#endregion Properties
#region Events
// Needs to be triggered from UI thread
public event EventHandler Deleted;
#endregion Events
#region Commands
public IAsyncCommand CancelEditCommand { get; }
public IAsyncCommand DeleteCommand { get; }
public RelayCommand EditCommand { get; }
public IAsyncCommand SaveChangesCommand { get; }
public IAsyncCommand AddKeyboardShortcutCommand { get; }
#endregion Commands
#region Constructor
protected ProfileViewModelBase(ShellProfile shellProfile, ISettingsService settingsService, IDialogService dialogService, bool isNew)
{
SettingsService = settingsService;
DialogService = dialogService;
IsNew = isNew;
KeyBindings = new KeyBindingsViewModel(shellProfile.Id.ToString(), dialogService, string.Empty, false);
foreach (var environmentVariable in shellProfile.EnvironmentVariables)
{
EnvironmentVariables.Add(new EnvironmentVariableViewModel
{
Name = environmentVariable.Key,
Value = environmentVariable.Value
});
}
Initialize(shellProfile);
DeleteCommand = new AsyncCommand(DeleteAsync, CanDelete);
EditCommand = new RelayCommand(Edit);
CancelEditCommand = new AsyncCommand(CancelEditAsync);
SaveChangesCommand = new AsyncCommand(SaveChangesAsync);
AddKeyboardShortcutCommand = new AsyncCommand(AddKeyboardShortcut);
}
#endregion Constructor
#region Methods
// Requires UI thread
// Loads view model properties from the input shellProfile
protected void Initialize(ShellProfile shellProfile)
{
Id = shellProfile.Id;
Name = shellProfile.Name;
KeyBindings.Clear();
foreach (var keyBinding in shellProfile.KeyBindings.Select(x => new KeyBinding(x)).ToList())
{
KeyBindings.Add(keyBinding);
}
}
protected virtual bool HasChanges()
{
return ProfileVm.HasChanges()
|| !_nameBeforeEdit.NullableEqualTo(_name)
|| !(_bindingsBeforeEdit?.SequenceEqual(KeyBindings.KeyBindings.Select(x => x.Model).ToList()) ?? false)
|| !(_environmentVariablesBeforeEdit?.SequenceEqual(EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value)) ?? false);
}
// Requires UI thread
private async Task DeleteAsync()
{
// ConfigureAwait(true) because we need to trigger Deleted event from the calling (UI) thread.
var result = await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDeleteProfile"), DialogButton.OK, DialogButton.Cancel).ConfigureAwait(true);
if (result == DialogButton.OK)
{
Deleted?.Invoke(this, EventArgs.Empty);
}
}
protected abstract bool CanDelete();
private void Edit()
{
if (InEditMode)
{
return;
}
_bindingsBeforeEdit = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
_environmentVariablesBeforeEdit = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
_nameBeforeEdit = _name;
KeyBindings.Editable = true;
InEditMode = true;
}
// Requires UI thread
private async Task CancelEditAsync()
{
if (IsNew)
{
await DeleteAsync().ConfigureAwait(false);
return;
}
if (!HasChanges())
{
KeyBindings.Editable = false;
InEditMode = false;
return;
}
// ConfigureAwait(true) because we're setting some view-model properties afterwards
if (await DialogService.ShowMessageDialogAsync(I18N.Translate("PleaseConfirm"),
I18N.Translate("ConfirmDiscardChanges"), DialogButton.OK, DialogButton.Cancel)
.ConfigureAwait(true) == DialogButton.OK)
{
// Cancelled, so rollback
// ConfigureAwait(true) because we're setting some view-model properties afterwards
await ProfileVm.RejectChangesAsync().ConfigureAwait(true);
KeyBindings.Editable = false;
InEditMode = false;
}
}
// Requires UI thread
private async Task SaveChangesAsync()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var error = await ProfileVm.AcceptChangesAsync().ConfigureAwait(true);
if (!string.IsNullOrEmpty(error))
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"), error, DialogButton.OK)
.ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Any(string.IsNullOrWhiteSpace))
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("EmptyEnvironmentVariableName"), DialogButton.OK).ConfigureAwait(false);
return;
}
if (EnvironmentVariables.Select(x => x.Name).Distinct().Count() != EnvironmentVariables.Count)
{
await DialogService.ShowMessageDialogAsync(I18N.Translate("InvalidInput"),
I18N.Translate("DuplicateEnvironmentVariable"), DialogButton.OK).ConfigureAwait(false);
return;
}
var profile = ProfileVm.Model;
profile.Id = Id;
profile.Name = _name;
profile.KeyBindings = KeyBindings.KeyBindings.Select(x => x.Model).ToList();
profile.EnvironmentVariables = EnvironmentVariables.ToDictionary(x => x.Name, x => x.Value);
if (profile is SshProfile sshProfile)
{
SettingsService.SaveSshProfile(sshProfile, IsNew);
}
else
{
SettingsService.SaveShellProfile(profile, IsNew);
}
KeyBindings.Editable = false;
InEditMode = false;
IsNew = false;
}
private Task AddKeyboardShortcut()
{
return KeyBindings.ShowAddKeyBindingDialogAsync();
}
#endregion Methods
}
}
@@ -0,0 +1,150 @@
using System.Threading.Tasks;
using FluentTerminal.App.Services;
using FluentTerminal.App.Services.Utilities;
using FluentTerminal.App.ViewModels.Infrastructure;
using FluentTerminal.Models;
namespace FluentTerminal.App.ViewModels.Profiles
{
/// <summary>
/// Used for common profiles (PowerShell, CMD, WSL).
/// </summary>
public class CommonProfileProviderViewModel : ProfileProviderViewModelBase
{
#region Fields
private readonly IFileSystemService _fileSystemService;
#endregion Fields
#region Properties
public bool PreInstalled { get; private set; }
private string _location;
public string Location
{
get => _location;
set => Set(ref _location, value);
}
private string _workingDirectory;
public string WorkingDirectory
{
get => _workingDirectory;
set => Set(ref _workingDirectory, value);
}
private string _arguments;
public string Arguments
{
get => _arguments;
set => Set(ref _arguments, value);
}
#endregion Properties
#region Commands
public IAsyncCommand BrowseForCustomShellCommand { get; }
public IAsyncCommand BrowseForWorkingDirectoryCommand { get; }
#endregion Commands
#region Constructor
public CommonProfileProviderViewModel(ISettingsService settingsService, IApplicationView applicationView,
IFileSystemService fileSystemService, ShellProfile original = null) : base(settingsService, applicationView,
true, original)
{
_fileSystemService = fileSystemService;
BrowseForCustomShellCommand = new AsyncCommand(BrowseForCustomShell);
BrowseForWorkingDirectoryCommand = new AsyncCommand(BrowseForWorkingDirectory);
Initialize(Model);
}
#endregion Constructor
#region Methods
private void Initialize(ShellProfile profile)
{
PreInstalled = profile.PreInstalled;
Location = profile.Location;
Arguments = profile.Arguments;
WorkingDirectory = profile.WorkingDirectory;
}
protected override void LoadFromProfile(ShellProfile profile)
{
base.LoadFromProfile(profile);
Initialize(profile);
}
protected override async Task CopyToProfileAsync(ShellProfile profile)
{
await base.CopyToProfileAsync(profile).ConfigureAwait(false);
profile.Location = _location;
profile.Arguments = _arguments;
profile.WorkingDirectory = _workingDirectory;
}
public override async Task<string> ValidateAsync()
{
var error = await base.ValidateAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
return error;
}
if (!string.IsNullOrEmpty(_location))
{
return null;
}
error = I18N.Translate("LocationCantBeEmpty");
return string.IsNullOrEmpty(error) ? "Location cannot be empty." : error;
}
public override bool HasChanges()
{
return base.HasChanges() || !Model.Location.NullableEqualTo(_location) ||
!Model.WorkingDirectory.NullableEqualTo(_workingDirectory) ||
!Model.Arguments.NullableEqualTo(_arguments);
}
// Requires UI thread
private async Task BrowseForCustomShell()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var file = await _fileSystemService.OpenFileAsync(new[] { ".exe" }).ConfigureAwait(true);
if (file != null)
{
Location = file.Path;
}
}
// Requires UI thread
private async Task BrowseForWorkingDirectory()
{
// ConfigureAwait(true) because we're setting some view-model properties afterwards.
var directory = await _fileSystemService.BrowseForDirectoryAsync().ConfigureAwait(true);
if (directory != null)
{
WorkingDirectory = directory;
}
}
#endregion Methods
}
}
Loaded 100 of 760 files, more files were not shown because too many files have changed in this diff. Show more