├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── media ├── demo.gif └── oh-my-posh.png └── src ├── .editorconfig ├── Directory.Build.props ├── MagicTooltips.Logging ├── LoggingService.cs ├── MagicTooltips.Logging.csproj └── Utilities.cs ├── MagicTooltips.psd1 ├── MagicTooltips.psm1 ├── MagicTooltips.sln ├── MagicTooltips ├── Dtos │ ├── HorizontalAlignmentEnum.cs │ ├── ProviderKeys.cs │ └── SettingsDto.cs ├── InternalsVisibleToTests.cs ├── InvokeMagicTooltipsCommand.cs ├── MagicTooltips.csproj ├── Properties │ └── launchSettings.json ├── Providers │ ├── AwsProvider.cs │ ├── AzCLIProvider.cs │ ├── AzPowerShellProvider.cs │ ├── IProvider.cs │ ├── KubernetesProvider.cs │ ├── M365Provider.cs │ ├── Md5Utility.cs │ ├── MicrosoftGraphCLIProvider.cs │ └── MicrosoftGraphPowerShellProvider.cs ├── RegisterMagicTooltipsCommand.cs └── Services │ ├── DependencyService │ ├── DependencyAssemblyLoadContext.cs │ └── ModuleInitializer.cs │ ├── PowershellInvoker.cs │ ├── ProviderFactory.cs │ ├── RenderService.cs │ ├── SettingsService.cs │ └── TriggerService.cs └── MagicTooltipsTests ├── MagicTooltipsTests.csproj ├── RenderServiceTests.cs ├── SettingsServiceTests.cs └── TriggerServiceTests.cs /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy PowerShell Gallery 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | env: 8 | # Stop wasting time caching packages 9 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 10 | # Disable sending usage data to Microsoft 11 | DOTNET_CLI_TELEMETRY_OPTOUT: true 12 | # Project name to pack and publish 13 | PROJECT_NAME: MagicTooltips 14 | PS_GALLERY_KEY: ${{secrets.PS_GALLERY_KEY}} 15 | 16 | jobs: 17 | deploy: 18 | if: github.event_name == 'release' 19 | runs-on: windows-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Setup .NET Core 23 | uses: actions/setup-dotnet@v1 24 | with: 25 | dotnet-version: 7.x 26 | - name: Publish Module to PowerShell Gallery 27 | run: | 28 | $VERSION=($env:GITHUB_REF -split "/" | select -skip 2).TrimStart("v") 29 | echo Version: $VERSION 30 | 31 | $distFolder = "./dist/MagicTooltips/" 32 | if (Test-Path $distFolder) { 33 | Remove-Item $distFolder -Recurse -Force 34 | } 35 | New-Item $distFolder -ItemType "directory" 36 | 37 | Copy-Item -Path "./src/MagicTooltips.psd1" -Destination $distFolder 38 | Copy-Item -Path "./src/MagicTooltips.psm1" -Destination $distFolder 39 | 40 | dotnet build -c Release -p:Version=$VERSION -o "$($distFolder)lib/" ./src/$env:PROJECT_NAME/$env:PROJECT_NAME.csproj 41 | 42 | $manifestPath = Resolve-Path -Path "$($distFolder)$env:PROJECT_NAME.psd1" 43 | Write-Host "Manifest Path: $manifestPath" 44 | 45 | Update-ModuleManifest -ReleaseNotes $releaseNotes -Path $manifestPath.Path -ModuleVersion $VERSION #-Verbose 46 | 47 | $moduleFilePath = Resolve-Path -Path "$($distFolder)$env:PROJECT_NAME.psm1" 48 | Write-Host "Module File Path: $moduleFilePath" 49 | 50 | try{ 51 | Publish-Module -Path $distFolder -NuGetApiKey $env:PS_GALLERY_KEY -ErrorAction Stop -Force 52 | Write-Host "v$($VERSION) has been Published to the PowerShell Gallery!" 53 | } 54 | catch { 55 | throw $_ 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### VisualStudio ### 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # StyleCop 67 | StyleCopReport.xml 68 | 69 | # Files built by Visual Studio 70 | *_i.c 71 | *_p.c 72 | *_h.h 73 | *.ilk 74 | *.meta 75 | *.obj 76 | *.iobj 77 | *.pch 78 | *.pdb 79 | *.ipdb 80 | *.pgc 81 | *.pgd 82 | *.rsp 83 | *.sbr 84 | *.tlb 85 | *.tli 86 | *.tlh 87 | *.tmp 88 | *.tmp_proj 89 | *_wpftmp.csproj 90 | *.log 91 | *.vspscc 92 | *.vssscc 93 | .builds 94 | *.pidb 95 | *.svclog 96 | *.scc 97 | 98 | # Chutzpah Test files 99 | _Chutzpah* 100 | 101 | # Visual C++ cache files 102 | ipch/ 103 | *.aps 104 | *.ncb 105 | *.opendb 106 | *.opensdf 107 | *.sdf 108 | *.cachefile 109 | *.VC.db 110 | *.VC.VC.opendb 111 | 112 | # Visual Studio profiler 113 | *.psess 114 | *.vsp 115 | *.vspx 116 | *.sap 117 | 118 | # Visual Studio Trace Files 119 | *.e2e 120 | 121 | # TFS 2012 Local Workspace 122 | $tf/ 123 | 124 | # Guidance Automation Toolkit 125 | *.gpState 126 | 127 | # ReSharper is a .NET coding add-in 128 | _ReSharper*/ 129 | *.[Rr]e[Ss]harper 130 | *.DotSettings.user 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Coverlet is a free, cross platform Code Coverage Tool 143 | coverage*[.json, .xml, .info] 144 | 145 | # Visual Studio code coverage results 146 | *.coverage 147 | *.coveragexml 148 | 149 | # NCrunch 150 | _NCrunch_* 151 | .*crunch*.local.xml 152 | nCrunchTemp_* 153 | 154 | # MightyMoose 155 | *.mm.* 156 | AutoTest.Net/ 157 | 158 | # Web workbench (sass) 159 | .sass-cache/ 160 | 161 | # Installshield output folder 162 | [Ee]xpress/ 163 | 164 | # DocProject is a documentation generator add-in 165 | DocProject/buildhelp/ 166 | DocProject/Help/*.HxT 167 | DocProject/Help/*.HxC 168 | DocProject/Help/*.hhc 169 | DocProject/Help/*.hhk 170 | DocProject/Help/*.hhp 171 | DocProject/Help/Html2 172 | DocProject/Help/html 173 | 174 | # Click-Once directory 175 | publish/ 176 | 177 | # Publish Web Output 178 | *.[Pp]ublish.xml 179 | *.azurePubxml 180 | # Note: Comment the next line if you want to checkin your web deploy settings, 181 | # but database connection strings (with potential passwords) will be unencrypted 182 | *.pubxml 183 | *.publishproj 184 | 185 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 186 | # checkin your Azure Web App publish settings, but sensitive information contained 187 | # in these scripts will be unencrypted 188 | PublishScripts/ 189 | 190 | # NuGet Packages 191 | *.nupkg 192 | # NuGet Symbol Packages 193 | *.snupkg 194 | # The packages folder can be ignored because of Package Restore 195 | **/[Pp]ackages/* 196 | # except build/, which is used as an MSBuild target. 197 | !**/[Pp]ackages/build/ 198 | # Uncomment if necessary however generally it will be regenerated when needed 199 | #!**/[Pp]ackages/repositories.config 200 | # NuGet v3's project.json files produces more ignorable files 201 | *.nuget.props 202 | *.nuget.targets 203 | 204 | # Microsoft Azure Build Output 205 | csx/ 206 | *.build.csdef 207 | 208 | # Microsoft Azure Emulator 209 | ecf/ 210 | rcf/ 211 | 212 | # Windows Store app package directories and files 213 | AppPackages/ 214 | BundleArtifacts/ 215 | Package.StoreAssociation.xml 216 | _pkginfo.txt 217 | *.appx 218 | *.appxbundle 219 | *.appxupload 220 | 221 | # Visual Studio cache files 222 | # files ending in .cache can be ignored 223 | *.[Cc]ache 224 | # but keep track of directories ending in .cache 225 | !?*.[Cc]ache/ 226 | 227 | # Others 228 | ClientBin/ 229 | ~$* 230 | *~ 231 | *.dbmdl 232 | *.dbproj.schemaview 233 | *.jfm 234 | *.pfx 235 | *.publishsettings 236 | orleans.codegen.cs 237 | 238 | # Including strong name files can present a security risk 239 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 240 | #*.snk 241 | 242 | # Since there are multiple workflows, uncomment next line to ignore bower_components 243 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 244 | #bower_components/ 245 | 246 | # RIA/Silverlight projects 247 | Generated_Code/ 248 | 249 | # Backup & report files from converting an old project file 250 | # to a newer Visual Studio version. Backup files are not needed, 251 | # because we have git ;-) 252 | _UpgradeReport_Files/ 253 | Backup*/ 254 | UpgradeLog*.XML 255 | UpgradeLog*.htm 256 | ServiceFabricBackup/ 257 | *.rptproj.bak 258 | 259 | # SQL Server files 260 | *.mdf 261 | *.ldf 262 | *.ndf 263 | 264 | # Business Intelligence projects 265 | *.rdl.data 266 | *.bim.layout 267 | *.bim_*.settings 268 | *.rptproj.rsuser 269 | *- [Bb]ackup.rdl 270 | *- [Bb]ackup ([0-9]).rdl 271 | *- [Bb]ackup ([0-9][0-9]).rdl 272 | 273 | # Microsoft Fakes 274 | FakesAssemblies/ 275 | 276 | # GhostDoc plugin setting file 277 | *.GhostDoc.xml 278 | 279 | # Node.js Tools for Visual Studio 280 | .ntvs_analysis.dat 281 | node_modules/ 282 | 283 | # Visual Studio 6 build log 284 | *.plg 285 | 286 | # Visual Studio 6 workspace options file 287 | *.opt 288 | 289 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 290 | *.vbw 291 | 292 | # Visual Studio LightSwitch build output 293 | **/*.HTMLClient/GeneratedArtifacts 294 | **/*.DesktopClient/GeneratedArtifacts 295 | **/*.DesktopClient/ModelManifest.xml 296 | **/*.Server/GeneratedArtifacts 297 | **/*.Server/ModelManifest.xml 298 | _Pvt_Extensions 299 | 300 | # Paket dependency manager 301 | .paket/paket.exe 302 | paket-files/ 303 | 304 | # FAKE - F# Make 305 | .fake/ 306 | 307 | # CodeRush personal settings 308 | .cr/personal 309 | 310 | # Python Tools for Visual Studio (PTVS) 311 | __pycache__/ 312 | *.pyc 313 | 314 | # Cake - Uncomment if you are using it 315 | # tools/** 316 | # !tools/packages.config 317 | 318 | # Tabs Studio 319 | *.tss 320 | 321 | # Telerik's JustMock configuration file 322 | *.jmconfig 323 | 324 | # BizTalk build output 325 | *.btp.cs 326 | *.btm.cs 327 | *.odx.cs 328 | *.xsd.cs 329 | 330 | # OpenCover UI analysis results 331 | OpenCover/ 332 | 333 | # Azure Stream Analytics local run output 334 | ASALocalRun/ 335 | 336 | # MSBuild Binary and Structured Log 337 | *.binlog 338 | 339 | # NVidia Nsight GPU debugger configuration file 340 | *.nvuser 341 | 342 | # MFractors (Xamarin productivity tool) working folder 343 | .mfractor/ 344 | 345 | # Local History for Visual Studio 346 | .localhistory/ 347 | 348 | # BeatPulse healthcheck temp database 349 | healthchecksdb 350 | 351 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 352 | MigrationBackup/ 353 | 354 | # Ionide (cross platform F# VS Code tools) working folder 355 | .ionide/ 356 | 357 | # End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode 358 | 359 | dist/ 360 | lib/ 361 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Travis Collins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✨ Magic Tooltips ✨ 2 | 3 | A PowerShell module to display contextual information about the command you're currently entering. 4 | 5 | ![Magic Tooltips Demo](/media/demo.gif) 6 | 7 | Pairs nicely with custom prompts, such as [oh-my-posh](https://github.com/JanDeDobbeleer/oh-my-posh)! 8 | ![Magic Tooltips with oh-my-posh3](media/oh-my-posh.png) 9 | 10 | 11 | Supported Providers: 12 | - Microsoft Graph Powershell - Shows the name of the connected account 13 | - M365 - Shows the name of the logged-in account for the CLI for Microsoft 365 14 | - Kubernetes - Shows the current kubernetes context 15 | - Azure - Shows the name of the current azure subscription 16 | - AWS - Shows the name of the selected AWS Profile (the AWS_Profile environment variable) 17 | - Microsoft Graph CLI (PREVIEW) - Shows the name of the connected account 18 | 19 | --- 20 | ## Prerequisites 21 | - Powershell 7+ 22 | - CLI tools installed and in your path for one or more of the supported providers 23 | - (optional) A [Nerd Font](https://www.nerdfonts.com/) installed and selected as your terminal's font 24 | 25 | --- 26 | ## Installation 27 | 28 | You can install and import Magic Tooltips from the PowerShell Gallery: 29 | 30 | ```pwsh 31 | Install-Module MagicTooltips 32 | Import-Module MagicTooltips -Force 33 | ``` 34 | 35 | To make the module auto-load, add the Import-Module line to your [PowerShell profile](#powershell-profile). 36 | 37 | --- 38 | ## Configuration 39 | 40 | MagicTooltips is configured by setting a global variables in your [PowerShell profile](#powershell-profile). Below is a sample showing all of the possible settings and their default values. 41 | 42 | > **NOTE:** v2 Breaking change 43 | > 44 | > The Azure provider has been separated into AzCLI and AzPwsh. 45 | 46 | ```pwsh 47 | $global:MagicTooltipsSettings = @{ 48 | Debug = $true 49 | HorizontalAlignment = "Right" 50 | VerticalOffset = -1 51 | HorizontalOffset = 0 52 | Providers= @{ 53 | MG = @{ 54 | NounPrefixes = "mg" 55 | FgColor => "#32A5E6" 56 | BgColor => ""; 57 | Template => "\uf871 {value}"; 58 | } 59 | M365 = @{ 60 | Commands = "m365" 61 | FgColor = "#EF5350" 62 | BgColor = "" 63 | Template = "\uf8c5 {value}" 64 | } 65 | AzCLI = @{ 66 | Commands = "az,terraform,pulumi,terragrunt" 67 | FgColor = "#3A96DD" 68 | BgColor = "" 69 | Template = "\ufd03 {value}" 70 | } 71 | AzPwsh = @{ 72 | NounPrefixes = "az" 73 | FgColor = "#3A96DD" 74 | BgColor = "" 75 | Template = "\ufd03 {value}" 76 | } 77 | Kubernetes = @{ 78 | Commands = "kubectl,helm,kubens,kubectx,oc,istioctl,kogito,k9s,helmfile" 79 | FgColor = "#AE5FD6" 80 | BgColor = "" 81 | Template = "\ufd31 {value}" 82 | } 83 | Aws = @{ 84 | Commands = "aws,awless,terraform,pulumi,terragrunt" 85 | FgColor = "#EC7211" 86 | BgColor = "" 87 | Template = "\uf270 {value}" 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | Feel free to delete settings that you do not want to change. For example, if the only thing you want to change is to add `k` to the list of kubernetes commands, this is a perfectly valid configuration: 94 | 95 | ```pwsh 96 | $global:MagicTooltipsSettings = @{ 97 | Providers= @{ 98 | Kubernetes = @{ 99 | Commands = "kubectl,helm,kubens,kubectx,oc,istioctl,kogito,k9s,helmfile,k" 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | ### Triggers 106 | To configure what will trigger MagicTooltips, edit the `Command` and `NounPrefixes` settings for a provider. This is a comma-separated list of values. If the entry in the terminal contains a command, or a PowerShell command begins with a specified prefix, the provider will be triggered to display a MagicTooltip. 107 | 108 | ### Colors 109 | To configure the colors, use hex colors in the `FgColor` and `BgColor` variables. 110 | 111 | ### Templates 112 | MagicTooltips are displayed using a simple template language in the `Template` variables. The string `{value}` will be replaced with the value returned by the provider (Microsoft Graph connected account, for example). 113 | 114 | If you would like to use icons in your template, make sure you have a [Nerd Font](https://www.nerdfonts.com/) selected as your terminal's font. Specify icons using the syntax ` \uf871` where `f871` is the hex code for the unicode character you wish to print. You can find these hex codes on the [Nerd Font Cheat Sheet](https://www.nerdfonts.com/cheat-sheet). 115 | 116 | ### Placement 117 | To configure placement, set the following variables: 118 | 119 | - `HorizontalAlignment` default "right". Possible values are "left" or "right" 120 | - `HorizontalOffset` default 0. specify the number of columns to offset from the left or right edge of the terminal 121 | - `VerticalOffset` default 0. specify the number of rows to offset from the cursor position. Negative values will cause printing 122 | to appear above the cursor row, while positive values will print below the cursor row. 123 | 124 | ### Debug 125 | To enable debug logs, set the `Debug` variable to `$true`. The log file is called `magictooltips.log` and is written to the module directory, which can be found using 126 | ```pwsh 127 | (Get-Module MagicTooltips).ModuleBase 128 | ``` 129 | 130 | (Debug logs will not work if the module is installed with the `AllUsers` scope.) 131 | 132 | --- 133 | ## PowerShell Profile 134 | 135 | The path to your PowerShell profile is stored by PowerShell in the variable `$profile`. The following commands will launch a text editor with your profile: 136 | 137 | Visual Studio Code 138 | ```pwsh 139 | code $profile 140 | ``` 141 | 142 | Notepad 143 | ```pwsh 144 | notepad $profile 145 | ``` 146 | 147 | Once you have made changes to your profile, you can reload your profile in PowerShell: 148 | ```pwsh 149 | .$profile 150 | ``` 151 | 152 | 153 | --- 154 | ## Acknowledgments 155 | - [Powerlevel10k](https://github.com/romkatv/powerlevel10k) 156 | - [oh-my-posh3](https://github.com/JanDeDobbeleer/oh-my-posh3) 157 | - [Nerd Fonts](https://www.nerdfonts.com/) 158 | - [gitmoji](https://gitmoji.dev/) -------------------------------------------------------------------------------- /media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschaeflein/MagicTooltips/877d2e43fbbcb322392a559b427922191c349632/media/demo.gif -------------------------------------------------------------------------------- /media/oh-my-posh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschaeflein/MagicTooltips/877d2e43fbbcb322392a559b427922191c349632/media/oh-my-posh.png -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | root = true 3 | 4 | # All files 5 | [*] 6 | indent_style = space 7 | 8 | # Xml files 9 | [*.xml] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/MagicTooltips.Logging/LoggingService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights; 2 | using Microsoft.ApplicationInsights.Extensibility; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Reflection; 7 | 8 | namespace MagicTooltips.Services 9 | { 10 | public class LoggingService 11 | { 12 | private static string logPath = ""; 13 | private static readonly object _lock = new object(); 14 | 15 | private static bool settingsDebug; 16 | private static bool settingsDisabled; 17 | 18 | private static TelemetryClient telemetryClient; 19 | private static Dictionary telemetryProperties; 20 | 21 | public static void Initialize(string appVersion, bool debug = false, bool disableTelemetry = false) 22 | { 23 | #if DEBUG 24 | debug = true; 25 | #endif 26 | 27 | settingsDebug = debug; 28 | settingsDisabled = disableTelemetry; 29 | 30 | if (disableTelemetry) 31 | { 32 | return; 33 | } 34 | 35 | if (debug) 36 | { 37 | if (string.IsNullOrEmpty(logPath)) 38 | { 39 | var logDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 40 | if (logDir.EndsWith("lib")) 41 | { 42 | logDir = Directory.GetParent(logDir).ToString(); 43 | } 44 | logPath = Path.Combine(logDir, "magictooltips.log"); 45 | } 46 | } 47 | 48 | if (telemetryClient == null) 49 | { 50 | TelemetryConfiguration config = TelemetryConfiguration.CreateDefault(); 51 | config.ConnectionString = "InstrumentationKey=bf4fb923-8051-426b-a657-7255b766deb2;IngestionEndpoint=https://northcentralus-0.in.applicationinsights.azure.com/;LiveEndpoint=https://northcentralus.livediagnostics.monitor.azure.com/"; 52 | telemetryClient = new TelemetryClient(config); 53 | telemetryClient.Context.Cloud.RoleInstance = "MagicTooltips"; 54 | telemetryClient.Context.Device.OperatingSystem = Environment.OSVersion.ToString(); 55 | 56 | var operatingSystem = Utilities.OperatingSystem.GetOSString(); 57 | 58 | telemetryProperties = new Dictionary 59 | { 60 | { "Version", appVersion }, 61 | { "OperatingSystem", operatingSystem} 62 | }; 63 | } 64 | } 65 | 66 | public static void LogOperation(string operation, string providerKey) 67 | { 68 | if (!settingsDisabled) 69 | { 70 | telemetryProperties["operation"] = operation.ToLower(); 71 | telemetryProperties["providerKey"] = providerKey; 72 | telemetryClient.TrackEvent(operation, telemetryProperties); 73 | telemetryClient.Flush(); 74 | } 75 | } 76 | 77 | public static void LogDebug(string message) 78 | { 79 | if (settingsDebug) 80 | { 81 | var formattedDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 82 | lock (_lock) 83 | { 84 | File.AppendAllText(logPath, $"{formattedDate} {message}\n"); 85 | } 86 | } 87 | } 88 | 89 | public static void LogError(Exception exception) 90 | { 91 | if (!settingsDisabled) 92 | { 93 | telemetryProperties.Remove("operation"); 94 | telemetryProperties.Remove("providerKey"); 95 | telemetryClient.TrackException(exception, telemetryProperties); 96 | } 97 | 98 | if (settingsDebug) 99 | { 100 | var formattedDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 101 | lock (_lock) 102 | { 103 | File.AppendAllText(logPath, $"{formattedDate} {exception.Message}\n"); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/MagicTooltips.Logging/MagicTooltips.Logging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/MagicTooltips.Logging/Utilities.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace MagicTooltips.Utilities 4 | { 5 | internal static class OperatingSystem 6 | { 7 | internal static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 8 | internal static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); 9 | internal static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); 10 | 11 | internal static string GetOSString() 12 | { 13 | if (IsWindows()) 14 | { 15 | return "Windows"; 16 | } 17 | else if (IsMacOS()) 18 | { 19 | return "MacOS"; 20 | } 21 | else if (IsLinux()) 22 | { 23 | return "Linux"; 24 | } 25 | else 26 | { 27 | return "Unknown"; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/MagicTooltips.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'MagicTooltips' 3 | # 4 | # Generated by: TravisTX 5 | # 6 | # Generated on: 12/19/2020 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'MagicTooltips.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.0.1' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'f88a0f6c-34aa-4b9e-b3f2-c965e1b13879' 22 | 23 | # Author of this module 24 | Author = 'Paul Schaeflein, TravisTX' 25 | 26 | # Company or vendor of this module 27 | CompanyName = '' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) TravisTX. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Show contextual information about the command as you are entering it' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '5.1' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | #FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | NestedModules = @('lib\MagicTooltips.dll') 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @() 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @('Register-MagicTooltips', 'Invoke-MagicTooltips') 76 | 77 | # Variables to export from this module 78 | VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @("Microsoft", "Graph", "Kubernetes", "Kubectl", "Azure", "Aws", "M365", "Terraform", "ShowOnCommand", "Show-On-Command", "Cloud") 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/pschaeflein/MagicTooltips/blob/main/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/pschaeflein/MagicTooltips' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | # ReleaseNotes = '' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 116 | # RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | # ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | HelpInfoURI = 'https://github.com/pschaeflein/MagicTooltips' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/MagicTooltips.psm1: -------------------------------------------------------------------------------- 1 | Register-MagicTooltips -------------------------------------------------------------------------------- /src/MagicTooltips.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30621.155 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicTooltips", "MagicTooltips\MagicTooltips.csproj", "{8FE986D6-CEB8-405F-9F3F-555C3C631966}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicTooltipsTests", "MagicTooltipsTests\MagicTooltipsTests.csproj", "{4ED922F3-691F-4C08-9605-CA6DB3E73759}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B0DB5D91-5CC8-4701-AD0A-B5838E1B91A5}" 11 | ProjectSection(SolutionItems) = preProject 12 | ..\.gitignore = ..\.gitignore 13 | Directory.Build.props = Directory.Build.props 14 | ..\README.md = ..\README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{577D3479-F735-4166-95F5-7E62BBC952B1}" 18 | ProjectSection(SolutionItems) = preProject 19 | ..\.github\workflows\deploy.yml = ..\.github\workflows\deploy.yml 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicTooltips.Logging", "MagicTooltips.Logging\MagicTooltips.Logging.csproj", "{82B49079-B1C0-495F-BFC3-C5EB205019B7}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {8FE986D6-CEB8-405F-9F3F-555C3C631966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {8FE986D6-CEB8-405F-9F3F-555C3C631966}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {8FE986D6-CEB8-405F-9F3F-555C3C631966}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {8FE986D6-CEB8-405F-9F3F-555C3C631966}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {4ED922F3-691F-4C08-9605-CA6DB3E73759}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {4ED922F3-691F-4C08-9605-CA6DB3E73759}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {4ED922F3-691F-4C08-9605-CA6DB3E73759}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {4ED922F3-691F-4C08-9605-CA6DB3E73759}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {82B49079-B1C0-495F-BFC3-C5EB205019B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {82B49079-B1C0-495F-BFC3-C5EB205019B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {82B49079-B1C0-495F-BFC3-C5EB205019B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {82B49079-B1C0-495F-BFC3-C5EB205019B7}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(NestedProjects) = preSolution 47 | {577D3479-F735-4166-95F5-7E62BBC952B1} = {B0DB5D91-5CC8-4701-AD0A-B5838E1B91A5} 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {B6956E8F-8056-45A3-80B1-9B367CD03F18} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /src/MagicTooltips/Dtos/HorizontalAlignmentEnum.cs: -------------------------------------------------------------------------------- 1 | namespace MagicTooltips.Dtos 2 | { 3 | public enum HorizontalAlignmentEnum 4 | { 5 | Left, 6 | Right 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MagicTooltips/Dtos/ProviderKeys.cs: -------------------------------------------------------------------------------- 1 | namespace MagicTooltips.Dtos 2 | { 3 | public class ProviderKeys 4 | { 5 | public const string MGCLI = "mgcli"; // Probably merge w/PWSH. Review after release 6 | public const string MicrosoftGraph = "mg"; 7 | public const string M365 = "m365"; 8 | public const string Kubernetes = "kubernetes"; 9 | public const string AzCLI = "azcli"; 10 | public const string AzPwsh = "azpwsh"; 11 | public const string Aws = "aws"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MagicTooltips/Dtos/SettingsDto.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using System.Collections.Generic; 3 | 4 | namespace MagicTooltips.Dtos 5 | { 6 | public class SettingsDto 7 | { 8 | public bool Debug { get; set; } 9 | public HorizontalAlignmentEnum HorizontalAlignment { get; set; } 10 | public int HorizontalOffset { get; set; } 11 | public int VerticalOffset { get; set; } 12 | public Dictionary Providers { get; set; } 13 | 14 | public bool DisableTelemetry { get; set; } 15 | 16 | public SettingsDto() 17 | { 18 | Providers = new Dictionary(); 19 | var allProviders = ProviderFactory.GetAllProviders(); 20 | foreach (var provider in allProviders) 21 | { 22 | Providers.Add(provider.Key, new ProviderSettingsDto()); 23 | } 24 | } 25 | } 26 | 27 | public class ProviderSettingsDto 28 | { 29 | public string Commands { get; set; } 30 | public string NounPrefixes { get; set; } 31 | public string FgColor { get; set; } 32 | public string BgColor { get; set; } 33 | public string Template { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MagicTooltips/InternalsVisibleToTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("MagicTooltipsTests")] 4 | namespace MagicTooltips 5 | { 6 | } -------------------------------------------------------------------------------- /src/MagicTooltips/InvokeMagicTooltipsCommand.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Providers; 2 | using MagicTooltips.Services; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Management.Automation; 7 | using System.Threading.Tasks; 8 | 9 | namespace MagicTooltips 10 | { 11 | [Cmdlet(VerbsLifecycle.Invoke, "MagicTooltips")] 12 | public class InvokeMagicTooltipsCommand : PSCmdlet 13 | { 14 | [Parameter(Position = 0, Mandatory = true)] 15 | public string Line { get; set; } 16 | 17 | protected override void ProcessRecord() 18 | { 19 | Line = Line.TrimEnd(' ').ToLowerInvariant(); 20 | LoggingService.LogDebug($"line: '{Line}'"); 21 | 22 | if (TriggerService.CommandList.ContainsKey(Line)) 23 | { 24 | LoggingService.LogDebug($"Command: {Line}"); 25 | Invoke(TriggerService.CommandList[Line]); 26 | } 27 | else 28 | { 29 | if (TriggerService.NounPrefixList.Count == 0) 30 | { 31 | return; 32 | } 33 | 34 | var nounsInLine = TriggerService.ParseLine(Line); 35 | 36 | foreach (var noun in nounsInLine) 37 | { 38 | foreach (var prefix in TriggerService.NounPrefixList) 39 | { 40 | if (noun.StartsWith(prefix.Key)) 41 | { 42 | LoggingService.LogDebug($"Noun prefix: {prefix.Key}"); 43 | Invoke(prefix.Value); 44 | } 45 | } 46 | } 47 | return; 48 | } 49 | } 50 | 51 | private void Invoke(List providers) 52 | { 53 | var providerKeys = string.Join(", ", providers.Select(x => x.ProviderKey)); 54 | 55 | Task.Run(() => 56 | { 57 | var initialY = Console.CursorTop; 58 | var horizontalOffset = SettingsService.Settings.HorizontalOffset; 59 | 60 | foreach (var provider in providers) 61 | { 62 | LoggingService.LogOperation("Invoke", provider.ProviderKey); 63 | var val = provider.GetValue(); 64 | horizontalOffset = RenderService.ShowTooltip(provider.ProviderKey, val, Host, initialY, horizontalOffset); 65 | } 66 | }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/MagicTooltips/MagicTooltips.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | MagicTooltips 6 | true 7 | 8 | 9 | true 10 | 11 | true 12 | 13 | 14 | embedded 15 | 16 | 17 | 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | All 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/MagicTooltips/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "PowerShell Core": { 4 | "commandName": "Executable", 5 | "executablePath": "C:\\Users\\PaulSchaeflein\\AppData\\Local\\Microsoft\\WindowsApps\\Microsoft.PowerShell_8wekyb3d8bbwe\\pwsh.exe", 6 | "commandLineArgs": "-NoProfile -NoExit -Command \"Import-Module .\\MagicTooltips.dll; Register-MagicTooltips\"" 7 | }, 8 | "Windows PowerShell": { 9 | "commandName": "Executable", 10 | "executablePath": "powershell", 11 | "commandLineArgs": "-NoExit -Command \"Import-Module .\\MagicTooltips.dll; Register-MagicTooltips\"" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/AwsProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MagicTooltips.Providers 4 | { 5 | public class AwsProvider : IProvider 6 | { 7 | public string ProviderKey => "aws"; 8 | public string DefaultCommands => "aws,awless,terraform,pulumi,terragrunt"; 9 | public string DefaultNounPrefixes => null; 10 | public string DefaultFgColor => "#EC7211"; 11 | public string DefaultBgColor => ""; 12 | public string DefaultTemplate => "\uf270 {value}"; 13 | 14 | public string GetValue() 15 | { 16 | return Environment.GetEnvironmentVariable("AWS_Profile"); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/AzCLIProvider.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using System; 3 | using System.IO; 4 | 5 | namespace MagicTooltips.Providers 6 | { 7 | public class AzCLIProvider : IProvider 8 | { 9 | public string ProviderKey => "azcli"; 10 | public string DefaultCommands => "az,terraform,pulumi,terragrunt"; 11 | public string DefaultNounPrefixes => null; 12 | public string DefaultFgColor => "#3A96DD"; 13 | public string DefaultBgColor => ""; 14 | public string DefaultTemplate => "\ufd03 {value}"; 15 | 16 | private static string fileHash = null; 17 | private static string azCliAccount = null; 18 | private static string azProfilePath = null; 19 | 20 | public string GetValue() 21 | { 22 | string currentFileHash = null; 23 | try 24 | { 25 | if (string.IsNullOrWhiteSpace(azProfilePath)) 26 | { 27 | azProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azure", "azureProfile.json"); 28 | } 29 | currentFileHash = Md5Utility.CalculateMd5(azProfilePath); 30 | } 31 | catch (Exception ex) 32 | { 33 | LoggingService.LogDebug(ex.ToString()); 34 | } 35 | 36 | if (currentFileHash != fileHash) 37 | { 38 | LoggingService.LogDebug("azureProfile.json has changed, clearing cache"); 39 | fileHash = currentFileHash; 40 | azCliAccount = null; 41 | } 42 | 43 | if (string.IsNullOrWhiteSpace(azCliAccount)) 44 | { 45 | var script = "az account show --query name --output tsv"; 46 | 47 | azCliAccount = PowershellInvoker.InvokeScript(script); 48 | } 49 | 50 | return azCliAccount; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/AzPowerShellProvider.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using System; 3 | using System.IO; 4 | 5 | namespace MagicTooltips.Providers 6 | { 7 | public class AzPowerShellProvider : IProvider 8 | { 9 | public string ProviderKey => "azpwsh"; 10 | public string DefaultCommands => null; 11 | public string DefaultNounPrefixes => "az"; 12 | public string DefaultFgColor => "#3A96DD"; 13 | public string DefaultBgColor => ""; 14 | public string DefaultTemplate => "\ufd03 {value}"; 15 | 16 | private static string fileHash = null; 17 | private static string azPwshAccount = null; 18 | private static string azpwshProfilePath = null; 19 | 20 | public string GetValue() 21 | { 22 | string currentFileHash = null; 23 | try 24 | { 25 | if (string.IsNullOrWhiteSpace(azpwshProfilePath)) 26 | { 27 | azpwshProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azure", "AzureRmContext.json"); 28 | } 29 | currentFileHash = Md5Utility.CalculateMd5(azpwshProfilePath); 30 | } 31 | catch (Exception ex) 32 | { 33 | LoggingService.LogDebug(ex.ToString()); 34 | } 35 | 36 | if (currentFileHash != fileHash) 37 | { 38 | LoggingService.LogDebug("AzureRmContext.json has changed, clearing cache"); 39 | fileHash = currentFileHash; 40 | azPwshAccount = null; 41 | } 42 | 43 | if (string.IsNullOrWhiteSpace(azPwshAccount)) 44 | { 45 | var script = "((Get-AzContext).Subscription | Get-AzSubscription).Name"; 46 | azPwshAccount = PowershellInvoker.InvokeScript(script); 47 | } 48 | 49 | return azPwshAccount; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/IProvider.cs: -------------------------------------------------------------------------------- 1 | namespace MagicTooltips.Providers 2 | { 3 | public interface IProvider 4 | { 5 | string ProviderKey { get; } 6 | string DefaultCommands { get; } 7 | string DefaultNounPrefixes { get; } 8 | string DefaultFgColor { get; } 9 | string DefaultBgColor { get; } 10 | string DefaultTemplate { get; } 11 | string GetValue(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/KubernetesProvider.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | 3 | namespace MagicTooltips.Providers 4 | { 5 | public class KubernetesProvider : IProvider 6 | { 7 | public string ProviderKey => "kubernetes"; 8 | public string DefaultCommands => "kubectl,helm,kubens,kubectx,oc,istioctl,kogito,k9s,helmlist"; 9 | public string DefaultNounPrefixes => null; 10 | public string DefaultFgColor => "#AE5FD6"; 11 | public string DefaultBgColor => ""; 12 | public string DefaultTemplate => "\ufd31 {value}"; 13 | 14 | public string GetValue() 15 | { 16 | var script = "kubectl config view --minify --output 'jsonpath={...context.cluster}::{..namespace}'"; 17 | return PowershellInvoker.InvokeScript(script); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/M365Provider.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using System; 3 | using System.IO; 4 | 5 | namespace MagicTooltips.Providers 6 | { 7 | public class M365Provider : IProvider 8 | { 9 | public string ProviderKey => "m365"; 10 | public string DefaultCommands => "m365"; 11 | public string DefaultNounPrefixes => null; 12 | public string DefaultFgColor => "#EF5350"; 13 | public string DefaultBgColor => ""; 14 | public string DefaultTemplate => "\uf8c5 {value}"; 15 | 16 | private static string fileHash = null; 17 | private static string m365Account = null; 18 | private static string m365ConnectionInfoFilePath = null; 19 | 20 | public string GetValue() 21 | { 22 | string currentFileHash = null; 23 | try 24 | { 25 | if (string.IsNullOrWhiteSpace(m365ConnectionInfoFilePath)) 26 | { 27 | m365ConnectionInfoFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cli-m365-tokens.json"); 28 | } 29 | currentFileHash = Md5Utility.CalculateMd5(m365ConnectionInfoFilePath); 30 | } 31 | catch (Exception ex) 32 | { 33 | LoggingService.LogDebug(ex.ToString()); 34 | } 35 | 36 | if (currentFileHash != fileHash) 37 | { 38 | LoggingService.LogDebug(".cli-m365-tokens.json has changed, clearing cache"); 39 | fileHash = currentFileHash; 40 | m365Account = null; 41 | } 42 | 43 | if (string.IsNullOrWhiteSpace(m365Account)) 44 | { 45 | var script = "m365 status --query connectedAs -o json"; 46 | 47 | m365Account = PowershellInvoker.InvokeScript(script); 48 | } 49 | 50 | if (m365Account == "null") 51 | { 52 | m365Account = "Logged out"; 53 | } 54 | return m365Account.Trim('"'); 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/Md5Utility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace MagicTooltips.Providers 8 | { 9 | static class Md5Utility 10 | { 11 | internal static string CalculateMd5(string filePath) 12 | { 13 | if (File.Exists(filePath)) 14 | { 15 | 16 | using (var md5 = MD5.Create()) 17 | { 18 | using (var stream = File.OpenRead(filePath)) 19 | { 20 | var hash = md5.ComputeHash(stream); 21 | return BitConverter.ToString(hash); 22 | } 23 | } 24 | } 25 | else 26 | { 27 | return null; 28 | } 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/MicrosoftGraphCLIProvider.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using System; 3 | using System.IO; 4 | using System.Text.Json; 5 | 6 | namespace MagicTooltips.Providers 7 | { 8 | public class MicrosoftGraphCLIProvider : IProvider 9 | { 10 | public string ProviderKey => "mgcli"; 11 | public string DefaultCommands => "mg"; // macOS will get a new name... 12 | public string DefaultNounPrefixes => ""; 13 | public string DefaultFgColor => "#32A5E6"; 14 | public string DefaultBgColor => ""; 15 | public string DefaultTemplate => "\uf871 {value}"; 16 | 17 | private static string fileHash = null; 18 | private static string mgAccount = null; 19 | private static string mgRecordFilePath = null; 20 | 21 | public string GetValue() 22 | { 23 | string currentFileHash = null; 24 | try 25 | { 26 | if (string.IsNullOrWhiteSpace(mgRecordFilePath)) 27 | { 28 | var folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".mg"); 29 | mgRecordFilePath = Path.Combine(folderPath, "record.txt"); 30 | LoggingService.LogDebug($"mgRecordFilePath: {mgRecordFilePath}"); 31 | } 32 | 33 | if (!string.IsNullOrEmpty(mgRecordFilePath)) 34 | { 35 | currentFileHash = Md5Utility.CalculateMd5(mgRecordFilePath) ?? "null"; 36 | } 37 | else 38 | { 39 | currentFileHash = "null"; 40 | } 41 | } 42 | catch (Exception ex) 43 | { 44 | LoggingService.LogDebug(ex.ToString()); 45 | } 46 | 47 | if (currentFileHash == "null") 48 | { 49 | LoggingService.LogDebug("No mgRecordFile found"); 50 | mgAccount = null; 51 | } 52 | if (currentFileHash != fileHash) 53 | { 54 | LoggingService.LogDebug("mgRecordFile has changed, clearing cache"); 55 | fileHash = currentFileHash; 56 | mgAccount = null; 57 | } 58 | 59 | /* 60 | * For now, there is no command to get the current user 61 | * 62 | * So I'm going to read & parse the record.txt file 63 | */ 64 | if (string.IsNullOrWhiteSpace(mgAccount)) 65 | { 66 | //var script = "(Get-MgContext).Account"; 67 | //mgAccount = PowershellInvoker.InvokeScript(script); 68 | 69 | if (File.Exists(mgRecordFilePath)) 70 | { 71 | var content = File.ReadAllText(mgRecordFilePath); 72 | using var contentDoc = JsonDocument.Parse(content); 73 | mgAccount = contentDoc.RootElement.GetProperty("username").GetString(); 74 | } 75 | } 76 | 77 | if (string.IsNullOrEmpty(mgAccount)) 78 | { 79 | return "Not connected"; 80 | } 81 | else 82 | { 83 | return mgAccount.Trim('"'); 84 | } 85 | } 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/MagicTooltips/Providers/MicrosoftGraphPowerShellProvider.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace MagicTooltips.Providers 7 | { 8 | public class MicrosoftGraphPowerShellProvider : IProvider 9 | { 10 | public string ProviderKey => "mg"; 11 | public string DefaultCommands => ""; 12 | public string DefaultNounPrefixes => "mg"; 13 | public string DefaultFgColor => "#32A5E6"; 14 | public string DefaultBgColor => ""; 15 | public string DefaultTemplate => "\uf871 {value}"; 16 | 17 | private static string fileHash = null; 18 | private static string mgAccount = null; 19 | private static string mgCacheFilePath = null; 20 | 21 | public string GetValue() 22 | { 23 | string currentFileHash = null; 24 | try 25 | { 26 | if (string.IsNullOrWhiteSpace(mgCacheFilePath)) 27 | { 28 | var folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".graph"); 29 | mgCacheFilePath = Directory.EnumerateFiles(folderPath, "*cache.bin*").FirstOrDefault(); 30 | LoggingService.LogDebug($"mgCacheFilePath: {mgCacheFilePath}"); 31 | } 32 | 33 | if (!string.IsNullOrEmpty(mgCacheFilePath)) 34 | { 35 | currentFileHash = Md5Utility.CalculateMd5(mgCacheFilePath); 36 | } 37 | else 38 | { 39 | currentFileHash = "null"; 40 | } 41 | } 42 | catch (Exception ex) 43 | { 44 | LoggingService.LogDebug(ex.ToString()); 45 | } 46 | 47 | if (currentFileHash == "null") 48 | { 49 | LoggingService.LogDebug("No mgTokenCache found"); 50 | mgAccount = null; 51 | } 52 | if (currentFileHash != fileHash) 53 | { 54 | LoggingService.LogDebug("mgTokenCache has changed, clearing cache"); 55 | fileHash = currentFileHash; 56 | mgAccount = null; 57 | } 58 | 59 | if (string.IsNullOrWhiteSpace(mgAccount)) 60 | { 61 | var script = "(Get-MgContext).Account"; 62 | mgAccount = PowershellInvoker.InvokeScript(script); 63 | } 64 | 65 | if (string.IsNullOrEmpty(mgAccount)) 66 | { 67 | return "Not connected"; 68 | } 69 | else 70 | { 71 | return mgAccount.Trim('"'); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/MagicTooltips/RegisterMagicTooltipsCommand.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using System.Management.Automation; 3 | 4 | namespace MagicTooltips 5 | { 6 | [Cmdlet(VerbsLifecycle.Register, "MagicTooltips")] 7 | public class RegisterMagicTooltipsCommand : PSCmdlet 8 | { 9 | protected override void ProcessRecord() 10 | { 11 | SettingsService.Populate(SessionState); 12 | 13 | try 14 | { 15 | var AppVersion = System.Diagnostics.FileVersionInfo.GetVersionInfo(typeof(RegisterMagicTooltipsCommand).Assembly.Location).FileVersion; 16 | 17 | LoggingService.Initialize(AppVersion, SettingsService.Settings.Debug, SettingsService.Settings.DisableTelemetry); 18 | LoggingService.LogOperation("Register", ""); 19 | } 20 | catch (System.Exception ex) 21 | { 22 | LoggingService.LogError(ex); 23 | base.WriteError(new ErrorRecord(ex, "ErrorOccurred", ErrorCategory.NotSpecified, null)); 24 | } 25 | 26 | TriggerService.PopulateTriggers(); 27 | 28 | PowerShell.Create().AddCommand("Remove-PSReadlineKeyHandler") 29 | .AddParameter("Key", "SpaceBar") 30 | .Invoke(); 31 | 32 | PowerShell.Create().AddCommand("Set-PSReadlineKeyHandler") 33 | .AddParameter("Key", "SpaceBar") 34 | .AddParameter("ScriptBlock", ScriptBlock.Create(@" 35 | [Microsoft.PowerShell.PSConsoleReadLine]::Insert(' ') 36 | $line = $cursor = $null 37 | [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) 38 | Invoke-MagicTooltips $line 39 | ")) 40 | .Invoke(); 41 | 42 | PowerShell.Create().AddCommand("Set-PSReadlineKeyHandler") 43 | .AddParameter("Key", "Tab") 44 | .AddParameter("ScriptBlock", ScriptBlock.Create(@" 45 | $line = $cursor = $null 46 | [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) 47 | Invoke-MagicTooltips $line 48 | [Microsoft.PowerShell.PSConsoleReadLine]::TabCompleteNext() 49 | ")) 50 | .Invoke(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MagicTooltips/Services/DependencyService/DependencyAssemblyLoadContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime.Loader; 6 | 7 | namespace MagicTooltips.Services.DependencyService 8 | { 9 | public class DependencyAssemblyLoadContext : AssemblyLoadContext 10 | { 11 | private static readonly string s_psHome = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); 12 | 13 | private static readonly ConcurrentDictionary s_dependencyLoadContexts = new(); 14 | 15 | internal static DependencyAssemblyLoadContext GetForDirectory(string directoryPath) 16 | { 17 | return s_dependencyLoadContexts.GetOrAdd(directoryPath, (path) => new DependencyAssemblyLoadContext(path)); 18 | } 19 | 20 | private readonly string _dependencyDirPath; 21 | 22 | public DependencyAssemblyLoadContext(string dependencyDirPath) 23 | : base(nameof(DependencyAssemblyLoadContext)) 24 | { 25 | _dependencyDirPath = dependencyDirPath; 26 | } 27 | 28 | protected override Assembly Load(AssemblyName assemblyName) 29 | { 30 | string assemblyFileName = $"{assemblyName.Name}.dll"; 31 | 32 | // Make sure we allow other common PowerShell dependencies to be loaded by PowerShell 33 | // But specifically exclude Microsoft.ApplicationInsights since we want to use a specific version here 34 | if (!assemblyName.Name.Equals("Microsoft.ApplicationInsights", StringComparison.OrdinalIgnoreCase)) 35 | { 36 | string psHomeAsmPath = Path.Join(s_psHome, assemblyFileName); 37 | if (File.Exists(psHomeAsmPath)) 38 | { 39 | // With this API, returning null means nothing is loaded 40 | return null; 41 | } 42 | } 43 | 44 | // Now try to load the assembly from the dependency directory 45 | string dependencyAsmPath = Path.Join(_dependencyDirPath, assemblyFileName); 46 | if (File.Exists(dependencyAsmPath)) 47 | { 48 | return LoadFromAssemblyPath(dependencyAsmPath); 49 | } 50 | 51 | return null; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/MagicTooltips/Services/DependencyService/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Management.Automation; 3 | using System.Reflection; 4 | using System.Runtime.Loader; 5 | 6 | namespace MagicTooltips.Services.DependencyService 7 | { 8 | public class ModuleInitializer : IModuleAssemblyInitializer 9 | { 10 | private static readonly string binBasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 11 | 12 | public void OnImport() 13 | { 14 | AssemblyLoadContext.Default.Resolving += ResolveAssembly_NetCore; 15 | } 16 | 17 | private static Assembly ResolveAssembly_NetCore( 18 | AssemblyLoadContext assemblyLoadContext, 19 | AssemblyName assemblyName) 20 | { 21 | // In .NET Core, PowerShell deals with assembly probing so our logic is much simpler 22 | // We only care about certain assemblies 23 | if (!assemblyName.Name.Equals("Microsoft.ApplicationInsights")) 24 | { 25 | return null; 26 | } 27 | 28 | // Now load the Engine assembly through the dependency ALC, and let it resolve further dependencies automatically 29 | return DependencyAssemblyLoadContext.GetForDirectory(binBasePath).LoadFromAssemblyName(assemblyName); 30 | } 31 | 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /src/MagicTooltips/Services/PowershellInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using System.Management.Automation.Runspaces; 4 | 5 | namespace MagicTooltips.Services 6 | { 7 | public static class PowershellInvoker 8 | { 9 | public static string InvokeScript(string script) 10 | { 11 | var runspace = RunspaceFactory.CreateRunspace(); 12 | var powershell = PowerShell.Create(); 13 | powershell.Runspace = runspace; 14 | runspace.Open(); 15 | powershell.AddScript(script); 16 | 17 | try 18 | { 19 | var results = powershell.Invoke(); 20 | 21 | if (results.Count > 0) 22 | { 23 | return results[0]?.ToString() ?? ""; 24 | } 25 | else 26 | { 27 | return ""; 28 | } 29 | } 30 | catch (Exception ex) 31 | { 32 | LoggingService.LogDebug($"InvokeScript: {ex}"); 33 | return ""; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/MagicTooltips/Services/ProviderFactory.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Dtos; 2 | using MagicTooltips.Providers; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace MagicTooltips.Services 7 | { 8 | class ProviderFactory 9 | { 10 | private static Dictionary AllProviders = null; 11 | 12 | public static IProvider GetProvider(string providerKey) 13 | { 14 | 15 | if (AllProviders.ContainsKey(providerKey)) 16 | { 17 | return AllProviders[providerKey]; 18 | } 19 | 20 | throw new NotImplementedException($"Unexpected providerKey: `{providerKey}`"); 21 | } 22 | 23 | public static Dictionary GetAllProviders() 24 | { 25 | AllProviders ??= new Dictionary 26 | { 27 | { ProviderKeys.Kubernetes, new KubernetesProvider() }, 28 | { ProviderKeys.AzCLI, new AzCLIProvider() }, 29 | { ProviderKeys.AzPwsh, new AzPowerShellProvider() }, 30 | { ProviderKeys.Aws, new AwsProvider() }, 31 | { ProviderKeys.M365, new M365Provider() }, 32 | { ProviderKeys.MicrosoftGraph, new MicrosoftGraphPowerShellProvider() }, 33 | { ProviderKeys.MGCLI, new MicrosoftGraphCLIProvider() } 34 | }; 35 | return AllProviders; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/MagicTooltips/Services/RenderService.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Dtos; 2 | using System; 3 | using System.Management.Automation.Host; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace MagicTooltips.Services 7 | { 8 | public class RenderService 9 | { 10 | public static int ShowTooltip(string providerKey, string value, PSHost host, int initialY, int horizontalOffset) 11 | { 12 | var providerConfiguraiton = SettingsService.Settings.Providers[providerKey]; 13 | var template = Regex.Unescape(providerConfiguraiton.Template); 14 | var tooltipText = template.Replace("{value}", value); 15 | 16 | var coloredTooltip = GetColoredString(tooltipText, providerConfiguraiton.FgColor, providerConfiguraiton.BgColor); 17 | var drawX = SettingsService.Settings.HorizontalAlignment switch 18 | { 19 | HorizontalAlignmentEnum.Left => horizontalOffset, 20 | _ => Console.WindowWidth - tooltipText.Length - horizontalOffset, 21 | }; 22 | var drawY = initialY + SettingsService.Settings.VerticalOffset; 23 | drawY = Math.Max(0, drawY); 24 | 25 | var saveX = Console.CursorLeft; 26 | var saveY = Console.CursorTop; 27 | Console.SetCursorPosition(drawX, drawY); 28 | host.UI.Write(coloredTooltip); 29 | Console.SetCursorPosition(saveX, saveY); 30 | 31 | return horizontalOffset + tooltipText.Length + 1; 32 | } 33 | 34 | internal static string GetColoredString(string text, string fgColor, string bgColor) 35 | { 36 | if (string.IsNullOrWhiteSpace(text)) 37 | { 38 | return ""; 39 | } 40 | 41 | var fgRgb = ConvertHexToRgb(fgColor); 42 | var bgRgb = ConvertHexToRgb(bgColor); 43 | 44 | if (fgRgb.HasValue && bgRgb.HasValue) 45 | { 46 | return $"\x1b[38;2;{fgRgb?.r};{fgRgb?.g};{fgRgb?.b};48;2;{bgRgb?.r};{bgRgb?.g};{bgRgb?.b}m{text}\x1b[0m"; 47 | } 48 | if (fgRgb.HasValue) 49 | { 50 | return $"\x1b[38;2;{fgRgb?.r};{fgRgb?.g};{fgRgb?.b}m{text}\x1b[0m"; 51 | } 52 | if (bgRgb.HasValue) 53 | { 54 | return $"\x1b[48;2;{bgRgb?.r};{bgRgb?.g};{bgRgb?.b}m{text}\x1b[0m"; 55 | } 56 | return text; 57 | } 58 | 59 | internal static (int r, int g, int b)? ConvertHexToRgb(string hex) 60 | { 61 | if (string.IsNullOrEmpty(hex) || !Regex.IsMatch(hex, "^#[a-fA-F0-9]{6}$")) 62 | { 63 | return null; 64 | } 65 | var r = Convert.ToInt32(hex.Substring(1, 2), 16); 66 | var g = Convert.ToInt32(hex.Substring(3, 2), 16); 67 | var b = Convert.ToInt32(hex.Substring(5, 2), 16); 68 | return (r, g, b); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MagicTooltips/Services/SettingsService.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Dtos; 2 | using System; 3 | using System.Collections; 4 | using System.Management.Automation; 5 | 6 | namespace MagicTooltips.Services 7 | { 8 | public static class SettingsService 9 | { 10 | public static SessionState SessionState { get; private set; } 11 | public static SettingsDto Settings { get; private set; } 12 | 13 | public static void Populate(SessionState sessionState) 14 | { 15 | SessionState = sessionState; 16 | Settings = new SettingsDto(); 17 | var settingsObj = sessionState.PSVariable.GetValue("MagicTooltipsSettings"); 18 | if (settingsObj is not Hashtable) 19 | { 20 | settingsObj = new Hashtable(); 21 | } 22 | 23 | var settingsHash = (Hashtable)settingsObj; 24 | Populate(settingsHash); 25 | } 26 | 27 | public static void Populate(Hashtable settingsHash) 28 | { 29 | Settings = new SettingsDto 30 | { 31 | Debug = GetSetting(settingsHash, "Debug", false), 32 | HorizontalAlignment = GetSetting(settingsHash, "HorizontalAlignment", HorizontalAlignmentEnum.Right), 33 | HorizontalOffset = GetSetting(settingsHash, "HorizontalOffset", 0), 34 | VerticalOffset = GetSetting(settingsHash, "VerticalOffset", -1), 35 | DisableTelemetry = GetSetting(settingsHash, "DisableTelemetry", false) 36 | }; 37 | 38 | var providerSettingsObj = settingsHash["Providers"]; 39 | 40 | if (providerSettingsObj is not Hashtable) 41 | { 42 | providerSettingsObj = new Hashtable(); 43 | } 44 | 45 | var providerSettingsHash = (Hashtable)providerSettingsObj; 46 | 47 | foreach (var provider in Settings.Providers) 48 | { 49 | PopulateProviderSettings(provider.Key, providerSettingsHash); 50 | } 51 | } 52 | 53 | private static void PopulateProviderSettings(string providerKey, Hashtable providerSettingsHash) 54 | { 55 | var provider = ProviderFactory.GetProvider(providerKey); 56 | var providerSettings = (Hashtable)providerSettingsHash[providerKey]; 57 | Settings.Providers[providerKey].Commands = GetSetting(providerSettings, "Commands", provider.DefaultCommands); 58 | Settings.Providers[providerKey].NounPrefixes = GetSetting(providerSettings, "NounPrefixes", provider.DefaultNounPrefixes); 59 | Settings.Providers[providerKey].FgColor = GetSetting(providerSettings, "FgColor", provider.DefaultFgColor); 60 | Settings.Providers[providerKey].BgColor = GetSetting(providerSettings, "BgColor", provider.DefaultBgColor); 61 | Settings.Providers[providerKey].Template = GetSetting(providerSettings, "Template", provider.DefaultTemplate); 62 | } 63 | 64 | internal static T GetSetting(Hashtable settings, string settingKey, T defaultValue) 65 | { 66 | if (settings == null) 67 | { 68 | return defaultValue; 69 | } 70 | 71 | var targetType = typeof(T); 72 | var settingObj = settings[settingKey]; 73 | if (settingObj == null) 74 | { 75 | return defaultValue; 76 | } 77 | 78 | var typedValue = (T)ConvertValue(settingObj, targetType, defaultValue); 79 | 80 | return typedValue; 81 | } 82 | 83 | internal static object ConvertValue(object value, Type targetType, object defaultValue) 84 | { 85 | try 86 | { 87 | if (targetType == typeof(string)) 88 | { 89 | return value.ToString(); 90 | } 91 | 92 | if (targetType.IsEnum) 93 | { 94 | if (value == null) 95 | { 96 | return defaultValue; 97 | } 98 | 99 | return Enum.Parse(targetType, value.ToString(), true); 100 | } 101 | 102 | if (targetType.IsValueType) 103 | { 104 | return Convert.ChangeType(value, targetType); 105 | } 106 | 107 | throw new NotImplementedException($"Setting has an invalid type: {targetType}"); 108 | } 109 | catch 110 | { 111 | throw new NotImplementedException($"Something went wrong parsing the value: '{value}' as '{targetType}'"); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/MagicTooltips/Services/TriggerService.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Providers; 2 | using System.Collections.Generic; 3 | 4 | namespace MagicTooltips.Services 5 | { 6 | public class TriggerService 7 | { 8 | internal static readonly Dictionary> CommandList = new(); 9 | internal static readonly Dictionary> NounPrefixList = new(); 10 | 11 | public static void PopulateTriggers() 12 | { 13 | foreach (var provider in SettingsService.Settings.Providers) 14 | { 15 | AddTriggers(provider.Key, provider.Value.Commands); 16 | AddTriggers(provider.Key, provider.Value.NounPrefixes, true); 17 | } 18 | } 19 | 20 | private static void AddTriggers(string provider, string triggersCsv, bool isNounPrefix = false) 21 | { 22 | if (string.IsNullOrEmpty(triggersCsv)) 23 | { 24 | return; 25 | } 26 | 27 | LoggingService.LogDebug($"AddTriggers {provider}"); 28 | var triggers = triggersCsv.ToLowerInvariant().Split(','); 29 | foreach (var trigger in triggers) 30 | { 31 | AddTrigger(trigger, provider, isNounPrefix ? NounPrefixList : CommandList); 32 | } 33 | } 34 | 35 | private static void AddTrigger(string trigger, string providerKey, Dictionary> triggerList) 36 | { 37 | LoggingService.LogDebug($" {trigger} -> {providerKey}"); 38 | if (!triggerList.ContainsKey(trigger)) 39 | { 40 | triggerList.Add(trigger, new List()); 41 | } 42 | var provider = ProviderFactory.GetProvider(providerKey); 43 | triggerList[trigger].Add(provider); 44 | } 45 | 46 | internal static IEnumerable ParseLine(string line) 47 | { 48 | List results = new(); 49 | 50 | var workingSet = line; 51 | 52 | var dashPos = line.IndexOf('-'); 53 | while (dashPos > -1) 54 | { 55 | // if dash is first character, move on... 56 | if (dashPos == 0) 57 | { 58 | workingSet = workingSet.Substring(dashPos + 1); 59 | } 60 | else 61 | { 62 | // the character before the dash cannot be blank 63 | var lastVerbChar = workingSet.Substring(dashPos - 1, 1); 64 | if (lastVerbChar == " ") 65 | { 66 | // toss this dash and continue 67 | workingSet = workingSet.Substring(dashPos + 1); 68 | } 69 | else 70 | { 71 | workingSet = workingSet.Substring(dashPos + 1); 72 | var spacePos = workingSet.IndexOf(" "); 73 | string noun; 74 | if (spacePos > 0) 75 | { 76 | noun = workingSet.Substring(0, spacePos); 77 | 78 | if (spacePos == workingSet.Length) 79 | { 80 | workingSet = workingSet.Substring(spacePos); 81 | } 82 | else 83 | { 84 | workingSet = workingSet.Substring(spacePos + 1); 85 | } 86 | } 87 | else 88 | { 89 | noun = workingSet; 90 | } 91 | results.Add(noun.ToLowerInvariant()); 92 | } 93 | 94 | } 95 | 96 | dashPos = workingSet.IndexOf('-'); 97 | }; 98 | 99 | return results; 100 | 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/MagicTooltipsTests/MagicTooltipsTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/MagicTooltipsTests/RenderServiceTests.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace MagicTooltipsTests 5 | { 6 | [TestClass] 7 | public class RenderServiceTests 8 | { 9 | [DataTestMethod] 10 | [DataRow("", "#ff0000", "#ffffff", "")] 11 | [DataRow(null, "#ff0000", "#ffffff", "")] 12 | [DataRow("foo", "#ff0000", "#ffffff", "\x1b[38;2;255;0;0;48;2;255;255;255mfoo\x1b[0m")] 13 | [DataRow("foo", "#ff0000", "", "\x1b[38;2;255;0;0mfoo\x1b[0m")] 14 | [DataRow("foo", "", "#ffffff", "\x1b[48;2;255;255;255mfoo\x1b[0m")] 15 | public void GetColoredString(string text, string fgColor, string bgColor, string expected) 16 | { 17 | var output = RenderService.GetColoredString(text, fgColor, bgColor); 18 | Assert.AreEqual(expected, output); 19 | } 20 | 21 | [DataTestMethod] 22 | [DataRow("")] 23 | [DataRow(null)] 24 | [DataRow("foo")] 25 | [DataRow("000000")] 26 | [DataRow("#000")] 27 | [DataRow("#GGGGGG")] 28 | public void ConvertHexToRgb_Invalid(string input) 29 | { 30 | var output = RenderService.ConvertHexToRgb(input); 31 | Assert.IsNull(output); 32 | } 33 | 34 | [DataTestMethod] 35 | [DataRow("#FF0000", 255, 0, 0)] 36 | [DataRow("#ff0000", 255, 0, 0)] 37 | [DataRow("#00FF00", 0, 255, 0)] 38 | [DataRow("#0000FF", 0, 0, 255)] 39 | public void ConvertHexToRgb(string input, int expectedR, int expectedG, int expectedB) 40 | { 41 | var output = RenderService.ConvertHexToRgb(input); 42 | Assert.AreEqual((expectedR, expectedG, expectedB), output); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/MagicTooltipsTests/SettingsServiceTests.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Collections; 4 | 5 | namespace MagicTooltipsTests 6 | { 7 | [TestClass] 8 | public class SettingsServiceTests 9 | { 10 | [DataTestMethod] 11 | [DataRow("left", "left")] 12 | [DataRow("right", "right")] 13 | [DataRow(null, "right")] 14 | public void Populate_HorizontalAlignment(string input, string expected) 15 | { 16 | var hash = new Hashtable 17 | { 18 | {"HorizontalAlignment", input } 19 | }; 20 | 21 | SettingsService.Populate(hash); 22 | 23 | Assert.AreEqual(expected, SettingsService.Settings.HorizontalAlignment.ToString(), true); 24 | } 25 | 26 | [DataTestMethod] 27 | [DataRow(true, true)] 28 | [DataRow(false, false)] 29 | [DataRow(null, false)] 30 | public void Populate_Debug(bool? input, bool expected) 31 | { 32 | var hash = new Hashtable 33 | { 34 | {"Debug", input } 35 | }; 36 | 37 | SettingsService.Populate(hash); 38 | 39 | Assert.AreEqual(expected, SettingsService.Settings.Debug); 40 | } 41 | 42 | [DataTestMethod] 43 | [DataRow(99, 99)] 44 | [DataRow(-99, -99)] 45 | [DataRow(null, 0)] 46 | public void Populate_Horizontaloffset(int? input, int expected) 47 | { 48 | var hash = new Hashtable 49 | { 50 | {"HorizontalOffset", input } 51 | }; 52 | 53 | SettingsService.Populate(hash); 54 | 55 | Assert.AreEqual(expected, SettingsService.Settings.HorizontalOffset); 56 | } 57 | 58 | [DataTestMethod] 59 | [DataRow("foo", "foo")] 60 | [DataRow(null, "kubectl,helm,kubens,kubectx,oc,istioctl,kogito,k9s,helmlist")] 61 | public void Populate_KubernetesCommands(string input, string expected) 62 | { 63 | var hash = new Hashtable 64 | { 65 | { "Providers", new Hashtable 66 | { 67 | { "kubernetes", new Hashtable 68 | { 69 | { "Commands", input } 70 | } 71 | } 72 | } 73 | } 74 | }; 75 | 76 | SettingsService.Populate(hash); 77 | 78 | Assert.AreEqual(expected, SettingsService.Settings.Providers["kubernetes"].Commands); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/MagicTooltipsTests/TriggerServiceTests.cs: -------------------------------------------------------------------------------- 1 | using MagicTooltips.Services; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace MagicTooltipsTests 9 | { 10 | [TestClass] 11 | public class TriggerServiceTests 12 | { 13 | [DataTestMethod] 14 | [DataRow("Get-MgApplication", "mgapplication" )] 15 | [DataRow("Get-MgApplication -ApplicationId c1ca6040-d0ae-493d-9b48-d35018390ea2 ", "mgapplication", "d0ae-493d-9b48-d35018390ea2")] 16 | [DataRow("Foreach-Object -Process { Get-MgApplication -ApplicationId c1ca6040-d0ae-493d-9b48-d35018390ea2 }", "object", "mgapplication", "d0ae-493d-9b48-d35018390ea2")] 17 | [DataRow("Get-AzAdApplication", "azadapplication")] 18 | public void ParseLine(string input, params string[] expected) 19 | { 20 | var actual = TriggerService.ParseLine(input).ToArray(); 21 | CollectionAssert.AreEqual(expected, actual); 22 | } 23 | 24 | } 25 | } 26 | --------------------------------------------------------------------------------