extras: add PowerShell greeting port, Rust gm-switch and Windows installers

- powershell_greeting: PowerShell port of fish_greeting (fastfetch random logo, sixel/kitty-direct per terminal, logs, modes)

- win-gm-switch: Rust port of gm-switch.cpp (cross-platform mode file, --status, Start Menu shortcuts installer)

- install-windows.ps1 umbrella installer; README pointers
This commit is contained in:
2026-09-30 09:34:01 -05:00
parent 9b431eb982
commit d2f857d3ee
12 changed files with 976 additions and 3 deletions
+190
View File
@@ -0,0 +1,190 @@
# J621Greeting.ps1 - show a random J621 library image as the shell greeting.
#
# PowerShell port of extras/fish_greeting/fish_greeting.fish (the J621 REST
# rewrite): asks GET /api/random/?fastfetch=1&rating=X for a random
# png/jpg/gif, downloads the signed url and runs fastfetch with the image as
# its logo. Based on the original script from
# J621-Django/extras/fish_greeting system/fish_greeting.fish.
#
# Requires: fastfetch and curl.exe (ships with Windows 10 1803+).
#
# Rating modes (same file name as the fish version):
# %APPDATA%\j621Greeting\greeting_mode 0 = NSFW (explicit)
# 1 = SFW (safe)
# 2 = Questionable (default)
#
# Configuration: %APPDATA%\j621Greeting\config.ps1 (sets environment
# variables; see config.example.ps1):
# J621_BASE API origin (default https://j621.rainbow-herring.ts.net)
# J621_TOKEN API / scoped greeting token, sent as "Authorization: Token ..."
# J621_WEB web origin used in the printed link (defaults to J621_BASE)
# J621_FASTFETCH_CONFIG fastfetch config name (default "jake")
# J621_MODE_FILE mode file override
# J621_LOGO_TYPE force a fastfetch logo type (sixel, kitty-direct, ...)
# J621_ALWAYS_FETCH=1 fetch even in terminals without image support
# J621_LOGO_WIDTH logo width in columns (default 35)
#Requires -Version 5.1
function Get-J621FileKind {
param([string]$Path)
try {
$stream = [IO.File]::OpenRead($Path)
try {
$buffer = New-Object byte[] 8
$read = $stream.Read($buffer, 0, 8)
if ($read -ge 4 -and $buffer[0] -eq 0x89 -and $buffer[1] -eq 0x50 -and $buffer[2] -eq 0x4E -and $buffer[3] -eq 0x47) { return 'png' }
if ($read -ge 2 -and $buffer[0] -eq 0xFF -and $buffer[1] -eq 0xD8) { return 'jpeg' }
if ($read -ge 3 -and $buffer[0] -eq 0x47 -and $buffer[1] -eq 0x49 -and $buffer[2] -eq 0x46) { return 'gif' }
return 'unknown'
} finally { $stream.Dispose() }
} catch { return 'unknown' }
}
function Format-J621Accent {
param([string]$Text)
return "$([char]27)[4;2;38;2;138;0;222;49m$Text$([char]27)[0m"
}
function Format-J621Error {
param([string]$Text)
return "$([char]27)[31m$Text$([char]27)[0m"
}
function Get-J621FastfetchArgs {
$configName = if ($env:J621_FASTFETCH_CONFIG) { $env:J621_FASTFETCH_CONFIG } else { 'jake' }
$candidates = @(
(Join-Path $env:APPDATA "fastfetch\$configName.jsonc"),
(Join-Path $env:APPDATA "fastfetch\$configName\config.jsonc")
)
foreach ($candidate in $candidates) {
if (Test-Path -LiteralPath $candidate) { return @('--config', $configName) }
}
return @()
}
function Show-J621Greeting {
param([int]$Depth = 0)
# --- Configuration ---
$configFile = Join-Path $env:APPDATA 'j621Greeting\config.ps1'
if (Test-Path -LiteralPath $configFile) { . $configFile }
$base = if ($env:J621_BASE) { $env:J621_BASE.TrimEnd('/') } else { 'https://j621.rainbow-herring.ts.net' }
$webBase = if ($env:J621_WEB) { $env:J621_WEB.TrimEnd('/') } else { $base }
$token = $env:J621_TOKEN
$logoWidth = if ($env:J621_LOGO_WIDTH) { $env:J621_LOGO_WIDTH } else { '35' }
$modeFile = if ($env:J621_MODE_FILE) { $env:J621_MODE_FILE } else { Join-Path $env:APPDATA 'j621Greeting\greeting_mode' }
# --- Setup logging ---
$logDir = Join-Path $env:APPDATA 'j621Logos'
$logFile = Join-Path $logDir 'logs.log'
if (-not (Test-Path -LiteralPath $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
$stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
# --- Mode selection ---
$mode = '2'
if (Test-Path -LiteralPath $modeFile) {
$raw = Get-Content -LiteralPath $modeFile -Raw -ErrorAction SilentlyContinue
if ($null -ne $raw) { $mode = $raw.Trim() }
}
switch ($mode) {
'0' { $rating = 'e'; $modeName = 'NSFW' }
'1' { $rating = 's'; $modeName = 'SFW' }
'2' { $rating = 'q'; $modeName = 'Questionable' }
default { Write-Output 'Unknown mode, using default NSFW'; $rating = 'e'; $modeName = 'NSFW' }
}
# --- Pick the image protocol for this terminal ---
$logoType = $env:J621_LOGO_TYPE
if (-not $logoType) {
if ($env:WT_SESSION) { $logoType = 'sixel' }
elseif ($env:WEZTERM_PANE) { $logoType = 'kitty-direct' }
}
$fetchImage = [bool]$logoType -or ($env:J621_ALWAYS_FETCH -eq '1')
$ffArgs = @(Get-J621FastfetchArgs)
if (-not $fetchImage) {
# Terminal without image support (e.g. VS Code): plain fastfetch.
& fastfetch @ffArgs
return
}
# --- Ask the API for a random image (JSON with a signed URL) ---
$apiUrl = "$base/api/random/?fastfetch=1&rating=$rating"
$curlArgs = @('-s', '-L', '--max-time', '20')
if ($token) { $curlArgs += @('-H', "Authorization: Token $token") }
$json = & curl.exe @curlArgs $apiUrl 2>$null | Out-String
$data = $null
try { $data = $json | ConvertFrom-Json } catch { $data = $null }
$imageUrl = $null; $jId = $null; $md5 = $null; $filename = $null; $detail = $null; $ext = 'img'
if ($data) {
if ($data.url) { $imageUrl = [string]$data.url }
if ($data.j_id) { $jId = [string]$data.j_id }
if ($data.md5) { $md5 = [string]$data.md5 }
if ($data.filename) { $filename = [string]$data.filename }
if ($data.detail) { $detail = [string]$data.detail }
if ($data.extension) { $ext = [string]$data.extension }
}
if ($ext -notmatch '^[A-Za-z0-9]+$') { $ext = 'img' }
if (-not $imageUrl) {
& fastfetch @ffArgs
if ($detail) { Write-Host (Format-J621Accent "No logo ($detail)") } else { Write-Host (Format-J621Accent 'No logo (API error)') }
Add-Content -LiteralPath $logFile -Value "$stamp No logo for rating=${rating}: $detail"
return
}
# --- Download the signed image ---
$tempFile = Join-Path ([IO.Path]::GetTempPath()) ('j621_logo_' + [guid]::NewGuid().ToString('N') + '.' + $ext)
$downloadSuccess = $false
& curl.exe -s -L --max-time 60 -o $tempFile $imageUrl 2>$null
if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $tempFile) -and (Get-Item -LiteralPath $tempFile).Length -gt 0) { $downloadSuccess = $true }
if (-not $downloadSuccess) {
& fastfetch @ffArgs
Write-Host (Format-J621Accent 'No logo (download failed)')
Add-Content -LiteralPath $logFile -Value "$stamp Download failed for $jId"
Remove-Item -LiteralPath $tempFile -ErrorAction SilentlyContinue
return
}
# --- Accept only PNG, JPEG, GIF; anything else causes one more roll ---
$kind = Get-J621FileKind -Path $tempFile
if ($kind -eq 'unknown') {
if ($Depth -lt 3) {
Show-J621Greeting -Depth ($Depth + 1)
} else {
Write-Host (Format-J621Error 'Max recursion depth reached, skipping unsupported file type: unknown')
}
if ($Depth -eq 0) {
Write-Host (Format-J621Error ("Forked (Got unsupported type: unknown - $filename)"))
if ($md5) { Write-Output "Link: $webBase/view/$md5" }
}
Remove-Item -LiteralPath $tempFile -ErrorAction SilentlyContinue
return
}
# --- Display result ---
if ($logoType) {
& fastfetch @ffArgs --logo-type $logoType --logo $tempFile --logo-width $logoWidth
} else {
& fastfetch @ffArgs
}
Add-Content -LiteralPath $logFile -Value "$stamp Downloaded: $jId $filename [$modeName]"
if ($filename) {
Write-Host (Format-J621Accent "Logo from API: $filename ($modeName, $jId)")
Write-Output "Link: $webBase/detail/$jId"
} else {
Write-Host (Format-J621Accent "Logo from API ($modeName)")
}
# --- Keep only the last 1000 lines of the log ---
$lines = @(Get-Content -LiteralPath $logFile -Tail 1000)
Set-Content -LiteralPath $logFile -Value $lines
# --- Cleanup ---
Remove-Item -LiteralPath $tempFile -ErrorAction SilentlyContinue
}