Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ to say β€” fastfetch hides a module entirely when it prints nothing, so a health
machine stays clean:

```console
Updates: πŸ“¦ 6 update(s) available (winget)
Updates: πŸ“¦ 6 update(s) available (winget) Β· checked 5m ago
Reboot: πŸ”„ Reboot required β€” linux-image-generic
Ansible: βœ… ansible-pull OK Β· ran 5m ago Β· next in 25m
```
Expand All @@ -178,6 +178,12 @@ the next login.
On Windows fastfetch reads the cache with `cmd /c type` rather than starting
PowerShell, which keeps the line free (a few milliseconds).

The `checked 5m ago` part is deliberately not stored in the cache β€” that would
freeze it until the next hourly refresh. Each section is kept twice: a source
file holding the moment as an absolute timestamp, and the rendered line the
banner shows. Expanding one into the other is pure arithmetic and happens on
every shell start, so the age is always correct when you read it.

Refresh by hand β€” useful right after installing updates:

```bash
Expand All @@ -195,6 +201,20 @@ Refresh by hand β€” useful right after installing updates:
| `FASTFETCH_STATUS_CACHE_DIR` | Override the cache directory (Windows; testing) |
| `ANSIBLE_PULL_WORKDIR` | ansible-pull checkout (default `/var/lib/ansible/local`) |

### A quiet PowerShell startup

fastfetch already reports the shell version and everything else worth knowing,
so the noise around it is turned off rather than printed twice:

- `PowerShell 7.6.4` and `Loading personal and system profiles took 1513ms.` are
printed by pwsh itself, before and after the profile runs. They cannot be
suppressed from `profile.ps1`, so the Windows Terminal PowerShell profile is
launched as `pwsh.exe -NoLogo -NoProfileLoadTime` instead (set by
`run_onchange_set-windows-terminal-powershell-args.ps1`).
- The dotfiles' own `[OK] PowerShell Profile Loaded` and `Type 'aliases'` lines
are skipped whenever the fastfetch banner rendered. On a light install without
fastfetch they stay, so an interactive shell still confirms the profile loaded.

## Windows PATH

On Windows, the setup adds `%OneDrive%\Portable Programs` to the user-scope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# {{ if eq .chezmoi.os "windows" }}
# Start the Windows Terminal PowerShell profile quietly.
#
# Two lines of noise appear before anything useful:
#
# PowerShell 7.6.4
# Loading personal and system profiles took 1513ms.
#
# Both are printed by pwsh itself around profile load, so they cannot be
# suppressed from profile.ps1 -- the banner is written before the profile runs
# and the timing line after it. The only lever is the command line the terminal
# starts, hence -NoLogo -NoProfileLoadTime here. fastfetch already reports the
# shell version in its banner, so nothing is lost.
#
# pwsh.exe is left to PATH resolution rather than pinned to an absolute path so
# the profile keeps working across PowerShell upgrades and reinstalls.
#
# As with the sibling default-profile script, Windows Terminal owns
# settings.json and every machine has a different, hand-tuned config, so only
# this one profile's commandline is touched.
$ErrorActionPreference = "Stop"

$modulePath = "{{ .chezmoi.sourceDir }}\dot_config\powershell\modules\DotfilesHelpers"
if (Test-Path $modulePath) {
Import-Module $modulePath -Force -DisableNameChecking
}
else {
Write-Error "DotfilesHelpers module not found at: $modulePath"
exit 1
}

Write-Host "Setting Windows Terminal PowerShell startup arguments..." -ForegroundColor Cyan
$results = Set-WindowsTerminalProfileCommandLine `
-Source "Windows.Terminal.PowershellCore" `
-CommandLine "pwsh.exe -NoLogo -NoProfileLoadTime"

if (-not $results) {
Write-Host "[SKIP] No Windows Terminal settings found to update" -ForegroundColor Yellow
}
elseif (-not ($results | Where-Object { $_.Status -in @('Updated', 'AlreadySet') })) {
Write-Host "[SKIP] PowerShell Core profile not found; leaving commandline unchanged" -ForegroundColor Yellow
}
# {{ end }}
35 changes: 22 additions & 13 deletions home/dot_config/fastfetch/executable_status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
#
# Relative times ("ran 5m ago", "next in 20m") are NOT baked into the cache
# β€” that would freeze them until the next refresh. The collectors store
# absolute epoch tokens (@ago:<epoch>@ / @in:<epoch>@) and the section
# commands expand them against the current clock on every fastfetch run.
# Expansion itself is pure shell arithmetic; reading the clock uses the
# EPOCHSECONDS builtin on bash >= 5.0 and falls back to one `date` call on
# older bash, so the emit path costs at most a single fork.
# absolute epoch tokens (@ago:<epoch>@ / @in:<epoch>@) that expand to a bare
# duration phrase ("5m ago" / "in 20m"), and the section commands expand them
# against the current clock on every fastfetch run. Expansion itself is pure
# shell arithmetic; reading the clock uses the EPOCHSECONDS builtin on
# bash >= 5.0 and falls back to one `date` call on older bash, so the emit
# path costs at most a single fork.
#
# Usage: status.sh <section|command>
# updates Print cached "updates available" line (may be empty)
Expand Down Expand Up @@ -90,8 +91,10 @@ _now() {
}

# _render LINE NOW -> print LINE with relative-time tokens expanded:
# @ago:<epoch>@ -> "ran 5m ago" (elapsed since <epoch>)
# @in:<epoch>@ -> "next in 20m", or "next due" once <epoch> has passed
# @ago:<epoch>@ -> "5m ago" (elapsed since <epoch>)
# @in:<epoch>@ -> "in 20m", or "due" once <epoch> has passed
# Tokens expand to a bare duration phrase; the collectors own the wording
# around them (e.g. "ran @ago:…@", "next @in:…@", "checked @ago:…@").
# Unknown or malformed tokens are left untouched.
_render() {
local line="${1}" now="${2}" tail="" pre kind epoch token
Expand All @@ -103,12 +106,12 @@ _render() {
token=""
if [ "${kind}" = "ago" ]; then
_fmt_duration "$((now - epoch))"
token="ran ${REL_DURATION} ago"
token="${REL_DURATION} ago"
elif [ "${epoch}" -gt "${now}" ]; then
_fmt_duration "$((epoch - now))"
token="next in ${REL_DURATION}"
token="in ${REL_DURATION}"
else
token="next due"
token="due"
fi
tail="${token}${BASH_REMATCH[4]}${tail}"
line="${pre}"
Expand Down Expand Up @@ -194,7 +197,13 @@ _collect_updates() {
count="${count//[!0-9]/}"
[ -z "${count}" ] && count=0
if [ "${count}" -gt 0 ]; then
printf '\360\237\223\246 %s update(s) available (%s)\n' "${count}" "${mgr}"
# The "checked" suffix is an absolute epoch token so it keeps counting
# up between refreshes instead of freezing at the value it had when
# the cache was written.
local checked_at
checked_at="$(_now)"
printf '\360\237\223\246 %s update(s) available (%s) \302\267 checked @ago:%s@\n' \
"${count}" "${mgr}" "${checked_at}"
fi
}

Expand Down Expand Up @@ -273,7 +282,7 @@ _collect_ansible() {
if [ -n "${finished_ts}" ]; then
local finished_epoch
finished_epoch="$(date -d "${finished_ts}" +%s 2>/dev/null || echo 0)"
[ "${finished_epoch}" -gt 0 ] && ran="@ago:${finished_epoch}@"
[ "${finished_epoch}" -gt 0 ] && ran="ran @ago:${finished_epoch}@"
fi

# Next scheduled run from the timer (microseconds since epoch).
Expand All @@ -282,7 +291,7 @@ _collect_ansible() {
next_us="${next_us//[!0-9]/}"
if [ -n "${next_us}" ] && [ "${next_us}" -gt 0 ]; then
next_epoch=$((next_us / 1000000))
[ "${next_epoch}" -gt "${now}" ] && next="@in:${next_epoch}@"
[ "${next_epoch}" -gt "${now}" ] && next="next @in:${next_epoch}@"
fi

local head tail="" part
Expand Down
70 changes: 64 additions & 6 deletions home/dot_config/fastfetch/status.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
* The PowerShell profile spawns a detached 'refresh' when the cache is
older than the TTL, so the next login shows fresh numbers.

Each section is stored twice: '<name>.src' keeps relative times as absolute
epoch tokens (@ago:<epoch>@), and '<name>' is the rendered copy fastfetch
reads. Baking "checked 5m ago" straight into the rendered file would freeze
it until the next refresh an hour later, so the tokens are expanded on every
shell start by Update-FastfetchStatusCache (DotfilesHelpers), which the
profile already imports. This script calls the same function after a refresh
so a manual 'refresh' also leaves a correctly rendered cache behind.

fastfetch hides a `command` module entirely when it prints nothing, so a
host with no pending updates simply does not get an "Updates" line.

Expand Down Expand Up @@ -59,10 +67,12 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

# U+1F4E6 PACKAGE. Built from its code point because .ps1 files in this repo
# must stay ASCII (see .github/copilot-instructions.md): PowerShell 5.1 reads
# unsigned scripts without a BOM as ANSI, which corrupts literal emoji.
# U+1F4E6 PACKAGE and U+00B7 MIDDLE DOT. Built from their code points because
# .ps1 files in this repo must stay ASCII (see .github/copilot-instructions.md):
# PowerShell 5.1 reads unsigned scripts without a BOM as ANSI, which corrupts
# literal non-ASCII characters.
$script:PackageIcon = [char]::ConvertFromUtf32(0x1F4E6)
$script:MiddleDot = [char]::ConvertFromUtf32(0x00B7)

function Get-StatusCacheDir {
if ($env:FASTFETCH_STATUS_CACHE_DIR) { return $env:FASTFETCH_STATUS_CACHE_DIR }
Expand Down Expand Up @@ -171,31 +181,46 @@ function Get-UpdatesLine {
<#
.SYNOPSIS
The "N update(s) available (winget)" line, or an empty string.

.DESCRIPTION
The "checked" suffix is emitted as an absolute epoch token rather than a
formatted duration, so it keeps counting up on every shell start instead of
freezing at the value it had when the cache was written.
#>
[CmdletBinding()]
param()

$count = Get-WingetUpdateCount
if ($null -eq $count -or $count -le 0) { return '' }
return ('{0} {1} update(s) available (winget)' -f $script:PackageIcon, $count)

$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
return ('{0} {1} update(s) available (winget) {2} checked @ago:{3}@' -f
$script:PackageIcon, $count, $script:MiddleDot, $now)
}

function Write-StatusSection {
<#
.SYNOPSIS
Atomically write a cache section, or remove it when the value is empty.
Atomically write a cache section source, or remove it when the value is empty.

.DESCRIPTION
Writes '<name>.src' (with relative-time tokens intact). The rendered
'<name>' file that fastfetch reads is produced from it by
Update-FastfetchStatusCache.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][AllowEmptyString()][string]$Value
)

$file = Join-Path (Get-StatusCacheDir) $Name
$cacheDir = Get-StatusCacheDir
$file = Join-Path $cacheDir "$Name.src"
if (-not $PSCmdlet.ShouldProcess($file, 'Write status section')) { return }

if ([string]::IsNullOrEmpty($Value)) {
Remove-Item -LiteralPath $file -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath (Join-Path $cacheDir $Name) -Force -ErrorAction SilentlyContinue
return
}

Expand All @@ -207,6 +232,38 @@ function Write-StatusSection {
Move-Item -LiteralPath $tmp -Destination $file -Force
}

function Invoke-StatusRender {
<#
.SYNOPSIS
Expand the cached tokens into the rendered files fastfetch reads.

.DESCRIPTION
Rendering lives in the DotfilesHelpers module because the PowerShell profile
imports it anyway, so expanding tokens on every shell start is free there
(dot-sourcing this script instead would add ~60ms to profile load). This
script imports the module so a manual 'refresh' also leaves a correctly
rendered cache behind; when the module is missing the rendered files are
simply left to the next shell start.
#>
[CmdletBinding()]
param()

try {
if (-not (Get-Command Update-FastfetchStatusCache -ErrorAction SilentlyContinue)) {
$modulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'powershell\modules\DotfilesHelpers'
if (-not (Test-Path -LiteralPath $modulePath)) {
Write-Verbose "DotfilesHelpers not found at '$modulePath'; skipping render."
return
}
Import-Module $modulePath -DisableNameChecking -ErrorAction Stop
}
Update-FastfetchStatusCache -SkipRefresh
}
catch {
Write-Verbose "Could not render the status cache: $($_.Exception.Message)"
}
}

function Invoke-StatusEmit {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Name)
Expand Down Expand Up @@ -264,6 +321,7 @@ function Invoke-StatusRefresh {
Write-StatusSection -Name 'updates' -Value (Get-UpdatesLine)
$stamp = Join-Path $cacheDir '.refreshed-at'
[System.IO.File]::WriteAllText($stamp, '')
Invoke-StatusRender
}
finally {
$handle.Dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@
# Windows Terminal settings
'Set-WindowsTerminalDefaultProfile'
'Set-WindowsTerminalCopilotProfile'
'Set-WindowsTerminalProfileCommandLine'

# fastfetch status cache
'Get-FastfetchStatusCacheDir'
'Format-FastfetchStatusDuration'
'Expand-FastfetchStatusToken'
'Update-FastfetchStatusCache'
'Test-FastfetchStatusStale'
'Start-FastfetchStatusRefresh'
'Write-FastfetchStatusFile'

# Office 365 mail aliases
'New-MailAlias'
Expand Down
Loading