Back to all articles
case-study
👀

Building Peek

A PowerShell module that wraps Get-ChildItem with human-readable sizes, relative timestamps, and emoji icons.

April 20, 2025 4 min read
Share:
CW

Cody Williamson

Senior Software Engineer

Why I Built This

PowerShell’s Get-ChildItem works, but the output isn’t great to look at. I wanted something more like exa or lsd in the Unix world—emoji icons for file types, sizes in KB/MB/GB instead of raw bytes, and timestamps shown as “5d ago” instead of “2025-04-15 14:32:01”. I also wanted to sort directories first and have short aliases for common operations.

Module Architecture

Peek follows a simple, single-file module structure optimized for easy distribution:

pwsh-peek/
├── DirectoryListing.psd1    # Module manifest (metadata & exports)
├── DirectoryListing.psm1    # All functions in one file
├── README.md
└── pwsh-peek/               # Marketing website (Vue 3 + Vite)
    └── public/
        └── install.ps1      # Web installer script

The module is intentionally flat—no Public/ or Private/ folders. This keeps installation simple (just download two files) while still maintaining clean separation through function scoping.

Core Implementation

Human-Readable Sizes

Instead of showing 1073741824 bytes, Peek converts to friendly units:

function Convert-ToHumanSize {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [long]$Bytes
    )
    if ($Bytes -lt 1KB) { return "$Bytes B" }
    $sizes = @('KB', 'MB', 'GB', 'TB', 'PB')
    $val = [double]$Bytes
    $i = 0
    while ($val -ge 1024 -and $i -lt $sizes.Count) {
        $val = $val / 1024
        $i++
    }
    '{0:N1} {1}' -f $val, $sizes[$i - 1]
}

Relative Timestamps

Dates like “2025-04-15 14:32:01” become “5d ago”:

function Convert-ToRelativeTime {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [datetime]$DateTime
    )
    $span = (Get-Date) - $DateTime
    if ($span.TotalSeconds -lt 60) { return "{0:N0}s ago" -f $span.TotalSeconds }
    if ($span.TotalMinutes -lt 60) { return "{0:N0}m ago" -f $span.TotalMinutes }
    if ($span.TotalHours -lt 24)   { return "{0:N0}h ago" -f $span.TotalHours }
    if ($span.TotalDays -lt 7)     { return "{0:N0}d ago" -f $span.TotalDays }
    if ($span.TotalDays -lt 30)    { return "{0:N0}w ago" -f ($span.TotalDays / 7) }
    if ($span.TotalDays -lt 365)   { return "{0:N0}mo ago" -f ($span.TotalDays / 30) }
    return "{0:N0}y ago" -f ($span.TotalDays / 365)
}

The Main Command: Get-DirectoryView

The core Get-DirectoryView function wraps Get-ChildItem and emits custom PSObjects:

function Get-DirectoryView {
    [CmdletBinding()]
    param(
        [string]$Path = '.',
        [int]$Depth = 1,
        [switch]$Recurse,
        [switch]$All,
        [switch]$Long,
        [switch]$DirsFirst,
        [switch]$SortNewest,
        [switch]$SortSize,
        [switch]$OnlyFiles,
        [switch]$OnlyDirs,
        [switch]$NoIcons,
        [switch]$Raw
    )
    # ... builds Get-ChildItem params, processes items, outputs formatted table
}

Each item is transformed into a consistent shape:

[PSCustomObject]@{
    Icon     = $icon      # 📁, 📄, or 🔗 (or D/F/L in ASCII mode)
    Name     = $it.Name
    Type     = $type      # Dir, File, or Link
    Size     = $size      # Human-readable or '-' for directories
    Modified = $when      # Relative timestamp
    Mode     = $it.Mode   # Only in -Long mode
    FullName = $it.FullName
}

Smart Sorting with Directory-First

The -DirsFirst flag uses compound Sort-Object expressions to group directories before files while preserving secondary sort order:

if ($DirsFirst) {
    $items = $items | Sort-Object @{
        Expression = { $_.PSIsContainer -eq $false }
        Descending = $true
    }, @{
        Expression = { $_.LastWriteTime }
        Descending = $true
    }, Name
}

Convenience Wrappers & Aliases

Rather than typing peek -All -DirsFirst -SortNewest every time, the module provides wrapper functions with memorable aliases:

CommandAliasShortDescription
Get-DirectoryViewpeekBase listing
Get-PeekAllpeek-allpkaInclude hidden/system items
Get-PeekAllRecursepeek-all-recursepkarRecurse with depth
Get-PeekFilespeek-filespkfFiles only
Get-PeekDirspeek-dirspkdDirectories only
Get-PeekAllSizepeek-all-sizepkasAll items, largest first
Get-PeekAllNewestpeek-all-newestpkanAll items, newest first

Handling Terminals Without Emoji Support

Not every terminal can display emoji. Peek has a few ways to handle this: you can pass -NoIcons (or -Ascii) on the command line, set an environment variable $env:PEEK_NO_ICONS = '1', or use Set-NoIconsForPeek -Scope User to save it to a config file. The module checks these in order: command-line first, then environment, then config file, then falls back to showing icons by default.

function Get-PeekPreference {
    $noIcons = $false
    $source = 'Default'

    if ($env:PEEK_NO_ICONS) {
        $noIcons = ($env:PEEK_NO_ICONS -ne '0')
        $source = 'Env'
    }
    else {
        $cfgPath = Get-PeekConfigPath
        if (Test-Path $cfgPath) {
            $json = Get-Content $cfgPath -Raw | ConvertFrom-Json
            if ($null -ne $json.NoIcons) {
                $noIcons = [bool]$json.NoIcons
                $source = 'Config'
            }
        }
    }
    [PSCustomObject]@{ NoIcons = $noIcons; Source = $source }
}

In ASCII mode, icons become simple letters: D for directory, F for file, L for link.

Installation

The module can be installed with a one-liner:

iex (irm peek.codywilliamson.com/install.ps1)

The installer downloads the module files from GitHub and puts them in your PowerShell modules folder. It supports -Force to overwrite existing installations and -AddToProfile to auto-import on shell startup.

Module Manifest

The .psd1 manifest declares all exports explicitly:

@{
    RootModule        = '.\DirectoryListing.psm1'
    ModuleVersion     = '1.0.0'
    PowerShellVersion = '7.0'
    FunctionsToExport = @(
        'Get-DirectoryView',
        'Get-PeekAll',
        'Get-PeekAllRecurse',
        'Get-PeekFiles',
        'Get-PeekDirs',
        'Get-PeekAllSize',
        'Get-PeekAllNewest',
        'Get-PeekPreference',
        'Set-NoIconsForPeek'
    )
    AliasesToExport   = @(
        'peek', 'peek-all', 'peek-all-recurse', 'peek-files',
        'peek-dirs', 'peek-all-size', 'peek-all-newest',
        'pka', 'pkar', 'pkf', 'pkd', 'pkas', 'pkan'
    )
}

What I Learned

A few things stood out while building this. Keeping the module as a single .psm1 file made distribution much simpler—you just download two files and you’re done. Short aliases like pka and pkf make the tool feel more natural to use once you’ve memorized them. And making the output consistent (always the same PSObject shape) means the results are predictable and work well with piping.

I use Peek daily now. It’s part of how I navigate projects.


Links:

Enjoyed this article? Share it with others!

Share:

Have a project in mind?

Whether you need full-stack development, cloud architecture consulting, or custom solutions—let's talk about how I can help bring your ideas to life.