├── .github └── workflows │ └── pester-test.yml ├── CHANGELOG.md ├── DynamicTitle ├── DynamicTitle.psd1 ├── DynamicTitle.psm1 ├── Examples │ ├── AllInOne.ps1 │ ├── CommandExecutionTime.ps1 │ ├── GitStatus.ps1 │ ├── Road.ps1 │ ├── Spinner.ps1 │ └── StatusBar.ps1 ├── Private │ ├── GlobalStore.ps1 │ ├── Pacemaker.ps1 │ ├── Thread.ps1 │ ├── _BackgroundJobHelper.ps1 │ └── _TitleUpdateHelper.ps1 └── Public │ ├── Enter-DTLegacyApplicationMode.ps1 │ ├── Exit-DTLegacyApplicationMode.ps1 │ ├── Get-DTExamplesPath.ps1 │ ├── Get-DTJobLatestOutput.ps1 │ ├── Start-DTExample.ps1 │ ├── Start-DTJobBackgroundThreadTimer.ps1 │ ├── Start-DTJobCommandPreExecutionCallback.ps1 │ ├── Start-DTJobPromptCallback.ps1 │ ├── Start-DTTitle.ps1 │ └── Stop-DTTitle.ps1 ├── LICENSE ├── README.md └── Tests ├── DynamicTitle.Tests.ps1 ├── Public ├── Enter-DTLegacyApplicationMode.Tests.ps1 ├── Exit-DTLegacyApplicationMode.Tests.ps1 ├── Get-DTJobLatestOutput.Tests.ps1 ├── Start-DTExample.Tests.ps1 ├── Start-DTJobBackgroundThreadTimer.Tests.ps1 ├── Start-DTJobCommandPreExecutionCallback.Tests.ps1 ├── Start-DTJobPromptCallback.Tests.ps1 ├── Start-DTTitle.Tests.ps1 └── Stop-DTTitle.Tests.ps1 └── RunTests.ps1 /.github/workflows/pester-test.yml: -------------------------------------------------------------------------------- 1 | name: Pester Test 2 | on: push 3 | 4 | jobs: 5 | windows-latest: 6 | name: Windows Latest 7 | runs-on: windows-latest 8 | steps: 9 | - name: Check out repository code 10 | uses: actions/checkout@v3 11 | - name: Run tests on pwsh 12 | shell: pwsh 13 | run: | 14 | & Tests\RunTests.ps1 15 | - name: Run tests on Windows PowerShell 16 | shell: powershell 17 | run: | 18 | & Tests\RunTests.ps1 19 | 20 | ubuntu-latest: 21 | name: Ubuntu Latest 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Check out repository code 25 | uses: actions/checkout@v3 26 | - name: Run tests 27 | shell: pwsh 28 | run: | 29 | & ./Tests/RunTests.ps1 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0] - 2023-07-10 4 | 5 | ### Added 6 | 7 | - Added `Spinner` example 8 | - Added `Road` example 9 | 10 | ### Changed 11 | 12 | - Changed the `AllInOne` example to show the network segment only in the first process 13 | 14 | ### Fixed 15 | 16 | - Removed a newline in the weather segment of the `StatusBar` example 17 | 18 | ## [0.3.0] - 2023-03-26 19 | 20 | ### Added 21 | 22 | - Added `Get-DTExamplesPath` 23 | - Added `Start-DTExample` 24 | - Added Examples directory to the module 25 | 26 | ## [0.2.0] - 2023-03-20 27 | 28 | ### Added 29 | 30 | - Added `Enter-DTLegacyApplicationMode` 31 | - Added `Exit-DTLegacyApplicationMode` 32 | 33 | ### Changed 34 | 35 | - Errors occurred in jobs and the title update thread are shown on the host 36 | 37 | ## [0.1.0] - 2023-03-11 38 | 39 | ### Added 40 | 41 | - Added `Get-DTJobLatestOutput` 42 | - Added `Start-DTJobBackgroundThreadTimer` 43 | - Added `Start-DTJobCommandPreExecutionCallback` 44 | - Added `Start-DTJobPromptCallback` 45 | - Added `Start-DTTitle` 46 | - Added `Stop-DTTitle` -------------------------------------------------------------------------------- /DynamicTitle/DynamicTitle.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script module or binary module file associated with this manifest. 4 | RootModule = 'DynamicTitle.psm1' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '0.4.0' 8 | 9 | # Supported PSEditions 10 | CompatiblePSEditions = @('Desktop', 'Core') 11 | 12 | # ID used to uniquely identify this module 13 | GUID = '96299aa9-83dc-4ffd-ae83-09afc0e5ef3c' 14 | 15 | # Author of this module 16 | Author = 'mdgrs-mei' 17 | 18 | # Company or vendor of this module 19 | CompanyName = 'Unknown' 20 | 21 | # Copyright statement for this module 22 | Copyright = '(c) mdgrs-mei. All rights reserved.' 23 | 24 | # Description of the functionality provided by this module 25 | Description = 'Module for advanced console title customizations' 26 | 27 | # Minimum version of the PowerShell engine required by this module 28 | PowerShellVersion = '5.1' 29 | 30 | # Name of the PowerShell host required by this module 31 | # PowerShellHostName = '' 32 | 33 | # Minimum version of the PowerShell host required by this module 34 | # PowerShellHostVersion = '' 35 | 36 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 37 | # DotNetFrameworkVersion = '' 38 | 39 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 40 | # ClrVersion = '' 41 | 42 | # Processor architecture (None, X86, Amd64) required by this module 43 | # ProcessorArchitecture = '' 44 | 45 | # Modules that must be imported into the global environment prior to importing this module 46 | # RequiredModules = @() 47 | 48 | # Assemblies that must be loaded prior to importing this module 49 | # RequiredAssemblies = @() 50 | 51 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 52 | # ScriptsToProcess = @() 53 | 54 | # Type files (.ps1xml) to be loaded when importing this module 55 | # TypesToProcess = @() 56 | 57 | # Format files (.ps1xml) to be loaded when importing this module 58 | # FormatsToProcess = @() 59 | 60 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 61 | # NestedModules = @() 62 | 63 | # 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. 64 | FunctionsToExport = @( 65 | 'Enter-DTLegacyApplicationMode', 66 | 'Exit-DTLegacyApplicationMode', 67 | 'Get-DTExamplesPath', 68 | 'Get-DTJobLatestOutput', 69 | 'Start-DTExample', 70 | 'Start-DTJobBackgroundThreadTimer', 71 | 'Start-DTJobCommandPreExecutionCallback', 72 | 'Start-DTJobPromptCallback', 73 | 'Start-DTTitle', 74 | 'Stop-DTTitle' 75 | ) 76 | 77 | # 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. 78 | CmdletsToExport = @() 79 | 80 | # Variables to export from this module 81 | VariablesToExport = @() 82 | 83 | # 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. 84 | AliasesToExport = @() 85 | 86 | # DSC resources to export from this module 87 | # DscResourcesToExport = @() 88 | 89 | # List of all modules packaged with this module 90 | # ModuleList = @() 91 | 92 | # List of all files packaged with this module 93 | # FileList = @() 94 | 95 | # 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. 96 | PrivateData = @{ 97 | 98 | PSData = @{ 99 | 100 | # Tags applied to this module. These help with module discovery in online galleries. 101 | Tags = @('Windows', 'Linux', 'MacOS') 102 | 103 | # A URL to the license for this module. 104 | LicenseUri = 'https://github.com/mdgrs-mei/DynamicTitle/blob/main/LICENSE' 105 | 106 | # A URL to the main website for this project. 107 | ProjectUri = 'https://github.com/mdgrs-mei/DynamicTitle' 108 | 109 | # A URL to an icon representing this module. 110 | # IconUri = '' 111 | 112 | # ReleaseNotes of this module 113 | # ReleaseNotes = '' 114 | 115 | # Prerelease string of this module 116 | # Prerelease = '' 117 | 118 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 119 | # RequireLicenseAcceptance = $false 120 | 121 | # External dependent modules of this module 122 | # ExternalModuleDependencies = @() 123 | 124 | } # End of PSData hashtable 125 | 126 | } # End of PrivateData hashtable 127 | 128 | # HelpInfo URI of this module 129 | # HelpInfoURI = '' 130 | 131 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 132 | # DefaultCommandPrefix = '' 133 | 134 | } 135 | 136 | -------------------------------------------------------------------------------- /DynamicTitle/DynamicTitle.psm1: -------------------------------------------------------------------------------- 1 | 2 | $private:privateScripts = @(Get-ChildItem $PSScriptRoot\Private\*.ps1 -Exclude _*) 3 | $private:publicScripts = @(Get-ChildItem $PSScriptRoot\Public\*.ps1) 4 | foreach ($private:script in ($privateScripts + $publicScripts)) 5 | { 6 | . $script.FullName 7 | } 8 | 9 | $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {Stop-DTTitle} 10 | 11 | Export-ModuleMember -Function $publicScripts.BaseName 12 | -------------------------------------------------------------------------------- /DynamicTitle/Examples/AllInOne.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules DynamicTitle 2 | 3 | if ($IsLinux -or $IsMacOS) 4 | { 5 | Write-Error -Message 'Runs only on Windows.' -Category InvalidOperation 6 | return 7 | } 8 | $modulePath = Join-Path (Get-Module DynamicTitle).ModuleBase 'DynamicTitle.psd1' 9 | 10 | $netThroughputJob = Start-DTJobBackgroundThreadTimer -ScriptBlock { 11 | $netInterface = (Get-CimInstance -class Win32_PerfFormattedData_Tcpip_NetworkInterface)[0] 12 | [Int]($netInterface.BytesReceivedPersec * 8 / 1MB), [Int]($netInterface.BytesSentPersec * 8 / 1MB) 13 | } -IntervalMilliseconds 1000 14 | 15 | $commandStartJob = Start-DTJobCommandPreExecutionCallback -ScriptBlock { 16 | param($command) 17 | (Get-Date), $command 18 | } 19 | 20 | $promptJob = Start-DTJobPromptCallback -ScriptBlock { 21 | (Get-Date), (Get-Location).Path 22 | } 23 | 24 | 25 | $importModuleScript = { 26 | param ($modulePath) 27 | Import-Module $modulePath 28 | } 29 | 30 | $gitJob = Start-DTJobBackgroundThreadTimer -ScriptBlock { 31 | param ($promptJob) 32 | $date, $location = Get-DTJobLatestOutput $promptJob 33 | if (-not $location) 34 | { 35 | return 36 | } 37 | 38 | Set-Location $location 39 | $branch = git branch --show-current 40 | if ($LastExitCode -ne 0) 41 | { 42 | # not a git repository 43 | return 44 | } 45 | if (-not $branch) 46 | { 47 | $branch = '❔' 48 | } 49 | 50 | $statusLines = git --no-optional-locks status -s 51 | $modifiedCount = 0 52 | $unversionedCount = 0 53 | foreach ($line in $statusLines) 54 | { 55 | $type = $line.Substring(0, 2) 56 | if (($type -eq ' M') -or ($type -eq ' R')) 57 | { 58 | $modifiedCount++ 59 | } 60 | elseif ($type -eq '??') 61 | { 62 | $unversionedCount++ 63 | } 64 | } 65 | 66 | $gitStatus = '🌿[{0}] ✏️{1}❔{2}' -f $branch, $modifiedCount, $unversionedCount 67 | $gitStatus, $location 68 | 69 | } -IntervalMilliseconds 2000 -ArgumentList $promptJob -InitializationScript $importModuleScript -InitializationArgumentList $modulePath 70 | 71 | 72 | $initializationScript = { 73 | param ($modulePath) 74 | Import-Module $modulePath 75 | $psVersion = 'PS ' + $PSVersionTable.PSVersion.ToString() 76 | $psVersion # For PSUseDeclaredVarsMoreThanAssignments false detection. 77 | 78 | $isInitialProcess = $false 79 | $mutex = [System.Threading.Mutex]::new($false, 'Global\DynamicTitleAllInOneMutex', ([ref]$isInitialProcess)) 80 | $mutex # For PSUseDeclaredVarsMoreThanAssignments false detection. 81 | } 82 | 83 | $scriptBlock = { 84 | param($netThroughputJob, $commandStartJob, $promptJob, $gitJob) 85 | 86 | $mbpsReceived, $mbpsSent = Get-DTJobLatestOutput $netThroughputJob 87 | $commandStartDate, $command = Get-DTJobLatestOutput $commandStartJob 88 | $commandEndDate, $location = Get-DTJobLatestOutput $promptJob 89 | $gitStatus, $gitLocation = Get-DTJobLatestOutput $gitJob 90 | 91 | if ($isInitialProcess -and ($mbpsReceived -or $mbpsSent)) 92 | { 93 | $netThroughputSegment = '' 94 | if ($mbpsSent) 95 | { 96 | $netThroughputSegment += '🔼{0}Mbps ' -f $mbpsSent 97 | } 98 | if ($mbpsReceived) 99 | { 100 | $netThroughputSegment += '🔽{0}Mbps' -f $mbpsReceived 101 | } 102 | } 103 | 104 | if ($null -ne $commandStartDate) 105 | { 106 | if (($null -eq $commandEndDate) -or ($commandEndDate -lt $commandStartDate)) 107 | { 108 | $commandDuration = (Get-Date) - $commandStartDate 109 | $isCommandRunning = $true 110 | } 111 | else 112 | { 113 | $commandDuration = $commandEndDate - $commandStartDate 114 | } 115 | } 116 | 117 | if ($command) 118 | { 119 | $command = $command.Split()[0] 120 | } 121 | 122 | $commandStatus = '🟢' 123 | if ($commandDuration) 124 | { 125 | if ($commandDuration.TotalSeconds -gt 1) 126 | { 127 | $commandSegment = '[{0}]-⌚{1}' -f $command, $commandDuration.ToString('mm\:ss') 128 | if ($isCommandRunning) 129 | { 130 | $commandStatus = '🟠' 131 | } 132 | } 133 | } 134 | 135 | if ($location) 136 | { 137 | $folderName = Split-Path $location -Leaf 138 | } 139 | if ($gitLocation -ne $location) 140 | { 141 | $gitStatus = $null 142 | } 143 | 144 | '{0} {1} {2} 🗂️{3} {4} {5}' -f $commandStatus, $psVersion, $commandSegment, $folderName, $gitStatus, $netThroughputSegment 145 | } 146 | 147 | $params = @{ 148 | ScriptBlock = $scriptBlock 149 | ArgumentList = $netThroughputJob, $commandStartJob, $promptJob, $gitJob 150 | InitializationScript = $initializationScript 151 | InitializationArgumentList = $modulePath 152 | } 153 | 154 | Start-DTTitle @params 155 | -------------------------------------------------------------------------------- /DynamicTitle/Examples/CommandExecutionTime.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules DynamicTitle 2 | 3 | $modulePath = Join-Path (Get-Module DynamicTitle).ModuleBase 'DynamicTitle.psd1' 4 | 5 | $commandStartJob = Start-DTJobCommandPreExecutionCallback -ScriptBlock { 6 | param($command) 7 | (Get-Date), $command 8 | } 9 | 10 | $commandEndJob = Start-DTJobPromptCallback -ScriptBlock { 11 | Get-Date 12 | } 13 | 14 | $initializationScript = { 15 | param ($modulePath) 16 | Import-Module $modulePath 17 | $psVersion = 'PS ' + $PSVersionTable.PSVersion.ToString() 18 | $psVersion # For PSUseDeclaredVarsMoreThanAssignments false detection. 19 | } 20 | $scriptBlock = { 21 | param($commandStartJob, $commandEndJob) 22 | $commandStartDate, $command = Get-DTJobLatestOutput $commandStartJob 23 | $commandEndDate = Get-DTJobLatestOutput $commandEndJob 24 | if ($null -ne $commandStartDate) 25 | { 26 | if (($null -eq $commandEndDate) -or ($commandEndDate -lt $commandStartDate)) 27 | { 28 | $commandDuration = (Get-Date) - $commandStartDate 29 | $isCommandRunning = $true 30 | } 31 | else 32 | { 33 | $commandDuration = $commandEndDate - $commandStartDate 34 | } 35 | } 36 | 37 | if ($command) 38 | { 39 | $command = $command.Split()[0] 40 | } 41 | 42 | $status = '🟢' 43 | if ($commandDuration) 44 | { 45 | if ($commandDuration.TotalSeconds -gt 1) 46 | { 47 | $commandSegment = '[{0}]-⌚{1}' -f $command, $commandDuration.ToString('mm\:ss') 48 | if ($isCommandRunning) 49 | { 50 | $status = '🟠' 51 | } 52 | } 53 | } 54 | 55 | '{0} {1} {2}' -f $status, $psVersion, $commandSegment 56 | } 57 | 58 | $params = @{ 59 | ScriptBlock = $scriptBlock 60 | ArgumentList = $commandStartJob, $commandEndJob 61 | InitializationScript = $initializationScript 62 | InitializationArgumentList = $modulePath 63 | } 64 | 65 | Start-DTTitle @params 66 | -------------------------------------------------------------------------------- /DynamicTitle/Examples/GitStatus.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules DynamicTitle 2 | 3 | $modulePath = Join-Path (Get-Module DynamicTitle).ModuleBase 'DynamicTitle.psd1' 4 | 5 | $initializationScript = { 6 | param ($modulePath) 7 | Import-Module $modulePath 8 | } 9 | 10 | $promptJob = Start-DTJobPromptCallback -ScriptBlock { 11 | (Get-Location).Path 12 | } 13 | 14 | $gitJob = Start-DTJobBackgroundThreadTimer -ScriptBlock { 15 | param ($promptJob) 16 | $location = Get-DTJobLatestOutput $promptJob 17 | if (-not $location) 18 | { 19 | return 20 | } 21 | 22 | Set-Location $location 23 | $branch = git branch --show-current 24 | if ($LastExitCode -ne 0) 25 | { 26 | # not a git repository 27 | return 28 | } 29 | if (-not $branch) 30 | { 31 | $branch = '❔' 32 | } 33 | 34 | $statusLines = git --no-optional-locks status -s 35 | $modifiedCount = 0 36 | $unversionedCount = 0 37 | foreach ($line in $statusLines) 38 | { 39 | $type = $line.Substring(0, 2) 40 | if (($type -eq ' M') -or ($type -eq ' R')) 41 | { 42 | $modifiedCount++ 43 | } 44 | elseif ($type -eq '??') 45 | { 46 | $unversionedCount++ 47 | } 48 | } 49 | 50 | $gitStatus = '🌿[{0}] ✏️{1}❔{2}' -f $branch, $modifiedCount, $unversionedCount 51 | $gitStatus, $location 52 | 53 | } -IntervalMilliseconds 1000 -ArgumentList $promptJob -InitializationScript $initializationScript -InitializationArgumentList $modulePath 54 | 55 | $scriptBlock = { 56 | param($promptJob, $gitJob) 57 | 58 | $location = Get-DTJobLatestOutput $promptJob 59 | $gitStatus, $gitLocation = Get-DTJobLatestOutput $gitJob 60 | 61 | if ($location) 62 | { 63 | $folderName = Split-Path $location -Leaf 64 | } 65 | if ($gitLocation -ne $location) 66 | { 67 | $gitStatus = $null 68 | } 69 | 70 | '🗂️{0} {1}' -f $folderName, $gitStatus 71 | } 72 | 73 | $params = @{ 74 | ScriptBlock = $scriptBlock 75 | ArgumentList = $promptJob, $gitJob 76 | InitializationScript = $initializationScript 77 | InitializationArgumentList = $modulePath 78 | } 79 | 80 | Start-DTTitle @params 81 | -------------------------------------------------------------------------------- /DynamicTitle/Examples/Road.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules DynamicTitle 2 | 3 | # Suppress this for $initializationScript 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 5 | param() 6 | 7 | $modulePath = Join-Path (Get-Module DynamicTitle).ModuleBase 'DynamicTitle.psd1' 8 | 9 | $promptJob = Start-DTJobPromptCallback { 10 | if ($null -eq $script:roadPromptFrame) { 11 | $script:roadPromptFrame = 0 12 | } 13 | $script:roadPromptFrame++ 14 | 15 | $isInError = $false 16 | if ($global:Error[0]) { 17 | $isInError = -not ($global:Error[0].Equals($script:roadLastError)) 18 | $script:roadLastError = $global:Error[0] 19 | } 20 | $isInError, $script:roadPromptFrame 21 | } 22 | 23 | $initializationScript = { 24 | param ($modulePath) 25 | Import-Module $modulePath 26 | 27 | $mainTitle = ' PowerShell ' 28 | $characters = @( 29 | '🚴' 30 | '🚴‍♂️' 31 | '🚴‍♀️' 32 | '🚙' 33 | '🚓' 34 | '🐈' 35 | '🐩' 36 | '🚚' 37 | '🚎' 38 | '🚕' 39 | '🚌' 40 | '🚒' 41 | '🚛' 42 | '🚁' 43 | '🛸' 44 | ) 45 | $caution = '❗' 46 | 47 | $streetParts = @( 48 | '_._._.' 49 | '_.-._' 50 | ) 51 | $streetLength = 2 52 | 53 | function GetCharacter { 54 | $characters | Get-Random 55 | } 56 | function GetStreet { 57 | $street = '' 58 | foreach ($i in 1..$streetLength) { 59 | $street += $streetParts | Get-Random 60 | } 61 | $street 62 | } 63 | function GetWaitFrame { 64 | Get-Random -Minimum 0 -Maximum 100 65 | } 66 | 67 | $character = GetCharacter 68 | $streetL = GetStreet 69 | $streetR = GetStreet 70 | $waitFrame = GetWaitFrame 71 | $characterPos = 1 72 | $lastPromptFrame = 0 73 | $isCaution = $false 74 | } 75 | 76 | $scriptBlock = { 77 | param($promptJob) 78 | 79 | $isInError, $promptFrame = Get-DTJobLatestOutput $promptJob 80 | if ($isInError -and ($promptFrame -ne $script:lastPromptFrame)) { 81 | $script:isCaution = $true 82 | } 83 | if (-not $isInError) { 84 | $script:isCaution = $false 85 | } 86 | $script:lastPromptFrame = $promptFrame 87 | 88 | $title = $streetL + $mainTitle + $streetR 89 | if ($script:waitFrame -gt 0) { 90 | $script:waitFrame-- 91 | $script:isCaution = $false 92 | $title 93 | return 94 | } 95 | 96 | $stringInfo = [System.Globalization.StringInfo]::new($title) 97 | $length = $stringInfo.LengthInTextElements 98 | $characterIndex = $length - 1 - $script:characterPos 99 | 100 | if ($script:isCaution) { 101 | if ($characterIndex -ge 1) { 102 | $characterIndex -= 1 103 | $character = $caution + $character 104 | } else { 105 | $character = $character + $caution 106 | } 107 | $title = $stringInfo.SubstringByTextElements(0, $characterIndex) + $character + $stringInfo.SubstringByTextElements($characterIndex + 2) 108 | } else { 109 | $title = $stringInfo.SubstringByTextElements(0, $characterIndex) + $character + $stringInfo.SubstringByTextElements($characterIndex + 1) 110 | $script:characterPos += 1 111 | if ($script:characterPos -ge $length) { 112 | $script:characterPos = 1 113 | $script:waitFrame = GetWaitFrame 114 | $script:character = GetCharacter 115 | } 116 | } 117 | $title 118 | } 119 | 120 | $params = @{ 121 | ScriptBlock = $scriptBlock 122 | ArgumentList = $promptJob 123 | InitializationScript = $initializationScript 124 | InitializationArgumentList = $modulePath 125 | } 126 | 127 | Start-DTTitle @params -------------------------------------------------------------------------------- /DynamicTitle/Examples/Spinner.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules DynamicTitle 2 | 3 | # Suppress this for $initializationScript 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 5 | param() 6 | 7 | $modulePath = Join-Path (Get-Module DynamicTitle).ModuleBase 'DynamicTitle.psd1' 8 | 9 | $commandStartJob = Start-DTJobCommandPreExecutionCallback -ScriptBlock { 10 | Get-Date 11 | } 12 | 13 | $commandEndJob = Start-DTJobPromptCallback -ScriptBlock { 14 | Get-Date 15 | } 16 | 17 | $initializationScript = { 18 | param ($modulePath) 19 | Import-Module $modulePath 20 | $spinnerSymbols = @('🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘') 21 | $spinnerSymbolIndex = 0 22 | } 23 | 24 | $scriptBlock = { 25 | param($commandStartJob, $commandEndJob) 26 | $commandStartDate = Get-DTJobLatestOutput $commandStartJob 27 | $commandEndDate = Get-DTJobLatestOutput $commandEndJob 28 | if ($null -ne $commandStartDate) 29 | { 30 | if (($null -eq $commandEndDate) -or ($commandEndDate -lt $commandStartDate)) 31 | { 32 | $commandDuration = (Get-Date) - $commandStartDate 33 | } 34 | } 35 | 36 | $spinner = $spinnerSymbols[0] 37 | if ($commandDuration) 38 | { 39 | if ($commandDuration.TotalSeconds -gt 1) 40 | { 41 | $script:spinnerSymbolIndex = ($script:spinnerSymbolIndex + 1) % $spinnerSymbols.Count 42 | $spinner = $spinnerSymbols[$script:spinnerSymbolIndex] 43 | } 44 | } 45 | else 46 | { 47 | $script:spinnerSymbolIndex = 0 48 | } 49 | 50 | '{0} PowerShell' -f $spinner 51 | } 52 | 53 | $params = @{ 54 | ScriptBlock = $scriptBlock 55 | ArgumentList = $commandStartJob, $commandEndJob 56 | InitializationScript = $initializationScript 57 | InitializationArgumentList = $modulePath 58 | } 59 | 60 | Start-DTTitle @params 61 | -------------------------------------------------------------------------------- /DynamicTitle/Examples/StatusBar.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules DynamicTitle 2 | 3 | if ($IsLinux -or $IsMacOS) 4 | { 5 | Write-Error -Message 'Runs only on Windows.' -Category InvalidOperation 6 | return 7 | } 8 | $modulePath = Join-Path (Get-Module DynamicTitle).ModuleBase 'DynamicTitle.psd1' 9 | 10 | $weatherJob = Start-DTJobBackgroundThreadTimer -ScriptBlock { 11 | $weather = Invoke-RestMethod https://wttr.in/?format="%c%t" 12 | $weather 13 | } -IntervalMilliseconds 60000 14 | 15 | $systemInfoJob = Start-DTJobBackgroundThreadTimer -ScriptBlock { 16 | $cpuUsage = (Get-Counter -Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue 17 | $netInterface = (Get-CimInstance -class Win32_PerfFormattedData_Tcpip_NetworkInterface)[0] 18 | $cpuUsage, ($netInterface.BytesReceivedPersec * 8), ($netInterface.BytesSentPersec * 8) 19 | } -IntervalMilliseconds 1000 20 | 21 | $initializationScript = { 22 | param ($modulePath) 23 | Import-Module $modulePath 24 | } 25 | 26 | $scriptBlock = { 27 | param($weatherJob, $systemInfoJob) 28 | 29 | $weather = Get-DTJobLatestOutput $weatherJob 30 | $cpuUsage, $bpsReceived, $bpsSent = Get-DTJobLatestOutput $systemInfoJob 31 | $date = Get-Date -Format 'MMM dd HH:mm:ss' 32 | 33 | '📆 {0} {1} --- 🔥CPU:{2:f1}% 🔼{3}Mbps 🔽{4}Mbps' -f $date, $weather, [double]$cpuUsage, [Int]($bpsSent/1MB), [Int]($bpsReceived/1MB) 34 | } 35 | 36 | $params = @{ 37 | ScriptBlock = $scriptBlock 38 | ArgumentList = $weatherJob, $systemInfoJob 39 | InitializationScript = $initializationScript 40 | InitializationArgumentList = $modulePath 41 | } 42 | 43 | Start-DTTitle @params 44 | -------------------------------------------------------------------------------- /DynamicTitle/Private/GlobalStore.ps1: -------------------------------------------------------------------------------- 1 | class GlobalStore 2 | { 3 | $originalTitle = $null 4 | $originalPrompt = $null 5 | $isPromptReplaced = $false 6 | $originalPSConsoleHostReadLine = $null 7 | $isReadLineReplaced = $false 8 | $isInLegacyApplicationMode = $false 9 | $titleUpdateThread = $null 10 | $backgroundThreadTimerJobs = @() 11 | $promptCallbacks = @() 12 | $commandPreExecutionCallbacks = @() 13 | 14 | [void] Clear() 15 | { 16 | if ($this.isInLegacyApplicationMode) 17 | { 18 | $this.ExitLegacyApplicationMode() 19 | } 20 | 21 | $this.ClearTitleUpdateThread() 22 | 23 | foreach ($timerJob in $this.backgroundThreadTimerJobs) 24 | { 25 | StopThread $timerJob.Thread 26 | } 27 | $this.backgroundThreadTimerJobs = @() 28 | 29 | $this.RestorePrompt() 30 | $this.RestorePSConsoleHostReadLine() 31 | } 32 | 33 | [void] ClearTitleUpdateThread() 34 | { 35 | if ($this.titleUpdateThread) 36 | { 37 | StopThread $this.titleUpdateThread 38 | $this.titleUpdateThread = $null 39 | } 40 | 41 | if ($null -ne $this.originalTitle) 42 | { 43 | (Get-Host).UI.RawUI.WindowTitle = $this.originalTitle 44 | $this.originalTitle = $null 45 | } 46 | } 47 | 48 | [void] EnterLegacyApplicationMode() 49 | { 50 | if ($this.isInLegacyApplicationMode) 51 | { 52 | Write-Error -Message 'Already in Legacy Application Mode.' -Category InvalidOperation 53 | return 54 | } 55 | (Get-Host).NotifyBeginApplication() 56 | $this.isInLegacyApplicationMode = $true 57 | } 58 | 59 | [void] ExitLegacyApplicationMode() 60 | { 61 | if (-not $this.isInLegacyApplicationMode) 62 | { 63 | Write-Error -Message 'Not in Legacy Application Mode.' -Category InvalidOperation 64 | return 65 | } 66 | (Get-Host).NotifyEndApplication() 67 | $this.isInLegacyApplicationMode = $false 68 | } 69 | 70 | [void] SetTitleUpdateThread($thread, [string]$originalTitle) 71 | { 72 | $this.titleUpdateThread = $thread 73 | $this.originalTitle = $originalTitle 74 | } 75 | 76 | [void] AddBackgroundThreadTimerJob($job) 77 | { 78 | $this.backgroundThreadTimerJobs += $job 79 | } 80 | 81 | [void] ReplacePrompt() 82 | { 83 | if ($this.isPromptReplaced) 84 | { 85 | return 86 | } 87 | 88 | $this.isPromptReplaced = $true 89 | $this.originalPrompt = $function:global:Prompt 90 | $function:global:Prompt = { 91 | $script:globalStore.InvokePrompt() 92 | } 93 | } 94 | 95 | [void] RestorePrompt() 96 | { 97 | if (-not $this.isPromptReplaced) 98 | { 99 | return 100 | } 101 | 102 | $function:global:Prompt = $this.originalPrompt 103 | $this.isPromptReplaced = $false 104 | 105 | $this.promptCallbacks = @() 106 | } 107 | 108 | [void] AddPromptCallback([ScriptBlock]$scriptBlock, $arguments) 109 | { 110 | $this.promptCallbacks += ,@($scriptBlock, $arguments) 111 | } 112 | 113 | [string] InvokePrompt() 114 | { 115 | foreach ($private:callback in $this.promptCallbacks) 116 | { 117 | $private:scriptBlock = $callback[0] 118 | $private:arguments = $callback[1] 119 | try 120 | { 121 | $scriptBlock.Invoke($arguments) 122 | } 123 | catch 124 | { 125 | $_ | Out-Default 126 | } 127 | } 128 | return $this.originalPrompt.Invoke() 129 | } 130 | 131 | [void] ReplacePSConsoleHostReadLine() 132 | { 133 | if ($this.isReadLineReplaced) 134 | { 135 | return 136 | } 137 | 138 | $this.isReadLineReplaced = $true 139 | $this.originalPSConsoleHostReadLine = $function:global:PSConsoleHostReadLine 140 | $function:global:PSConsoleHostReadLine = { 141 | $script:globalStore.InvokePSConsoleHostReadLine() 142 | } 143 | } 144 | 145 | [void] RestorePSConsoleHostReadLine() 146 | { 147 | if (-not $this.isReadLineReplaced) 148 | { 149 | return 150 | } 151 | 152 | $function:global:PSConsoleHostReadLine = $this.originalPSConsoleHostReadLine 153 | $this.isReadLineReplaced = $false 154 | 155 | $this.commandPreExecutionCallbacks = @() 156 | } 157 | 158 | [void] AddCommandPreExecutionCallback([ScriptBlock]$scriptBlock, $arguments) 159 | { 160 | $this.commandPreExecutionCallbacks += ,@($scriptBlock, $arguments) 161 | } 162 | 163 | [Object] InvokePSConsoleHostReadLine() 164 | { 165 | $private:command = $this.originalPSConsoleHostReadLine.Invoke() 166 | foreach ($private:callback in $this.commandPreExecutionCallbacks) 167 | { 168 | $private:scriptBlock = $callback[0] 169 | $private:arguments = $callback[1] 170 | $arguments.command = $command 171 | try 172 | { 173 | $scriptBlock.Invoke($arguments) 174 | } 175 | catch 176 | { 177 | $_ | Out-Default 178 | } 179 | } 180 | return $command 181 | } 182 | } 183 | 184 | $script:globalStore = [GlobalStore]::new() 185 | -------------------------------------------------------------------------------- /DynamicTitle/Private/Pacemaker.ps1: -------------------------------------------------------------------------------- 1 | class Pacemaker 2 | { 3 | $stopwatch = $null 4 | $intervalMilliseconds = 0 5 | 6 | Pacemaker($intervalMilliseconds) 7 | { 8 | $this.stopwatch = [System.Diagnostics.Stopwatch]::new() 9 | $this.intervalMilliseconds = $intervalMilliseconds 10 | } 11 | 12 | [void] Tick() 13 | { 14 | if ($this.stopwatch.IsRunning) 15 | { 16 | $this.stopwatch.Stop() 17 | $waitMilliseconds = $this.intervalMilliseconds - $this.stopwatch.ElapsedMilliseconds 18 | $this.stopwatch.Reset() 19 | if ($waitMilliseconds -gt 0) 20 | { 21 | Start-Sleep -Milliseconds $waitMilliseconds 22 | } 23 | } 24 | $this.stopwatch.Start() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DynamicTitle/Private/Thread.ps1: -------------------------------------------------------------------------------- 1 | function StartThread([ScriptBlock]$scriptBlock, $arguments) 2 | { 3 | $runspace = [RunSpaceFactory]::CreateRunspace() 4 | $runspace.Open() 5 | 6 | $powershell = [PowerShell]::Create() 7 | $powershell.Runspace = $runspace 8 | $powershell.AddScript($scriptBlock.ToString()) | Out-Null 9 | $powershell.AddArgument($arguments) | Out-Null 10 | 11 | $asyncHandle = $powershell.BeginInvoke() 12 | 13 | $thread = [PSCustomObject]@{ 14 | Runspace = $runspace 15 | Powershell = $powershell 16 | AsyncHandle = $asyncHandle 17 | } 18 | $thread 19 | } 20 | 21 | function StopThread($thread) 22 | { 23 | $thread.Runspace.Dispose() 24 | $thread.Powershell.Dispose() 25 | } 26 | -------------------------------------------------------------------------------- /DynamicTitle/Private/_BackgroundJobHelper.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot\Pacemaker.ps1 2 | 3 | $pacemaker = $null 4 | 5 | function Init($arguments) 6 | { 7 | $script:pacemaker = [Pacemaker]::new($arguments.intervalMilliseconds) 8 | } 9 | 10 | function Tick() 11 | { 12 | $script:pacemaker.Tick() 13 | $true 14 | } 15 | -------------------------------------------------------------------------------- /DynamicTitle/Private/_TitleUpdateHelper.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot\Pacemaker.ps1 2 | 3 | $arguments = $null 4 | $pacemaker = $null 5 | $lineIndex = 0 6 | $verticalScrollFrameCount = 0 7 | $horizontalScrollIndex = 0 8 | $horizontalScrollFrameCount = 0 9 | 10 | function Init($arguments) 11 | { 12 | $script:arguments = $arguments 13 | $script:pacemaker = [Pacemaker]::new($arguments.intervalMilliseconds) 14 | } 15 | 16 | function Tick() 17 | { 18 | $script:pacemaker.Tick() 19 | $true 20 | } 21 | 22 | function GetTitle($lines) 23 | { 24 | $script:lineIndex = $script:lineIndex % $lines.Count 25 | $line = $lines[$script:lineIndex] 26 | 27 | $title, $isHorizontalScrollEnd = HorizontalScroll $line 28 | 29 | VerticalScroll $isHorizontalScrollEnd 30 | 31 | $title 32 | } 33 | 34 | function HorizontalScroll($line) 35 | { 36 | $isScrollEnd = $false 37 | if ($script:arguments.horizontalScrollFrameWidth -gt 0) 38 | { 39 | $stringInfo = [System.Globalization.StringInfo]::new($line) 40 | if ($stringInfo.LengthInTextElements -gt $script:arguments.horizontalScrollFrameWidth) 41 | { 42 | $script:horizontalScrollFrameCount++ 43 | if ($script:horizontalScrollIndex -eq 0) 44 | { 45 | # start wait 46 | $wait = $script:arguments.horizontalScrollWaitFrame + $script:arguments.horizontalScrollFrame 47 | if ($script:horizontalScrollFrameCount -gt $wait) 48 | { 49 | $script:horizontalScrollFrameCount = 0 50 | $script:horizontalScrollIndex++ 51 | } 52 | } 53 | elseif (($stringInfo.LengthInTextElements - $script:horizontalScrollIndex) -gt $script:arguments.horizontalScrollFrameWidth) 54 | { 55 | # scrolling 56 | if ($script:horizontalScrollFrameCount -ge $script:arguments.horizontalScrollFrame) 57 | { 58 | $script:horizontalScrollFrameCount = 0 59 | $script:horizontalScrollIndex++ 60 | } 61 | } 62 | else 63 | { 64 | # end wait 65 | $script:horizontalScrollIndex = [Math]::Min($script:horizontalScrollIndex, $stringInfo.LengthInTextElements-1) 66 | $wait = $script:arguments.horizontalScrollWaitFrame + $script:arguments.horizontalScrollFrame 67 | if ($script:horizontalScrollFrameCount -ge $wait) 68 | { 69 | $script:horizontalScrollFrameCount = 0 70 | $isScrollEnd = $true 71 | } 72 | } 73 | 74 | $line = $stringInfo.SubstringByTextElements($script:horizontalScrollIndex, $script:arguments.horizontalScrollFrameWidth) 75 | 76 | if ($isScrollEnd) 77 | { 78 | $script:horizontalScrollIndex = 0 79 | } 80 | } 81 | else 82 | { 83 | # no need to scroll 84 | $script:horizontalScrollFrameCount = 0 85 | $script:horizontalScrollIndex = 0 86 | $isScrollEnd = $true 87 | } 88 | } 89 | else 90 | { 91 | $isScrollEnd = $true 92 | } 93 | 94 | $line, $isScrollEnd 95 | } 96 | 97 | function VerticalScroll($isHorizontalScrollEnd) 98 | { 99 | $script:verticalScrollFrameCount++ 100 | if ($isHorizontalScrollEnd -and ($script:verticalScrollFrameCount -ge $script:arguments.verticalScrollFrame)) 101 | { 102 | $script:verticalScrollFrameCount = 0 103 | $script:lineIndex++ 104 | } 105 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Enter-DTLegacyApplicationMode.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Calls $host.NotifyBeginApplication() so that the host doesn't reset the title when executing legacy command line applications. 4 | 5 | .DESCRIPTION 6 | Calls $host.NotifyBeginApplication() so that the host doesn't reset the title when executing legacy command line applications. 7 | 8 | When command line applications are executed on the main thread, the host saves the title string before calling the application and resets it when the application returns. This behavior sometimes causes a blink on the title as you are changing the title on a background thread. 9 | Enter-DTLegacyApplicationMode calls $host.NotifyBeginApplication() to notify the host that the subsequent commands are all legacy command line applications, and therefore the title reset behavior on every command call is suppressed. 10 | Note that this workaround stops all the state restore from the command line applications which may cause some other issues. 11 | 12 | .INPUTS 13 | None. 14 | 15 | .OUTPUTS 16 | None. 17 | 18 | .EXAMPLE 19 | Enter-DTLegacyApplicationMode 20 | Start-DTTitle {Get-Date} 21 | 22 | #> 23 | function Enter-DTLegacyApplicationMode 24 | { 25 | process 26 | { 27 | $script:globalStore.EnterLegacyApplicationMode() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DynamicTitle/Public/Exit-DTLegacyApplicationMode.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Calls $host.NotifyEndApplication(). 4 | 5 | .DESCRIPTION 6 | Calls $host.NotifyEndApplication(). See the help of Enter-DTLegacyApplicationMode. 7 | 8 | .INPUTS 9 | None. 10 | 11 | .OUTPUTS 12 | None. 13 | 14 | .EXAMPLE 15 | Exit-DTLegacyApplicationMode 16 | 17 | #> 18 | function Exit-DTLegacyApplicationMode 19 | { 20 | process 21 | { 22 | $script:globalStore.ExitLegacyApplicationMode() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DynamicTitle/Public/Get-DTExamplesPath.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Returns the path of the Examples directory. 4 | 5 | .DESCRIPTION 6 | Returns the path of the Examples directory. 7 | 8 | .INPUTS 9 | None. 10 | 11 | .OUTPUTS 12 | String. 13 | 14 | .EXAMPLE 15 | $examples = Get-DTExamplesPath 16 | Get-ChildItem -Path $examples 17 | 18 | #> 19 | function Get-DTExamplesPath 20 | { 21 | process 22 | { 23 | Join-Path (Split-Path $PSScriptRoot -Parent) 'Examples' 24 | } 25 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Get-DTJobLatestOutput.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Returns the latest output of a job in a thread-safe way. 4 | 5 | .DESCRIPTION 6 | Returns the latest output of a job in a thread-safe way. 7 | It returns immediately without waiting for the job output. It returns $null if the job has never returned an output. 8 | 9 | .PARAMETER InputObject 10 | Job object to get the output. 11 | 12 | .INPUTS 13 | PSCustomObject that represents a job object. 14 | 15 | .OUTPUTS 16 | Objects returned by the job's ScriptBlock. 17 | 18 | .EXAMPLE 19 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {Invoke-RestMethod https://wttr.in/?format="%c%t\n"} -IntervalMilliseconds 60000 20 | $weather = Get-DTJobLatestOutput $job 21 | 22 | #> 23 | function Get-DTJobLatestOutput 24 | { 25 | param 26 | ( 27 | [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] 28 | [PSCustomObject]$InputObject 29 | ) 30 | 31 | process 32 | { 33 | $InputObject.Sync.output 34 | } 35 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Start-DTExample.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Starts an example DynamicTitle script. 4 | 5 | .DESCRIPTION 6 | Starts an example DynamicTitle script. 7 | 8 | .PARAMETER Name 9 | Filename of the example script. The tab completion helps you pick one of the examples. 10 | 11 | .INPUTS 12 | None. 13 | 14 | .OUTPUTS 15 | None. 16 | 17 | .EXAMPLE 18 | Start-DTExample -Name GitStatus 19 | 20 | #> 21 | function Start-DTExample 22 | { 23 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 24 | param 25 | ( 26 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 27 | [ValidateScript({ 28 | $_ -in (Get-ChildItem (Get-DTExamplesPath)).BaseName 29 | })] 30 | [ArgumentCompleter({ 31 | $wordToComplete = $args[2] 32 | $names = (Get-ChildItem (Get-DTExamplesPath)).BaseName 33 | $names -like "$wordToComplete*" 34 | })] 35 | [String]$Name 36 | ) 37 | 38 | process 39 | { 40 | Stop-DTTitle 41 | $private:examplesPath = Get-DTExamplesPath 42 | $private:scriptFile = Join-Path $examplesPath "$Name.ps1" 43 | . $scriptFile 44 | } 45 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Start-DTJobBackgroundThreadTimer.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Starts a background thread and calls a function periodically on the thread. 4 | 5 | .DESCRIPTION 6 | Starts a background thread and calls a function periodically on the thread. 7 | The specified ScriptBlock is called periodically at the specified interval on a background thread. It can be used to get the information that takes time to process. 8 | 9 | .PARAMETER ScriptBlock 10 | ScriptBlock that is called periodically. 11 | 12 | .PARAMETER ArgumentList 13 | The arguments passed to the ScriptBlock. The ScriptBlock runs on another thread (Runspace) so variables on the main thread need to be passed as this ArgumentList. 14 | 15 | .PARAMETER InitializationScript 16 | ScriptBlock that is called at the start of the new thread. It runs in the global scope of the thread. 17 | 18 | .PARAMETER InitializationArgumentList 19 | The arguments passed to the InitializationScript. 20 | 21 | .PARAMETER IntervalMilliseconds 22 | The ScriptBlock is called at this interval milliseconds. 23 | 24 | .INPUTS 25 | None. 26 | 27 | .OUTPUTS 28 | PSCustomObject that represents a job object. 29 | 30 | .EXAMPLE 31 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {Invoke-RestMethod https://wttr.in/?format="%c%t\n"} -IntervalMilliseconds 60000 32 | $weather = Get-DTJobLatestOutput $job 33 | 34 | #> 35 | function Start-DTJobBackgroundThreadTimer 36 | { 37 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 38 | [OutputType([PSCustomObject])] 39 | param 40 | ( 41 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 42 | [ScriptBlock]$ScriptBlock, 43 | 44 | [Parameter(ValueFromPipelineByPropertyName=$true)] 45 | [Object[]]$ArgumentList, 46 | 47 | [Parameter(ValueFromPipelineByPropertyName=$true)] 48 | [ScriptBlock]$InitializationScript, 49 | 50 | [Parameter(ValueFromPipelineByPropertyName=$true)] 51 | [Object[]]$InitializationArgumentList, 52 | 53 | [Parameter(ValueFromPipelineByPropertyName=$true)] 54 | [Int]$IntervalMilliseconds = 500 55 | ) 56 | 57 | process 58 | { 59 | $sync = [System.Collections.Hashtable]::Synchronized(@{}) 60 | 61 | $arguments = @{ 62 | host = $host 63 | sync = $sync 64 | scriptBlock = $ScriptBlock.Ast.GetScriptBlock() 65 | argumentList = $ArgumentList 66 | intervalMilliseconds = $IntervalMilliseconds 67 | scriptRoot = $PSScriptRoot 68 | } 69 | 70 | if ($InitializationScript) 71 | { 72 | $arguments.initializationScript = $InitializationScript.Ast.GetScriptBlock() 73 | $arguments.initializationArgumentList = $InitializationArgumentList 74 | } 75 | 76 | $threadFunc = { 77 | $private:dynamicTitleBackgroundThreadTimerJob = New-Module -ScriptBlock { 78 | . $args 79 | } -ArgumentList (Join-Path $args.scriptRoot '..\Private\_BackgroundJobHelper.ps1') -AsCustomObject 80 | $private:dynamicTitleBackgroundThreadTimerJob.Init($args) 81 | 82 | if ($args.initializationScript) 83 | { 84 | $private:dynamicTitleErrorVariable = $null 85 | Invoke-Command $args.initializationScript -NoNewScope -ArgumentList $args.initializationArgumentList -ErrorVariable dynamicTitleErrorVariable 86 | if ($dynamicTitleErrorVariable) 87 | { 88 | $args.host.UI.WriteErrorLine($dynamicTitleErrorVariable) 89 | } 90 | } 91 | 92 | while ($dynamicTitleBackgroundThreadTimerJob.Tick()) 93 | { 94 | $private:dynamicTitleErrorVariable = $null 95 | $args.sync.output = Invoke-Command $args.scriptBlock -ArgumentList $args.argumentList -ErrorVariable dynamicTitleErrorVariable 96 | if ($dynamicTitleErrorVariable) 97 | { 98 | $args.host.UI.WriteErrorLine($dynamicTitleErrorVariable) 99 | } 100 | } 101 | } 102 | 103 | $thread = StartThread $threadFunc $arguments 104 | 105 | $job = [PSCustomObject]@{ 106 | Sync = $sync 107 | Thread = $thread 108 | } 109 | $script:globalStore.AddBackgroundThreadTimerJob($job) 110 | 111 | $job 112 | } 113 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Start-DTJobCommandPreExecutionCallback.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Registers a callback function that is called before the commands entered on the console are executed. 4 | 5 | .DESCRIPTION 6 | Registers a callback function that is called before the commands entered on the console are executed. 7 | The callback runs on the main thread. It can be used to get the command string or the command start time. 8 | This function overwrites PSConsoleHostReadLine so if you define your original PSConsoleHostReadLine, this function needs to be called after those definitions. 9 | 10 | .PARAMETER ScriptBlock 11 | ScriptBlock that is called before the command entered on the console is executed. The command string is passed as $args[0]. 12 | 13 | .PARAMETER ArgumentList 14 | The arguments passed to the ScriptBlock. The arguments are stored from $args[1]. 15 | 16 | .INPUTS 17 | None. 18 | 19 | .OUTPUTS 20 | PSCustomObject that represents a Job object. 21 | 22 | .EXAMPLE 23 | $job = Start-DTJobCommandPreExecutionCallback -ScriptBlock {$args[0]} 24 | $commandString = Get-DTJobLatestOutput $job 25 | 26 | #> 27 | function Start-DTJobCommandPreExecutionCallback 28 | { 29 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 30 | [OutputType([PSCustomObject])] 31 | param 32 | ( 33 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 34 | [ScriptBlock]$ScriptBlock, 35 | 36 | [Parameter(ValueFromPipelineByPropertyName=$true)] 37 | [Object[]]$ArgumentList 38 | ) 39 | 40 | process 41 | { 42 | $sync = [System.Collections.Hashtable]::Synchronized(@{}) 43 | 44 | $arguments = @{ 45 | sync = $sync 46 | scriptBlock = $ScriptBlock 47 | argumentList = $ArgumentList 48 | } 49 | 50 | $callback = { 51 | if ($args.argumentList) 52 | { 53 | $args.sync.output = $args.scriptBlock.Invoke(@($args.command) + @($args.argumentList)) 54 | } 55 | else 56 | { 57 | $args.sync.output = $args.scriptBlock.Invoke($args.command) 58 | } 59 | } 60 | 61 | $script:globalStore.ReplacePSConsoleHostReadLine() 62 | $script:globalStore.AddCommandPreExecutionCallback($callback, $arguments) 63 | 64 | $job = [PSCustomObject]@{ 65 | Sync = $sync 66 | } 67 | $job 68 | } 69 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Start-DTJobPromptCallback.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Registers a callback function that is called before the Prompt function. 4 | 5 | .DESCRIPTION 6 | Registers a callback function that is called before the Prompt function. The callback runs on the main thread so it can be used to get the current directly for example. 7 | If you define your Prompt function, this function needs to be called after those definitions because the callback is achieved by overwriting the Prompt function. 8 | 9 | .PARAMETER ScriptBlock 10 | ScriptBlock that is called before the Prompt function. 11 | 12 | .PARAMETER ArgumentList 13 | The arguments passed to the ScriptBlock. 14 | 15 | .INPUTS 16 | None. 17 | 18 | .OUTPUTS 19 | PSCustomObject that represents a Job object. 20 | 21 | .EXAMPLE 22 | $job = Start-DTJobPromptCallback -ScriptBlock {Get-Location} 23 | $currentDirectory = Get-DTJobLatestOutput $job 24 | 25 | #> 26 | function Start-DTJobPromptCallback 27 | { 28 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 29 | [OutputType([PSCustomObject])] 30 | param 31 | ( 32 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 33 | [ScriptBlock]$ScriptBlock, 34 | 35 | [Parameter(ValueFromPipelineByPropertyName=$true)] 36 | [Object[]]$ArgumentList 37 | ) 38 | 39 | process 40 | { 41 | $sync = [System.Collections.Hashtable]::Synchronized(@{}) 42 | 43 | $arguments = @{ 44 | sync = $sync 45 | scriptBlock = $ScriptBlock 46 | argumentList = $ArgumentList 47 | } 48 | 49 | $callback = { 50 | $args.sync.output = $args.scriptBlock.Invoke($args.argumentList) 51 | } 52 | 53 | $script:globalStore.ReplacePrompt() 54 | $script:globalStore.AddPromptCallback($callback, $arguments) 55 | 56 | $job = [PSCustomObject]@{ 57 | Sync = $sync 58 | } 59 | $job 60 | } 61 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Start-DTTitle.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Starts a thread to set the console title. 4 | 5 | .DESCRIPTION 6 | Starts a thread to set the console title. The specified ScriptBlock is called periodically at the specified interval on a background thread. 7 | The string returned by the ScriptBlock is set as the console title. 8 | 9 | .PARAMETER ScriptBlock 10 | ScriptBlock that is called periodically. It should return a string or an array of strings. If an array is returned, the strings are shown in order with a vertical scroll. 11 | 12 | .PARAMETER ArgumentList 13 | The arguments passed to the ScriptBlock. The ScriptBlock runs on another thread (Runspace) so variables on the main thread need to be passed as this ArgumentList. 14 | 15 | .PARAMETER InitializationScript 16 | ScriptBlock that is called at the start of the new thread. It runs in the global scope of the thread. 17 | 18 | .PARAMETER InitializationArgumentList 19 | The arguments passed to the InitializationScript. 20 | 21 | .PARAMETER UpdateIntervalMilliseconds 22 | The ScriptBlock is called at this interval milliseconds. 23 | 24 | .PARAMETER VerticalScrollIntervalMilliseconds 25 | If an array of strings is returned from the ScriptBlock, each element is set as the title in order at this interval milliseconds. 26 | 27 | .PARAMETER HorizontalScrollFrameWidth 28 | The title scrolls horizontally if the number of characters is higher than this number. 29 | 30 | .PARAMETER HorizontalScrollIntervalMilliseconds 31 | The title scrolls 1 character at this interval milliseconds. 32 | 33 | .PARAMETER HorizontalScrollWaitMilliseconds 34 | The horizontal scroll stops for this milliseconds at the start and the end of the scroll. 35 | 36 | .INPUTS 37 | None. 38 | 39 | .OUTPUTS 40 | None. 41 | 42 | .EXAMPLE 43 | Start-DTTitle -ScriptBlock {Get-Date} -UpdateIntervalMilliseconds 1000 44 | 45 | #> 46 | function Start-DTTitle 47 | { 48 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 49 | param 50 | ( 51 | [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] 52 | [ScriptBlock]$ScriptBlock, 53 | 54 | [Parameter(ValueFromPipelineByPropertyName=$true)] 55 | [Object[]]$ArgumentList, 56 | 57 | [Parameter(ValueFromPipelineByPropertyName=$true)] 58 | [ScriptBlock]$InitializationScript, 59 | 60 | [Parameter(ValueFromPipelineByPropertyName=$true)] 61 | [Object[]]$InitializationArgumentList, 62 | 63 | [Parameter(ValueFromPipelineByPropertyName=$true)] 64 | [Int]$UpdateIntervalMilliseconds = 200, 65 | 66 | [Parameter(ValueFromPipelineByPropertyName=$true)] 67 | [Int]$VerticalScrollIntervalMilliseconds = 5000, 68 | 69 | [Parameter(ValueFromPipelineByPropertyName=$true)] 70 | [Int]$HorizontalScrollFrameWidth = 0, 71 | 72 | [Parameter(ValueFromPipelineByPropertyName=$true)] 73 | [Int]$HorizontalScrollIntervalMilliseconds = 400, 74 | 75 | [Parameter(ValueFromPipelineByPropertyName=$true)] 76 | [Int]$HorizontalScrollWaitMilliseconds = 2000 77 | ) 78 | 79 | process 80 | { 81 | $script:globalStore.ClearTitleUpdateThread() 82 | 83 | $verticalScrollFrame = [Math]::Max([Int]($VerticalScrollIntervalMilliseconds / $UpdateIntervalMilliseconds), 1) 84 | $horizontalScrollFrame = [Math]::Max([Int]($HorizontalScrollIntervalMilliseconds / $UpdateIntervalMilliseconds), 1) 85 | $horizontalScrollWaitFrame = [Int]($HorizontalScrollWaitMilliseconds / $UpdateIntervalMilliseconds) 86 | 87 | $arguments = @{ 88 | host = $host 89 | scriptBlock = $ScriptBlock.Ast.GetScriptBlock() 90 | argumentList = $ArgumentList 91 | intervalMilliseconds = $UpdateIntervalMilliseconds 92 | scriptRoot = $PSScriptRoot 93 | verticalScrollFrame = $verticalScrollFrame 94 | horizontalScrollFrameWidth = $HorizontalScrollFrameWidth 95 | horizontalScrollFrame = $horizontalScrollFrame 96 | horizontalScrollWaitFrame = $horizontalScrollWaitFrame 97 | } 98 | 99 | if ($InitializationScript) 100 | { 101 | $arguments.initializationScript = $InitializationScript.Ast.GetScriptBlock() 102 | $arguments.initializationArgumentList = $InitializationArgumentList 103 | } 104 | 105 | $threadFunc = { 106 | $private:dynamicTitleUpdateMain = New-Module -ScriptBlock { 107 | . $args 108 | } -ArgumentList (Join-Path $args.scriptRoot '..\Private\_TitleUpdateHelper.ps1') -AsCustomObject 109 | $private:dynamicTitleUpdateMain.Init($args) 110 | 111 | if ($args.initializationScript) 112 | { 113 | $private:dynamicTitleErrorVariable = $null 114 | Invoke-Command $args.initializationScript -NoNewScope -ArgumentList $args.initializationArgumentList -ErrorVariable dynamicTitleErrorVariable 115 | if ($dynamicTitleErrorVariable) 116 | { 117 | $args.host.UI.WriteErrorLine($dynamicTitleErrorVariable) 118 | } 119 | } 120 | 121 | while ($dynamicTitleUpdateMain.Tick()) 122 | { 123 | $private:dynamicTitleErrorVariable = $null 124 | $private:titleLines = [string[]]@(Invoke-Command $args.scriptBlock -ArgumentList $args.argumentList -ErrorVariable dynamicTitleErrorVariable) 125 | $args.host.UI.RawUI.WindowTitle = $dynamicTitleUpdateMain.GetTitle($titleLines) 126 | if ($dynamicTitleErrorVariable) 127 | { 128 | $args.host.UI.WriteErrorLine($dynamicTitleErrorVariable) 129 | } 130 | } 131 | } 132 | 133 | $originalTitle = $host.UI.RawUI.WindowTitle 134 | $thread = StartThread $threadFunc $arguments 135 | $script:globalStore.SetTitleUpdateThread($thread, $originalTitle) 136 | } 137 | } -------------------------------------------------------------------------------- /DynamicTitle/Public/Stop-DTTitle.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Stops the DynamicTitle thread and restores the original title. 4 | 5 | .DESCRIPTION 6 | Stops the DynamicTitle thread and restores the original title. All the jobs are also stopped. 7 | 8 | .INPUTS 9 | None. 10 | 11 | .OUTPUTS 12 | None. 13 | 14 | .EXAMPLE 15 | Stop-DTTitle 16 | 17 | #> 18 | function Stop-DTTitle 19 | { 20 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 21 | param() 22 | 23 | process 24 | { 25 | $script:globalStore.Clear() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mdgrs-mei 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 |
2 | 3 | # DynamicTitle 4 | 5 | [![GitHub license](https://img.shields.io/github/license/mdgrs-mei/DynamicTitle)](https://github.com/mdgrs-mei/DynamicTitle/blob/main/LICENSE) 6 | [![PowerShell Gallery](https://img.shields.io/powershellgallery/p/DynamicTitle)](https://www.powershellgallery.com/packages/DynamicTitle) 7 | [![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/DynamicTitle)](https://www.powershellgallery.com/packages/DynamicTitle) 8 | 9 | [![Pester Test](https://github.com/mdgrs-mei/DynamicTitle/actions/workflows/pester-test.yml/badge.svg)](https://github.com/mdgrs-mei/DynamicTitle/actions/workflows/pester-test.yml) 10 | 11 | [![Hashnode](https://img.shields.io/badge/Hashnode-2962FF?style=for-the-badge&logo=hashnode&logoColor=white)](https://mdgrs.hashnode.dev/building-your-own-terminal-status-bar-in-powershell) 12 | 13 | *DynamicTitle* is a PowerShell module for advanced console title customizations. 14 | 15 | ![DynamicTitle](https://github.com/mdgrs-mei/DynamicTitle/assets/81177095/e606e65b-6a42-4e0c-987a-4df3e2f412f3) 16 | 17 |
18 | 19 | The module provides you with the ability to set the console title from a background thread. Unlike the prompt string, it can show information without blocking or being blocked by the main thread. 20 | 21 | ## Requirements 22 | 23 | This module has been tested on: 24 | 25 | - Windows 10, Windows 11, Ubuntu 20.04 and macOS 26 | - Windows PowerShell 5.1 and PowerShell 7.3 27 | 28 | ## Installation 29 | 30 | *DynamicTitle* is available on the PowerShell Gallery. You can install the module with the following command: 31 | 32 | ```powershell 33 | Install-Module -Name DynamicTitle -Scope CurrentUser 34 | ``` 35 | 36 | ## Basic Usage 37 | 38 | This one-liner shows a live-updating clock on the title bar. The specified ScriptBlock is called periodically at a certain interval on a background thread. The module sets the console title to the string that is returned by the ScriptBlock. 39 | 40 | ```powershell 41 | Start-DTTitle {Get-Date} 42 | ``` 43 | ![LiveClock](https://github.com/mdgrs-mei/DynamicTitle/assets/81177095/048aa512-a654-40e9-8187-2d2018eff9b9) 44 | 45 | If an array is returned from the script block, they are shown with a vertical scrolling. 46 | 47 | ```powershell 48 | Start-DTTitle {'🌷 Hello', '🌼 World'} 49 | ``` 50 | 51 | ![VerticalScroll](https://github.com/mdgrs-mei/DynamicTitle/assets/81177095/d48edd3a-5063-48e4-a231-5a1d80ea5489) 52 | 53 | If the title width is fixed by your terminal app or you want to limit the width, you can specify `HorizontalScrollFrameWidth` parameter. The title text Horizontally scrolls when its length is longer than the parameter. 54 | 55 | ```powershell 56 | Start-DTTitle { 57 | '🍷 Showing a long text as a title 🍸' 58 | } -HorizontalScrollFrameWidth 25 59 | ``` 60 | 61 | ![HorizontalScroll](https://github.com/mdgrs-mei/DynamicTitle/assets/81177095/375bf799-6db3-4fd6-a2a0-4313335a9fe7) 62 | 63 | ## Jobs 64 | 65 | Although the ScriptBlock specified to `Start-DTTitle` runs on another thread to avoid blocking, sometimes you need to get information from the main thread such as Current Directory, or you might need another thread to get some information that takes long time to process. For this purpose, the module provides you with three types of job objects. In either case, you can get the job output in a thread-safe way like this: 66 | 67 | ```powershell 68 | $output = Get-DTJobLatestOutput $job 69 | ``` 70 | 71 | ### CommandPreExecutionCallback Job 72 | 73 | With this job, you can register a ScriptBlock that is called right before the command entered on the console is executed. The command string is passed as `$args[0]`. This job is useful to get the command running on the main thread or the command start time. 74 | 75 | ```powershell 76 | $job = Start-DTJobCommandPreExecutionCallback -ScriptBlock { 77 | param($command) 78 | $command, (Get-Date) 79 | } 80 | 81 | Start-DTTitle { 82 | param($job) 83 | $commandString, $commandStartDate = Get-DTJobLatestOutput $job 84 | # ... 85 | } -ArgumentList $job 86 | ``` 87 | 88 | ### PromptCallback Job 89 | 90 | PromptCallback job registers a ScriptBlock that is called right before the Prompt function is called. This job can be used to get the current directory for example. 91 | 92 | ```powershell 93 | $job = Start-DTJobPromptCallback -ScriptBlock {Get-Location} 94 | 95 | Start-DTTitle { 96 | param($job) 97 | $currentDirectory = Get-DTJobLatestOutput $job 98 | # ... 99 | } -ArgumentList $job 100 | ``` 101 | 102 | ### BackgroundThreadTimer Job 103 | 104 | BackgroundTimerJob starts a new thread and calls a ScriptBlock periodically at the specified interval on the thread. It is good for tasks that take long time to finish so as not to block the title update thread. 105 | 106 | ```powershell 107 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock { 108 | # Get weather 109 | Invoke-RestMethod https://wttr.in/?format="%c%t" 110 | } -IntervalMilliseconds 60000 111 | 112 | Start-DTTitle { 113 | param($job) 114 | $weather = Get-DTJobLatestOutput $job 115 | # ... 116 | } -ArgumentList $job 117 | ``` 118 | 119 | ## Legacy Application Mode 120 | 121 | When command line applications are executed on the main thread, the host saves the title string before calling the application and resets it when the application returns. This behavior sometimes causes a blink on the title as you are changing the title on a background thread. 122 | 123 | `Enter-DTLegacyApplicationMode` calls `$host.NotifyBeginApplication()` to notify the host that the subsequent commands are all legacy command line applications, and therefore the title reset behavior on every command call is suppressed. Note that this workaround stops all the state restore from the command line applications which may cause some other issues. 124 | 125 | ```powershell 126 | Start-DTTitle {Get-Date} 127 | Enter-DTLegacyApplicationMode 128 | # Calling command line applications here does not cause a blink on the title. 129 | git status -s 130 | # ... 131 | Exit-DTLegacyApplicationMode 132 | # This call causes a blink. 133 | git status -s 134 | ``` 135 | 136 | ## Terminal Settings Recommendations 137 | 138 | - If you are using Windows Terminal, there is a setting called `Tab width mode`. Setting it to `Title length` or `Compact` should be better for longer titles. 139 | 140 | - [Hyper](https://github.com/vercel/hyper) terminal allows you to change the appearance of the title by css, such as font and size. It's a good fit for this module if you want the title to stand out or want to use special emojis. 141 | 142 | ## Examples 143 | 144 | Examples are included in the module. You can play an example DynamicTitle with `Start-DTExample` function. The tab completion for the `Name` parameter helps you find available examples. 145 | 146 | ```powershell 147 | Start-DTExample -Name CommandExecutionTime 148 | ``` 149 | 150 | `Get-DTExamplesPath` returns the path where the example scripts are stored. 151 | 152 | ```powershell 153 | PS D:\> Get-ChildItem (Get-DTExamplesPath) 154 | 155 | Mode LastWriteTime Length Name 156 | ---- ------------- ------ ---- 157 | -a--- 3/26/2023 1:56 PM 4095 AllInOne.ps1 158 | -a--- 3/26/2023 1:56 PM 1810 CommandExecutionTime.ps1 159 | -a--- 3/26/2023 1:56 PM 1956 GitStatus.ps1 160 | -a--- 3/26/2023 1:56 PM 1512 StatusBar.ps1 161 | ``` 162 | 163 | ## Get-Help 164 | 165 | `Get-Command` can list all the available functions in the module: 166 | 167 | ```powershell 168 | Get-Command -Module DynamicTitle 169 | ``` 170 | 171 | To get the detailed help of a function, try: 172 | 173 | ```powershell 174 | Get-Help Start-DTTitle -Full 175 | ``` 176 | 177 | ## Changelog 178 | 179 | Changelog is available [here](https://github.com/mdgrs-mei/DynamicTitle/blob/main/CHANGELOG.md). 180 | -------------------------------------------------------------------------------- /Tests/DynamicTitle.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules PSScriptAnalyzer 2 | 3 | BeforeAll { 4 | $moduleDir = "$PSScriptRoot\..\DynamicTitle\" 5 | } 6 | 7 | Describe 'DynamicTitle' { 8 | It 'shows no warnings and errors of PSScriptAnalyzer' { 9 | $result = Invoke-ScriptAnalyzer -Path $moduleDir -Recurse 10 | $result | Out-String | Write-Host 11 | $result.Count | Should -Be 0 12 | } 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /Tests/Public/Enter-DTLegacyApplicationMode.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Enter-DTLegacyApplicationMode' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should not throw an error' { 7 | Enter-DTLegacyApplicationMode 8 | } 9 | 10 | AfterEach { 11 | Remove-Module DynamicTitle -Force 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Public/Exit-DTLegacyApplicationMode.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Exit-DTLegacyApplicationMode' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should not throw an error' { 7 | Enter-DTLegacyApplicationMode 8 | Exit-DTLegacyApplicationMode 9 | } 10 | 11 | AfterEach { 12 | Remove-Module DynamicTitle -Force 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Public/Get-DTJobLatestOutput.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Get-DTJobLatestOutput' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should return a job output' { 7 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {'hello'} -IntervalMilliseconds 1000 8 | Start-Sleep -Milliseconds 500 9 | $job | Get-DTJobLatestOutput | Should -Be 'hello' 10 | } 11 | 12 | AfterEach { 13 | Remove-Module DynamicTitle -Force 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Public/Start-DTExample.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Start-DTExample' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should not throw an error' { 7 | Start-DTExample -Name CommandExecutionTime 8 | } 9 | 10 | AfterEach { 11 | Remove-Module DynamicTitle -Force 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Public/Start-DTJobBackgroundThreadTimer.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Start-DTJobBackgroundThreadTimer' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should run a script' { 7 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {'hello'} 8 | Start-Sleep -Milliseconds 100 9 | $job | Get-DTJobLatestOutput | Should -Be 'hello' 10 | } 11 | 12 | It 'should pass an ArgumentList' { 13 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {$args[0] + $args[1]} -ArgumentList 1, 2 14 | Start-Sleep -Milliseconds 500 15 | $job | Get-DTJobLatestOutput | Should -Be 3 16 | } 17 | 18 | It 'should run an InitializationScript' { 19 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {$var} -InitializationScript {$var = 5} 20 | Start-Sleep -Milliseconds 500 21 | $job | Get-DTJobLatestOutput | Should -Be 5 22 | } 23 | 24 | It 'should pass an InitializationArgumentList' { 25 | $argumentList = 'hello, hello' 26 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {$var} -InitializationScript {$var = $args[0]} -InitializationArgumentList $argumentList 27 | Start-Sleep -Milliseconds 500 28 | $job | Get-DTJobLatestOutput | Should -Be $argumentList 29 | } 30 | 31 | It 'should reflect IntarvalMilliseconds' { 32 | $job = Start-DTJobBackgroundThreadTimer -ScriptBlock {$script:count++;$script:count} -InitializationScript {$count = 0} -IntervalMilliseconds 300 33 | Start-Sleep -Milliseconds 400 34 | $job | Get-DTJobLatestOutput | Should -Be 2 35 | } 36 | 37 | AfterEach { 38 | Remove-Module DynamicTitle -Force 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/Public/Start-DTJobCommandPreExecutionCallback.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Start-DTJobCommandPreExecutionCallback' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should return a job object' { 7 | $job = Start-DTJobCommandPreExecutionCallback -ScriptBlock {'hello'} -ArgumentList 1 8 | $job.Sync | Should -Not -Be $null 9 | } 10 | 11 | AfterEach { 12 | Remove-Module DynamicTitle -Force 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Public/Start-DTJobPromptCallback.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Start-DTJobPromptCallback' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should return a job object' { 7 | $job = Start-DTJobPromptCallback -ScriptBlock {'hello'} -ArgumentList 1 8 | $job.Sync | Should -Not -Be $null 9 | } 10 | 11 | AfterEach { 12 | Remove-Module DynamicTitle -Force 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Public/Start-DTTitle.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Start-DTTitle' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should set a title' { 7 | Start-DTTitle {'hello'} 8 | Start-Sleep -Milliseconds 500 9 | $host.UI.RawUI.WindowTitle | Should -Be 'hello' 10 | } 11 | 12 | It 'should pass an ArgumentList' { 13 | $title1 = 'hello ' 14 | $title2 = 'world' 15 | Start-DTTitle -ScriptBlock {$args[0] + $args[1]} -ArgumentList $title1, $title2 16 | Start-Sleep -Milliseconds 500 17 | $host.UI.RawUI.WindowTitle | Should -Be ($title1 + $title2) 18 | } 19 | 20 | It 'should run an InitializationScript' { 21 | Start-DTTitle -ScriptBlock {$title} -InitializationScript {$title = 'hi, hi'} 22 | Start-Sleep -Milliseconds 500 23 | $host.UI.RawUI.WindowTitle | Should -Be 'hi, hi' 24 | } 25 | 26 | It 'should pass an InitializationArgumentList' { 27 | $title = 'hello, hello' 28 | Start-DTTitle -ScriptBlock {$title} -InitializationScript {$title = $args[0]} -InitializationArgumentList $title 29 | Start-Sleep -Milliseconds 500 30 | $host.UI.RawUI.WindowTitle | Should -Be $title 31 | } 32 | 33 | It 'should reflect UpdateIntervalMilliseconds' { 34 | Start-DTTitle -ScriptBlock {$script:count++;$script:count} -InitializationScript {$count = 0} -UpdateIntervalMilliseconds 1000 35 | Start-Sleep -Milliseconds 500 36 | [Int]($host.UI.RawUI.WindowTitle) | Should -Be 1 37 | } 38 | 39 | It 'should reflect VerticalScrollIntervalMilliseconds' { 40 | Start-DTTitle -ScriptBlock {'line1', 'line2'} -VerticalScrollIntervalMilliseconds 400 41 | Start-Sleep -Milliseconds 500 42 | $host.UI.RawUI.WindowTitle | Should -Be 'line2' 43 | } 44 | 45 | It 'should reflect HorizontalScrollFrameWidth' { 46 | Start-DTTitle -ScriptBlock {'123456789'} -HorizontalScrollFrameWidth 5 47 | Start-Sleep -Milliseconds 200 48 | $host.UI.RawUI.WindowTitle.Length | Should -Be 5 49 | } 50 | 51 | It 'should reflect HorizontalScroll interval and wait' { 52 | Start-DTTitle -ScriptBlock {'123456789'} -HorizontalScrollFrameWidth 5 -HorizontalScrollIntervalMilliseconds 400 -HorizontalScrollWaitMilliseconds 0 53 | Start-Sleep -Milliseconds 500 54 | $host.UI.RawUI.WindowTitle[0] | Should -Be '2' 55 | } 56 | 57 | AfterEach { 58 | Remove-Module DynamicTitle -Force 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/Public/Stop-DTTitle.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Stop-DTTitle' { 2 | BeforeEach { 3 | Import-Module $PSScriptRoot\..\..\DynamicTitle -Force 4 | } 5 | 6 | It 'should reset the title' { 7 | $originalTitle = $host.UI.RawUI.WindowTitle 8 | Start-DTTitle {'hello'} 9 | Start-Sleep -Milliseconds 500 10 | Stop-DTTitle 11 | $host.UI.RawUI.WindowTitle | Should -Be $originalTitle 12 | } 13 | 14 | AfterEach { 15 | Remove-Module DynamicTitle -Force 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/RunTests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.0.0'} 2 | 3 | $config = New-PesterConfiguration 4 | $config.Run.PassThru = $true 5 | $config.Run.Path = $PSScriptRoot 6 | $config.CodeCoverage.Enabled = $true 7 | $config.CodeCoverage.Path = "$PSScriptRoot\..\DynamicTitle\Public" 8 | 9 | Invoke-Pester -Configuration $config 10 | --------------------------------------------------------------------------------