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
+1
View File
@@ -0,0 +1 @@
/target
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "gm-switch"
version = "0.1.0"
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "gm-switch"
version = "0.1.0"
edition = "2021"
description = "Set the J621 shell greeting rating mode (NSFW / SFW / Questionable)"
publish = false
[profile.release]
strip = true
lto = true
+54
View File
@@ -0,0 +1,54 @@
# win-gm-switch (Rust)
Rust port of the old `gm-switch.cpp` from
`J621-Django/extras/fish_greeting system/`: a tiny CLI that sets the J621
shell greeting rating mode (0 = NSFW, 1 = SFW, 2 = Questionable).
## Usage
```
gm-switch --mode 0 # NSFW (explicit)
gm-switch --mode 1 # SFW (safe)
gm-switch --mode 2 # Questionable
gm-switch --status # print the current mode
```
`--path <file>` overrides the mode file (also via the `J621_MODE_FILE`
environment variable). Invalid input prints the same usage/errors as the C++
tool and exits 1.
Mode file locations:
- **Windows**: `%APPDATA%\j621Greeting\greeting_mode` - read by
[`../powershell_greeting/`](../powershell_greeting/).
- **Linux/macOS**: `~/.config/fish/greeting_mode` - read by
[`../fish_greeting/`](../fish_greeting/), so this binary can stand in for
the C++ tool there.
## Install (Windows)
```powershell
cd extras\win-gm-switch
.\install.ps1
```
The installer builds with `cargo build --release`, installs `gm-switch.exe`
to `%LOCALAPPDATA%\J621\bin`, adds that folder to your user PATH (new
terminals pick it up) and creates Start Menu shortcuts under **J621
Greeting** (NSFW / SFW / Questionable). Each shortcut opens a small console
showing the confirmation; close it with `exit` or the window button.
Uninstall:
```powershell
.\install.ps1 -Uninstall
```
## Build and test without installing
```powershell
cargo build --release # -> target\release\gm-switch.exe
cargo test
```
Zero runtime dependencies (std only). The `target\` folder is gitignored.
+105
View File
@@ -0,0 +1,105 @@
# Build and install gm-switch:
# * cargo build --release (needs the Rust toolchain on PATH)
# * copies gm-switch.exe to %LOCALAPPDATA%\J621\bin
# * adds that folder to your user PATH (if missing)
# * creates Start Menu shortcuts for each mode under "J621 Greeting"
#
# Usage:
# .\install.ps1
# .\install.ps1 -Uninstall
[CmdletBinding()]
param(
[switch]$Uninstall
)
$ErrorActionPreference = 'Stop'
$here = $PSScriptRoot
if (-not $here) { $here = Split-Path -Parent $MyInvocation.MyCommand.Path }
$binDir = Join-Path $env:LOCALAPPDATA 'J621\bin'
$exe = Join-Path $binDir 'gm-switch.exe'
$startMenuDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\J621 Greeting'
if ($Uninstall) {
if (Test-Path -LiteralPath $startMenuDir) {
Remove-Item -LiteralPath $startMenuDir -Recurse -Force
Write-Host "Removed $startMenuDir"
}
if (Test-Path -LiteralPath $exe) {
Remove-Item -LiteralPath $exe -Force
Write-Host "Removed $exe"
}
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($userPath) {
$parts = @($userPath -split ';' | Where-Object { $_ -and $_ -ne $binDir })
[Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User')
Write-Host "Removed $binDir from your user PATH"
}
exit 0
}
# --- Build ---------------------------------------------------------------------
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
Write-Host 'cargo was not found on PATH - install the Rust toolchain (https://rustup.rs) first.'
exit 1
}
Push-Location $here
try {
Write-Host 'Building gm-switch (cargo build --release) ...'
# cargo writes progress to stderr; keep that from tripping $ErrorActionPreference.
$previousPreference = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
cargo build --release 2>&1 | Write-Host
$buildExit = $LASTEXITCODE
$ErrorActionPreference = $previousPreference
if ($buildExit -ne 0) {
Write-Host 'cargo build failed.'
exit 1
}
} finally {
Pop-Location
}
$built = Join-Path $here 'target\release\gm-switch.exe'
if (-not (Test-Path -LiteralPath $built)) {
Write-Host "Build output not found: $built"
exit 1
}
# --- Install -------------------------------------------------------------------
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
Copy-Item -LiteralPath $built -Destination $exe -Force
Write-Host "Installed $exe"
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not $userPath) { $userPath = '' }
if (-not (($userPath -split ';') -contains $binDir)) {
$newPath = ($userPath.TrimEnd(';') + ';' + $binDir).TrimStart(';')
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
Write-Host "Added $binDir to your user PATH (open a new terminal to pick it up)"
}
# --- Shortcuts ------------------------------------------------------------------
New-Item -ItemType Directory -Path $startMenuDir -Force | Out-Null
$modes = @(
@{ Value = '0'; Name = 'NSFW' },
@{ Value = '1'; Name = 'SFW' },
@{ Value = '2'; Name = 'Questionable' }
)
$shell = New-Object -ComObject WScript.Shell
foreach ($m in $modes) {
$lnkPath = Join-Path $startMenuDir ('J621 Greeting - ' + $m.Name + '.lnk')
$shortcut = $shell.CreateShortcut($lnkPath)
$shortcut.TargetPath = "$env:SystemRoot\System32\cmd.exe"
$shortcut.Arguments = '/k ""' + $exe + '" --mode ' + $m.Value + '"'
$shortcut.WorkingDirectory = $env:USERPROFILE
$shortcut.Description = 'Set the J621 shell greeting to ' + $m.Name + ' mode'
$shortcut.Save()
Write-Host ('Created shortcut: ' + $lnkPath)
}
Write-Host ''
Write-Host 'Done. Try: gm-switch --status'
Write-Host 'The Start Menu shortcuts open a small console showing the confirmation.'
exit 0
+188
View File
@@ -0,0 +1,188 @@
//! gm-switch - set the J621 shell greeting rating mode.
//!
//! Rust port of the old gm-switch.cpp (J621-Django/extras/fish_greeting
//! system). On Windows it writes the mode file used by the PowerShell
//! greeting (%APPDATA%\j621Greeting\greeting_mode); on Linux/macOS it writes
//! ~/.config/fish/greeting_mode, so it can stand in for the C++ tool there.
//!
//! Usage:
//! gm-switch --mode <0|1|2> [--path <file>]
//! gm-switch --status [--path <file>]
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::exit;
const USAGE: &str = "\
Usage: gm-switch --mode <0|1|2>
--mode 0 Set to NSFW mode (explicit)
--mode 1 Set to SFW mode (safe)
--mode 2 Set to Questionable mode
--status Print the current greeting mode
--path <file> Override the mode file location
";
fn mode_name(mode: &str) -> Option<&'static str> {
match mode {
"0" => Some("NSFW"),
"1" => Some("SFW"),
"2" => Some("Questionable"),
_ => None,
}
}
fn default_mode_file() -> PathBuf {
if let Ok(path) = env::var("J621_MODE_FILE") {
if !path.is_empty() {
return PathBuf::from(path);
}
}
#[cfg(windows)]
{
if let Ok(appdata) = env::var("APPDATA") {
return Path::new(&appdata).join("j621Greeting").join("greeting_mode");
}
}
#[cfg(not(windows))]
{
if let Ok(home) = env::var("HOME") {
return Path::new(&home)
.join(".config")
.join("fish")
.join("greeting_mode");
}
}
PathBuf::from("greeting_mode")
}
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
let mut mode: Option<String> = None;
let mut status = false;
let mut path_override: Option<String> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--mode" => {
if i + 1 < args.len() {
mode = Some(args[i + 1].clone());
i += 2;
} else {
eprintln!("Error: --mode requires a value");
print!("{USAGE}");
exit(1);
}
}
"--status" => {
status = true;
i += 1;
}
"--path" => {
if i + 1 < args.len() {
path_override = Some(args[i + 1].clone());
i += 2;
} else {
eprintln!("Error: --path requires a value");
print!("{USAGE}");
exit(1);
}
}
"-h" | "--help" => {
print!("{USAGE}");
exit(0);
}
other => {
eprintln!("Error: unknown argument: {other}");
print!("{USAGE}");
exit(1);
}
}
}
let mode_file = match &path_override {
Some(p) => PathBuf::from(p),
None => default_mode_file(),
};
if status {
match fs::read_to_string(&mode_file) {
Ok(content) => {
let value = content.trim().to_string();
match mode_name(&value) {
Some(name) => println!("Current greeting mode: {name} ({value})"),
None => println!(
"Current greeting mode: {value} (unrecognized - the greeting falls back to NSFW)"
),
}
}
Err(_) => {
println!("Current greeting mode: 2 (Questionable, default - no mode file yet)")
}
}
return;
}
let value = match mode {
Some(v) => v,
None => {
print!("{USAGE}");
exit(1);
}
};
let name = match mode_name(&value) {
Some(name) => name,
None => {
eprintln!("Error: Mode must be 0, 1, or 2");
print!("{USAGE}");
exit(1);
}
};
if let Some(parent) = mode_file.parent() {
if !parent.as_os_str().is_empty() {
if let Err(err) = fs::create_dir_all(parent) {
eprintln!("Error: {err}");
exit(1);
}
}
}
if let Err(err) = fs::write(&mode_file, &value) {
eprintln!(
"Error: Could not open file for writing: {}",
mode_file.display()
);
eprintln!("Error: {err}");
exit(1);
}
println!("Successfully set greeting mode to: {name}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_modes_to_names() {
assert_eq!(mode_name("0"), Some("NSFW"));
assert_eq!(mode_name("1"), Some("SFW"));
assert_eq!(mode_name("2"), Some("Questionable"));
assert_eq!(mode_name("x"), None);
}
#[test]
fn writes_and_reads_mode_file() {
let dir = env::temp_dir().join("gm-switch-test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let file = dir.join("greeting_mode");
fs::write(&file, "1").unwrap();
assert_eq!(fs::read_to_string(&file).unwrap().trim(), "1");
let _ = fs::remove_dir_all(&dir);
}
}