From 9c3de053abc5bf62581b4bdeff8a1d1d7c9aeb5f Mon Sep 17 00:00:00 2001 From: phireforge Date: Sat, 4 Jul 2026 08:25:08 -0400 Subject: [PATCH] Adding in paletteShell sample scripts --- .github/workflows/build-index.yml | 30 +--- .github/workflows/validate-pr.yml | 15 +- CONTRIBUTING.md | 4 +- scripts/paletteshell/Base64-Decode.ps1 | 32 +++++ scripts/paletteshell/Base64-Encode.ps1 | 26 ++++ scripts/paletteshell/Clear-TempFiles.ps1 | 44 ++++++ .../Clipboard-RemoveDuplicateLines.ps1 | 28 ++++ scripts/paletteshell/Clipboard-SortLines.ps1 | 27 ++++ scripts/paletteshell/Clipboard-ToCSV.ps1 | 21 +++ .../paletteshell/Clipboard-ToLowerCase.ps1 | 25 ++++ .../paletteshell/Clipboard-ToUpperCase.ps1 | 25 ++++ scripts/paletteshell/Clipboard-TrimLines.ps1 | 27 ++++ .../paletteshell/Clipboard-UnixTimestamp.ps1 | 19 +++ scripts/paletteshell/Clipboard-UrlDecode.ps1 | 27 ++++ scripts/paletteshell/Clipboard-UrlEncode.ps1 | 25 ++++ scripts/paletteshell/Export-ProcessList.ps1 | 27 ++++ scripts/paletteshell/Generate-GUID.ps1 | 18 +++ scripts/paletteshell/Get-PublicIP.ps1 | 19 +++ scripts/paletteshell/Git-Branches.ps1 | 98 ++++++++++++++ scripts/paletteshell/Json-Format.ps1 | 32 +++++ scripts/paletteshell/Json-Minify.ps1 | 32 +++++ scripts/paletteshell/README.md | 24 +++- scripts/paletteshell/Restart-Explorer.ps1 | 27 ++++ scripts/paletteshell/System-Report.ps1 | 32 +++++ scripts/paletteshell/Text-Transform.ps1 | 128 ++++++++++++++++++ tools/Test-Contribution.ps1 | 23 +++- 26 files changed, 790 insertions(+), 45 deletions(-) create mode 100644 scripts/paletteshell/Base64-Decode.ps1 create mode 100644 scripts/paletteshell/Base64-Encode.ps1 create mode 100644 scripts/paletteshell/Clear-TempFiles.ps1 create mode 100644 scripts/paletteshell/Clipboard-RemoveDuplicateLines.ps1 create mode 100644 scripts/paletteshell/Clipboard-SortLines.ps1 create mode 100644 scripts/paletteshell/Clipboard-ToCSV.ps1 create mode 100644 scripts/paletteshell/Clipboard-ToLowerCase.ps1 create mode 100644 scripts/paletteshell/Clipboard-ToUpperCase.ps1 create mode 100644 scripts/paletteshell/Clipboard-TrimLines.ps1 create mode 100644 scripts/paletteshell/Clipboard-UnixTimestamp.ps1 create mode 100644 scripts/paletteshell/Clipboard-UrlDecode.ps1 create mode 100644 scripts/paletteshell/Clipboard-UrlEncode.ps1 create mode 100644 scripts/paletteshell/Export-ProcessList.ps1 create mode 100644 scripts/paletteshell/Generate-GUID.ps1 create mode 100644 scripts/paletteshell/Get-PublicIP.ps1 create mode 100644 scripts/paletteshell/Git-Branches.ps1 create mode 100644 scripts/paletteshell/Json-Format.ps1 create mode 100644 scripts/paletteshell/Json-Minify.ps1 create mode 100644 scripts/paletteshell/Restart-Explorer.ps1 create mode 100644 scripts/paletteshell/System-Report.ps1 create mode 100644 scripts/paletteshell/Text-Transform.ps1 diff --git a/.github/workflows/build-index.yml b/.github/workflows/build-index.yml index abd1ca0..3e4c4d6 100644 --- a/.github/workflows/build-index.yml +++ b/.github/workflows/build-index.yml @@ -1,30 +1,15 @@ name: Build script catalog -# Keeps index.json - the catalog PaletteShell's in-palette "Browse community scripts" page -# fetches - in sync with whatever is actually under scripts/. Contributors never touch -# index.json themselves; it's generated by tools/Build-Index.ps1 and committed back by this -# workflow after a merge to main. Pull requests only get a dry-run build, so a script that -# fails to parse is caught before merge instead of silently breaking the catalog. +# Manually regenerates index.json - the catalog PaletteShell's in-palette "Browse community +# scripts" page fetches - from whatever is actually under scripts/. Contributors never touch +# index.json themselves; run this workflow when the committed catalog should be refreshed. on: - push: - branches: [main] - paths: - - "scripts/**" - - "tools/Build-Index.ps1" - - ".github/workflows/build-index.yml" - pull_request: - paths: - - "scripts/**" - - "tools/Build-Index.ps1" workflow_dispatch: {} permissions: contents: write -# index.json itself is excluded from the path filters above, so the bot's own commit doesn't -# retrigger this workflow - no [skip ci] dance needed. This concurrency group just protects -# against overlapping runs from rapid consecutive pushes. concurrency: group: build-index-${{ github.ref }} cancel-in-progress: true @@ -41,11 +26,8 @@ jobs: with: fetch-depth: 0 - # Manual runs are restricted to codeowners (.github/CODEOWNERS) - push-to-main and PR - # validation builds are untouched by this and need no such check (push already requires - # write access; PR builds are a read-only parse check open to any contributor). + # Manual runs are restricted to codeowners (.github/CODEOWNERS). - name: Restrict manual runs to codeowners - if: github.event_name == 'workflow_dispatch' env: ACTOR: ${{ github.actor }} run: | @@ -68,10 +50,8 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - # Only push on a direct push to main with real write permissions - pull_request runs - # (including ones from forks, which get a read-only token) are build-only validation. - name: Commit index.json - if: github.event_name != 'pull_request' && steps.diff.outputs.changed == 'true' + if: steps.diff.outputs.changed == 'true' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 685ee58..05dcd20 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -30,17 +30,4 @@ jobs: - name: Validate contribution checklist shell: pwsh run: ./tools/Test-Contribution.ps1 - - - name: Build catalog - shell: pwsh - run: ./tools/Build-Index.ps1 -OutputPath ([System.IO.Path]::GetTempFileName()) - - - name: Check generated catalog was not edited - if: github.event_name == 'pull_request' - shell: pwsh - run: | - $changedIndex = git diff --name-only '${{ github.event.pull_request.base.sha }}' '${{ github.event.pull_request.head.sha }}' -- index.json - if ($changedIndex) { - Write-Error 'index.json is generated after merge. Remove it from this PR and let the build-index workflow update it on main.' - exit 1 - } + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fbc907..f81778d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Set-ClipboardText $guid | `[ScriptVersion('1.0.0')]` | Script version (SemVer recommended) β€” lets tools detect when a newer copy is available | | `[ScriptIcon('πŸ†”')]` | Emoji or glyph icon | | `[ScriptTimeout(5000)]` | Timeout in milliseconds | -| `[ScriptOutput('None'\|'Toast'\|'Clipboard'\|'Markdown'\|'File'\|'List')]` | How stdout is handled | +| `[ScriptOutput('None'\|'Toast'\|'Clipboard'\|'Markdown'\|'Result'\|'File'\|'File:csv'\|'List')]` | How stdout is handled. `File:` opens captured stdout as that file type. | | `[ScriptCwd('path')]` | Working directory | | `[ScriptEnv('KEY','value')]` | Set an environment variable | | `[RequiresElevation()]` | Run as administrator (or use `#Requires -RunAsAdministrator`) | @@ -68,4 +68,4 @@ Set-ClipboardText $guid - [ ] Everything is under `scripts//`. - [ ] Each script has a `.SYNOPSIS` and `.DESCRIPTION`. - [ ] Destructive or elevated scripts declare `[ConfirmBeforeRun(...)]` / `[RequiresElevation()]`. -- [ ] Your folder's `README.md` lists your scripts. \ No newline at end of file +- [ ] Your folder's `README.md` lists your scripts. diff --git a/scripts/paletteshell/Base64-Decode.ps1 b/scripts/paletteshell/Base64-Decode.ps1 new file mode 100644 index 0000000..10a895e --- /dev/null +++ b/scripts/paletteshell/Base64-Decode.ps1 @@ -0,0 +1,32 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Base64 Decode Clipboard +.DESCRIPTION + Decode Base64 clipboard text and copy back +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”“')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +try { + $bytes = [System.Convert]::FromBase64String($text) + $decoded = [System.Text.Encoding]::UTF8.GetString($bytes) + Set-ClipboardText $decoded + Write-Host "Decoded Base64 to $($decoded.Length) characters and copied to clipboard" +} +catch { + Write-Host "Invalid Base64 string" + exit 1 +} diff --git a/scripts/paletteshell/Base64-Encode.ps1 b/scripts/paletteshell/Base64-Encode.ps1 new file mode 100644 index 0000000..bdedd57 --- /dev/null +++ b/scripts/paletteshell/Base64-Encode.ps1 @@ -0,0 +1,26 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Base64 Encode Clipboard +.DESCRIPTION + Encode clipboard text to Base64 and copy back +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +$bytes = [System.Text.Encoding]::UTF8.GetBytes($text) +$base64 = [System.Convert]::ToBase64String($bytes) +Set-ClipboardText $base64 +Write-Host "Encoded $($text.Length) characters to Base64 and copied to clipboard" \ No newline at end of file diff --git a/scripts/paletteshell/Clear-TempFiles.ps1 b/scripts/paletteshell/Clear-TempFiles.ps1 new file mode 100644 index 0000000..bb65249 --- /dev/null +++ b/scripts/paletteshell/Clear-TempFiles.ps1 @@ -0,0 +1,44 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Clean Temp Files +.DESCRIPTION + Delete files in your user TEMP folder older than the given number of days, then report + how many were removed and how much space was freed. Because it's destructive it asks for + confirmation first β€” and since it takes a parameter, the prompt appears after you submit + the form (demonstrates ConfirmBeforeRun on a parameterized script). +.PARAMETER OlderThanDays + Only delete files last modified more than this many days ago. +#> +[ScriptGroup('System')] +[ScriptVersion('1.0.0')] +[ScriptIcon('🧹')] +[ScriptTimeout(60000)] +[ScriptOutput('Toast')] +[ConfirmBeforeRun('This permanently deletes old files from your TEMP folder.')] +param( + [ValidateRange(0, 3650)] + [int]$OlderThanDays = 7 +) + +$cutoff = (Get-Date).AddDays(-$OlderThanDays) +$files = Get-ChildItem -Path $env:TEMP -File -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt $cutoff } + +$count = 0 +$bytes = 0L +foreach ($file in $files) { + try { + $size = $file.Length + Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop + $count++ + $bytes += $size + } + catch { + # File is in use or protected; skip it. + } +} + +$mb = [math]::Round($bytes / 1MB, 1) +Write-Host "Deleted $count files Β· freed $mb MB" diff --git a/scripts/paletteshell/Clipboard-RemoveDuplicateLines.ps1 b/scripts/paletteshell/Clipboard-RemoveDuplicateLines.ps1 new file mode 100644 index 0000000..5b0b173 --- /dev/null +++ b/scripts/paletteshell/Clipboard-RemoveDuplicateLines.ps1 @@ -0,0 +1,28 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Remove Duplicate Lines +.DESCRIPTION + Remove duplicate lines from clipboard text +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +$lines = $text -split "(\r\n|\n)" | Where-Object { $_ -match '\S' } +$originalCount = $lines.Count +$unique = $lines | Select-Object -Unique +$result = $unique -join [Environment]::NewLine +Set-ClipboardText $result +Write-Host "Original: $originalCount lines β†’ Unique: $($unique.Count) lines" diff --git a/scripts/paletteshell/Clipboard-SortLines.ps1 b/scripts/paletteshell/Clipboard-SortLines.ps1 new file mode 100644 index 0000000..306611b --- /dev/null +++ b/scripts/paletteshell/Clipboard-SortLines.ps1 @@ -0,0 +1,27 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Sort Lines in Clipboard +.DESCRIPTION + Sort clipboard lines alphabetically +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”€')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +$lines = $text -split "(\r\n|\n)" | Where-Object { $_ -match '\S' } +$sorted = $lines | Sort-Object +$result = $sorted -join [Environment]::NewLine +Set-ClipboardText $result +Write-Host "Sorted $($lines.Count) lines alphabetically" diff --git a/scripts/paletteshell/Clipboard-ToCSV.ps1 b/scripts/paletteshell/Clipboard-ToCSV.ps1 new file mode 100644 index 0000000..0583cee --- /dev/null +++ b/scripts/paletteshell/Clipboard-ToCSV.ps1 @@ -0,0 +1,21 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Clipboard β†’ CSV (quoted) +.DESCRIPTION + Convert multiline clipboard text to comma-delimited, quote-wrapped CSV +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ“‹')] +[ScriptTimeout(15000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +$lines = $text -split "\r\n|\n|\r" | Where-Object { $_ -ne "" } +$csv = ($lines | ForEach-Object { '"' + ($_ -replace '"','""') + '"' }) -join ',' +Set-ClipboardText $csv +Write-Host "Converted $($lines.Count) lines to CSV and copied to clipboard" diff --git a/scripts/paletteshell/Clipboard-ToLowerCase.ps1 b/scripts/paletteshell/Clipboard-ToLowerCase.ps1 new file mode 100644 index 0000000..6b626dd --- /dev/null +++ b/scripts/paletteshell/Clipboard-ToLowerCase.ps1 @@ -0,0 +1,25 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Convert to lowercase +.DESCRIPTION + Convert clipboard text to lowercase +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”‘')] +[ScriptTimeout(5000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +$lower = $text.ToLower() +Set-ClipboardText $lower +Write-Host "Converted to lowercase and copied to clipboard" diff --git a/scripts/paletteshell/Clipboard-ToUpperCase.ps1 b/scripts/paletteshell/Clipboard-ToUpperCase.ps1 new file mode 100644 index 0000000..978fadf --- /dev/null +++ b/scripts/paletteshell/Clipboard-ToUpperCase.ps1 @@ -0,0 +1,25 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Convert to UPPERCASE +.DESCRIPTION + Convert clipboard text to uppercase +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ” ')] +[ScriptTimeout(5000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +$upper = $text.ToUpper() +Set-ClipboardText $upper +Write-Host "Converted to uppercase and copied to clipboard" diff --git a/scripts/paletteshell/Clipboard-TrimLines.ps1 b/scripts/paletteshell/Clipboard-TrimLines.ps1 new file mode 100644 index 0000000..dda073e --- /dev/null +++ b/scripts/paletteshell/Clipboard-TrimLines.ps1 @@ -0,0 +1,27 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Trim Lines in Clipboard +.DESCRIPTION + Remove leading and trailing whitespace from each line +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('βœ‚οΈ')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +$lines = $text -split "(\r\n|\n)" +$trimmed = $lines | ForEach-Object { $_.Trim() } +$result = $trimmed -join [Environment]::NewLine +Set-ClipboardText $result +Write-Host "Trimmed whitespace from $($lines.Count) lines" diff --git a/scripts/paletteshell/Clipboard-UnixTimestamp.ps1 b/scripts/paletteshell/Clipboard-UnixTimestamp.ps1 new file mode 100644 index 0000000..80dd047 --- /dev/null +++ b/scripts/paletteshell/Clipboard-UnixTimestamp.ps1 @@ -0,0 +1,19 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Get Unix Timestamp +.DESCRIPTION + Generate current Unix timestamp and copy to clipboard +#> +[ScriptGroup('Utilities')] +[ScriptVersion('1.0.0')] +[ScriptIcon('⏰')] +[ScriptTimeout(5000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() +Set-ClipboardText $timestamp.ToString() +Write-Host "Unix Timestamp: $timestamp copied to clipboard" diff --git a/scripts/paletteshell/Clipboard-UrlDecode.ps1 b/scripts/paletteshell/Clipboard-UrlDecode.ps1 new file mode 100644 index 0000000..736fb4e --- /dev/null +++ b/scripts/paletteshell/Clipboard-UrlDecode.ps1 @@ -0,0 +1,27 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + URL Decode Clipboard +.DESCRIPTION + URL decode clipboard text and copy back +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”—')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +# Use Uri.UnescapeDataString instead of HttpUtility.UrlDecode +# Also handle '+' to space conversion for form-encoded data +$decoded = [System.Uri]::UnescapeDataString($text.Replace('+', ' ')) +Set-ClipboardText $decoded +Write-Host "Copied to clipboard" \ No newline at end of file diff --git a/scripts/paletteshell/Clipboard-UrlEncode.ps1 b/scripts/paletteshell/Clipboard-UrlEncode.ps1 new file mode 100644 index 0000000..c5bfaed --- /dev/null +++ b/scripts/paletteshell/Clipboard-UrlEncode.ps1 @@ -0,0 +1,25 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + URL Encode Clipboard +.DESCRIPTION + URL encode clipboard text and copy back +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('🌐')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +$encoded = [System.Uri]::EscapeDataString($text) +Set-ClipboardText $encoded +Write-Host "Copied to clipboard" diff --git a/scripts/paletteshell/Export-ProcessList.ps1 b/scripts/paletteshell/Export-ProcessList.ps1 new file mode 100644 index 0000000..9f0a7ce --- /dev/null +++ b/scripts/paletteshell/Export-ProcessList.ps1 @@ -0,0 +1,27 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Export Process List β†’ CSV +.DESCRIPTION + Snapshot running processes (name, PID, memory, CPU) as CSV and open it in the editor / Excel +#> +[ScriptGroup('Utilities')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ“Š')] +[ScriptTimeout(15000)] +[ScriptOutput('File:csv')] +[CmdletBinding()] +param() + +$rows = Get-Process | + Sort-Object -Property WorkingSet64 -Descending | + Select-Object ` + Name, + Id, + @{ Name = 'MemoryMB'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 1) } }, + @{ Name = 'CPUSeconds'; Expression = { if ($_.CPU) { [math]::Round($_.CPU, 1) } else { 0 } } }, + @{ Name = 'Threads'; Expression = { $_.Threads.Count } } + +# Emit CSV to stdout; File output mode writes it to a temp .csv and opens it. +$rows | ConvertTo-Csv -NoTypeInformation | Write-Host diff --git a/scripts/paletteshell/Generate-GUID.ps1 b/scripts/paletteshell/Generate-GUID.ps1 new file mode 100644 index 0000000..606d961 --- /dev/null +++ b/scripts/paletteshell/Generate-GUID.ps1 @@ -0,0 +1,18 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Generate GUID +.DESCRIPTION + Generate a new GUID and show it as a result β€” press Enter to copy it, + or "Run again" to generate a fresh one. +#> +[ScriptGroup('Utilities')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ†”')] +[ScriptTimeout(5000)] +[ScriptOutput('Result')] +param() + +# Emit just the value on stdout; Result mode shows it as a copyable result. +[System.Guid]::NewGuid().ToString() diff --git a/scripts/paletteshell/Get-PublicIP.ps1 b/scripts/paletteshell/Get-PublicIP.ps1 new file mode 100644 index 0000000..566e065 --- /dev/null +++ b/scripts/paletteshell/Get-PublicIP.ps1 @@ -0,0 +1,19 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Get Public IP +.DESCRIPTION + Look up this machine's public IP address and show it as a result β€” + press Enter to copy it, or "Run again" to re-check. +#> +[ScriptGroup('Utilities')] +[ScriptVersion('1.0.0')] +[ScriptIcon('🌐')] +[ScriptTimeout(10000)] +[ScriptOutput('Result')] +param() + +# Emit just the address on stdout; Result mode shows it as a copyable result. +# A longer timeout is set above since this depends on a network round-trip. +(Invoke-RestMethod -Uri 'https://api.ipify.org?format=text').Trim() diff --git a/scripts/paletteshell/Git-Branches.ps1 b/scripts/paletteshell/Git-Branches.ps1 new file mode 100644 index 0000000..5ce9761 --- /dev/null +++ b/scripts/paletteshell/Git-Branches.ps1 @@ -0,0 +1,98 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Pick a Git Branch +.DESCRIPTION + List the local branches of a Git repository as a searchable, pickable list. Select a + branch to copy its name. Demonstrates List output mode as a live provider. +.PARAMETER Path + Folder inside the repository to inspect. In the palette, type or paste this path into + the search box β€” the branch list refreshes as you type. +#> +[ScriptGroup('Developer')] +[ScriptVersion('1.0.0')] +[ScriptIcon('🌿')] +[ScriptTimeout(15000)] +[ScriptOutput('List')] +[CmdletBinding()] +param( + [Parameter()] + [string]$Path +) + +# List output works in two shapes: newline-delimited text (one item per line) or a JSON +# array. We emit a JSON array of objects so each item can carry a subtitle and a distinct +# copy value: +# title -> shown as the item's title (also used as the copy value if no 'value') +# subtitle -> shown under the title +# value -> what gets copied when the item is picked +# Always emit a JSON array (even for the error case) so the picker has something to show. + +function Write-ListResult { + param([Parameter(Mandatory)][object[]]$Items) + # Compress keeps it a single clean line of stdout for the parser. + $Items | ConvertTo-Json -Depth 4 -Compress -AsArray | Write-Output +} + +# When this script is used as a live provider, $Path is whatever the user has typed in the +# palette search box. Until they type something, prompt instead of guessing a folder. +if ([string]::IsNullOrWhiteSpace($Path)) { + Write-ListResult @( + [pscustomobject]@{ + title = 'Type a repository folder path…' + subtitle = 'Paste or type a path in the search box to list that repo''s branches.' + value = '' + } + ) + return +} + +if (-not (Test-Path -LiteralPath $Path)) { + Write-ListResult @( + [pscustomobject]@{ title = 'Folder not found'; subtitle = $Path; value = $Path } + ) + return +} + +# Run git relative to the requested folder. -C makes git treat $Path as the working tree. +$inRepo = (& git -C $Path rev-parse --is-inside-work-tree 2>$null) -eq 'true' +if (-not $inRepo) { + Write-ListResult @( + [pscustomobject]@{ + title = 'Not a Git repository' + subtitle = "$Path is not inside a Git repo β€” type or paste a repo path in the search box." + value = $Path + } + ) + return +} + +# One row per local branch. %(HEAD) is '*' for the checked-out branch; the format string +# packs name, last-commit subject and relative date so we can build a useful subtitle. +$branches = & git -C $Path for-each-ref --sort=-committerdate refs/heads ` + --format='%(HEAD)%09%(refname:short)%09%(contents:subject)%09%(committerdate:relative)' + +$items = foreach ($line in $branches) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + $parts = $line -split "`t", 4 + $isCurrent = $parts[0].Trim() -eq '*' + $name = $parts[1] + $subject = if ($parts.Count -ge 3) { $parts[2] } else { '' } + $when = if ($parts.Count -ge 4) { $parts[3] } else { '' } + + [pscustomobject]@{ + title = if ($isCurrent) { "● $name" } else { $name } + subtitle = (@($when, $subject) | Where-Object { $_ } ) -join ' β€” ' + value = $name + } +} + +if (-not $items) { + Write-ListResult @( + [pscustomobject]@{ title = 'No branches found'; subtitle = $Path; value = '' } + ) + return +} + +Write-ListResult @($items) diff --git a/scripts/paletteshell/Json-Format.ps1 b/scripts/paletteshell/Json-Format.ps1 new file mode 100644 index 0000000..4061227 --- /dev/null +++ b/scripts/paletteshell/Json-Format.ps1 @@ -0,0 +1,32 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Format JSON Clipboard +.DESCRIPTION + Pretty-print JSON from clipboard and copy back +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ“„')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "⚠️ Clipboard is empty" + exit 1 +} + +try { + $json = $text | ConvertFrom-Json + $formatted = $json | ConvertTo-Json -Depth 100 + Set-ClipboardText $formatted + Write-Host "Formatted JSON and copied to clipboard" +} +catch { + Write-Host "Invalid JSON: $($_.Exception.Message)" + exit 1 +} diff --git a/scripts/paletteshell/Json-Minify.ps1 b/scripts/paletteshell/Json-Minify.ps1 new file mode 100644 index 0000000..b079927 --- /dev/null +++ b/scripts/paletteshell/Json-Minify.ps1 @@ -0,0 +1,32 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Minify JSON Clipboard +.DESCRIPTION + Compress JSON from clipboard and copy back +#> +[ScriptGroup('Clipboard')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ—œοΈ')] +[ScriptTimeout(10000)] +[ScriptOutput('None')] +[CmdletBinding()] +param() + +$text = Get-ClipboardText +if ([string]::IsNullOrEmpty($text)) { + Write-Host "Clipboard is empty" + exit 1 +} + +try { + $json = $text | ConvertFrom-Json + $minified = $json | ConvertTo-Json -Depth 100 -Compress + Set-ClipboardText $minified + Write-Host "Saved $($text.Length - $minified.Length) characters ($([math]::Round((($text.Length - $minified.Length) / $text.Length) * 100, 1))% reduction)" +} +catch { + Write-Host "Invalid JSON: $($_.Exception.Message)" + exit 1 +} diff --git a/scripts/paletteshell/README.md b/scripts/paletteshell/README.md index 896a8b3..89511d2 100644 --- a/scripts/paletteshell/README.md +++ b/scripts/paletteshell/README.md @@ -6,4 +6,26 @@ Official scripts maintained by the PaletteShell team. This folder is a good plac ## Scripts -_None yet β€” this collection will grow with curated, first-party examples. In the meantime, see the sample scripts that ship with the [PaletteShell extension](https://github.com/paletteshell/PaletteShellExtension)._ +| Script | What it does | +|---|---| +| `Base64-Decode.ps1` | Decode Base64 clipboard text and copy back. | +| `Base64-Encode.ps1` | Encode clipboard text to Base64 and copy back. | +| `Clear-TempFiles.ps1` | Delete old files in your user TEMP folder, then report the count and space freed. | +| `Clipboard-RemoveDuplicateLines.ps1` | Remove duplicate lines from clipboard text. | +| `Clipboard-SortLines.ps1` | Sort clipboard lines alphabetically. | +| `Clipboard-ToCSV.ps1` | Convert multiline clipboard text to comma-delimited, quote-wrapped CSV. | +| `Clipboard-ToLowerCase.ps1` | Convert clipboard text to lowercase. | +| `Clipboard-ToUpperCase.ps1` | Convert clipboard text to uppercase. | +| `Clipboard-TrimLines.ps1` | Remove leading and trailing whitespace from each line. | +| `Clipboard-UnixTimestamp.ps1` | Generate the current Unix timestamp and copy it to the clipboard. | +| `Clipboard-UrlDecode.ps1` | URL decode clipboard text and copy back. | +| `Clipboard-UrlEncode.ps1` | URL encode clipboard text and copy back. | +| `Export-ProcessList.ps1` | Snapshot running processes as CSV and open the output as a file. | +| `Generate-GUID.ps1` | Generate a new GUID and show it as a copyable result. | +| `Get-PublicIP.ps1` | Look up this machine's public IP address and show it as a copyable result. | +| `Git-Branches.ps1` | List local Git branches as a searchable, pickable list. | +| `Json-Format.ps1` | Pretty-print JSON from the clipboard and copy back. | +| `Json-Minify.ps1` | Compress JSON from the clipboard and copy back. | +| `Restart-Explorer.ps1` | Restart the Windows Explorer shell after confirmation. | +| `System-Report.ps1` | Show a quick system summary rendered as Markdown. | +| `Text-Transform.ps1` | Apply customizable transformations to clipboard text. | diff --git a/scripts/paletteshell/Restart-Explorer.ps1 b/scripts/paletteshell/Restart-Explorer.ps1 new file mode 100644 index 0000000..1864a5b --- /dev/null +++ b/scripts/paletteshell/Restart-Explorer.ps1 @@ -0,0 +1,27 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Restart Explorer +.DESCRIPTION + Restart the Windows Explorer shell β€” handy after changing shell settings. Open File + Explorer windows will close, so it asks for confirmation first (demonstrates + ConfirmBeforeRun). +#> +[ScriptGroup('System')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”')] +[ScriptTimeout(10000)] +[ScriptOutput('Toast')] +[ConfirmBeforeRun('This restarts Windows Explorer. Open File Explorer windows will close.')] +param() + +Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 1 + +# Windows usually relaunches the shell on its own; start it if it didn't come back. +if (-not (Get-Process -Name explorer -ErrorAction SilentlyContinue)) { + Start-Process explorer.exe +} + +Write-Host 'Explorer restarted' diff --git a/scripts/paletteshell/System-Report.ps1 b/scripts/paletteshell/System-Report.ps1 new file mode 100644 index 0000000..67b824c --- /dev/null +++ b/scripts/paletteshell/System-Report.ps1 @@ -0,0 +1,32 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + System Report +.DESCRIPTION + Show a quick system summary rendered as Markdown +#> +[ScriptGroup('Utilities')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ–₯️')] +[ScriptTimeout(15000)] +[ScriptOutput('Markdown')] +param() + +$os = Get-CimInstance Win32_OperatingSystem +$cpu = (Get-CimInstance Win32_Processor | Select-Object -First 1).Name +$totalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1) +$freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1) +$uptime = (Get-Date) - $os.LastBootUpTime + +Write-Host "# System Report" +Write-Host "" +Write-Host "| Property | Value |" +Write-Host "| --- | --- |" +Write-Host "| Computer | $env:COMPUTERNAME |" +Write-Host "| User | $env:USERNAME |" +Write-Host "| OS | $($os.Caption) ($($os.Version)) |" +Write-Host "| CPU | $cpu |" +Write-Host "| Memory | $freeGB GB free / $totalGB GB total |" +Write-Host "| Uptime | $([math]::Floor($uptime.TotalHours))h $($uptime.Minutes)m |" +Write-Host "| PowerShell | $($PSVersionTable.PSVersion) |" diff --git a/scripts/paletteshell/Text-Transform.ps1 b/scripts/paletteshell/Text-Transform.ps1 new file mode 100644 index 0000000..8995b8b --- /dev/null +++ b/scripts/paletteshell/Text-Transform.ps1 @@ -0,0 +1,128 @@ +using module .\PaletteScriptAttributes.psm1 + +<# +.SYNOPSIS + Transform Text +.DESCRIPTION + Apply various transformations to clipboard text with customizable options +.PARAMETER Operation + The transformation operation to perform +.PARAMETER Prefix + Text to add before each line +.PARAMETER Suffix + Text to add after each line +.PARAMETER RepeatCount + Number of times to repeat each line +.PARAMETER RemoveEmptyLines + Remove empty lines from the output +.EXAMPLE + .\Text-Transform.ps1 -Operation "Quote" -Prefix '"' -Suffix '"' + Wraps each line in double quotes +#> +[ScriptGroup('Text Utilities')] +[ScriptVersion('1.0.0')] +[ScriptIcon('πŸ”„')] +[ScriptTimeout(15000)] +[ScriptOutput('None')] +[CmdletBinding()] +param( + # Transformation type + [Parameter(Mandatory=$true, HelpMessage="Select the transformation to apply")] + [ValidateSet("Quote", "AddLineNumbers", "Indent", "Custom", "Repeat")] + [string]$Operation = "Quote", + + # Prefix text (used in Custom mode) + [Parameter(Mandatory=$false, HelpMessage="Text to add before each line")] + [string]$Prefix = "", + + # Suffix text (used in Custom mode) + [Parameter(Mandatory=$false, HelpMessage="Text to add after each line")] + [string]$Suffix = "", + + # Number of spaces for indentation + [Parameter(Mandatory=$false, HelpMessage="Number of spaces to indent")] + [ValidateRange(1, 20)] + [int]$IndentSpaces = 4, + + # Repeat count + [Parameter(Mandatory=$false, HelpMessage="Times to repeat each line")] + [ValidateRange(1, 100)] + [int]$RepeatCount = 1, + + # Remove empty lines + [Parameter(Mandatory=$false)] + [switch]$RemoveEmptyLines +) + +# Get clipboard text +$text = Get-ClipboardText + +if ([string]::IsNullOrEmpty($text)) { + Write-Host "⚠️ Clipboard is empty" + exit 1 +} + +# Split into lines +$lines = $text -split "(\r\n|\n|\r)" +if ($RemoveEmptyLines) { + $lines = $lines | Where-Object { $_ -ne "" -and $_ -notmatch '^\s*$' } +} + +Write-Host "Processing $($lines.Count) lines with operation: $Operation" + +# Apply transformation based on operation +$result = switch ($Operation) { + "Quote" { + $lines | ForEach-Object { '"' + ($_ -replace '"', '""') + '"' } + } + + "AddLineNumbers" { + $lineNum = 1 + $lines | ForEach-Object { + if ($_ -match '\S') { # Only number non-empty lines + "$lineNum. $_" + $lineNum++ + } else { + $_ + } + } + } + + "Indent" { + $indent = " " * $IndentSpaces + $lines | ForEach-Object { $indent + $_ } + } + + "Custom" { + $lines | ForEach-Object { $Prefix + $_ + $Suffix } + } + + "Repeat" { + $lines | ForEach-Object { + $line = $_ + 1..$RepeatCount | ForEach-Object { $line } + } + } +} + +# Join lines back together +$output = $result -join "`n" + +# Copy to clipboard +Set-ClipboardText $output + +# Report results +Write-Host "βœ… Transformation complete!" +Write-Host " Operation: $Operation" +if ($Operation -eq "Indent") { + Write-Host " Indent: $IndentSpaces spaces" +} +if ($Operation -eq "Custom") { + Write-Host " Prefix: '$Prefix'" + Write-Host " Suffix: '$Suffix'" +} +if ($Operation -eq "Repeat") { + Write-Host " Repeat count: $RepeatCount" +} +Write-Host " Lines processed: $($lines.Count)" +Write-Host " Output copied to clipboard" diff --git a/tools/Test-Contribution.ps1 b/tools/Test-Contribution.ps1 index b930a58..41c5265 100644 --- a/tools/Test-Contribution.ps1 +++ b/tools/Test-Contribution.ps1 @@ -18,7 +18,7 @@ $ErrorActionPreference = 'Stop' $ExcludedAuthors = @('_template') $AllowedHosts = @('pwsh', 'powershell') -$AllowedOutputs = @('None', 'Toast', 'Clipboard', 'Markdown', 'File', 'List') +$AllowedOutputs = @('None', 'Toast', 'Clipboard', 'Markdown', 'Result', 'File', 'List') $failures = [System.Collections.Generic.List[string]]::new() function Add-Failure { @@ -91,6 +91,23 @@ function Get-FirstAttributeArgument { return $null } +function Test-ScriptOutputSpec { + param([Parameter(Mandatory)][string]$Value) + + $parts = $Value.Split(':', 2) + $mode = $parts[0].Trim() + if ($AllowedOutputs -notcontains $mode) { + return $false + } + + if ($parts.Count -eq 1) { + return $true + } + + # PaletteShell supports extension hints only for File output, e.g. File:csv. + return $mode -eq 'File' -and -not [string]::IsNullOrWhiteSpace($parts[1]) +} + $scriptsRoot = Join-Path $RepoRoot 'scripts' if (-not (Test-Path -Path $scriptsRoot -PathType Container)) { throw "Scripts directory not found: $scriptsRoot" @@ -165,8 +182,8 @@ foreach ($authorFolder in $authorFolders) { Add-Failure -Path $relativePath -Message "Invalid ScriptHost '$($attributes['ScriptHost'])'. Expected one of: $($AllowedHosts -join ', ')." } - if ($attributes.ContainsKey('ScriptOutput') -and $AllowedOutputs -notcontains $attributes['ScriptOutput']) { - Add-Failure -Path $relativePath -Message "Invalid ScriptOutput '$($attributes['ScriptOutput'])'. Expected one of: $($AllowedOutputs -join ', ')." + if ($attributes.ContainsKey('ScriptOutput') -and -not (Test-ScriptOutputSpec -Value $attributes['ScriptOutput'])) { + Add-Failure -Path $relativePath -Message "Invalid ScriptOutput '$($attributes['ScriptOutput'])'. Expected one of: $($AllowedOutputs -join ', '), or File:." } if ($attributes.ContainsKey('ScriptTimeout')) {