├── .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 | 
6 |
7 | Pairs nicely with custom prompts, such as [oh-my-posh](https://github.com/JanDeDobbeleer/oh-my-posh)!
8 | 
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 |
--------------------------------------------------------------------------------