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
28 changes: 28 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,34 @@ so the noise around it is turned off rather than printed twice:
are skipped whenever the fastfetch banner rendered. On a light install without
fastfetch they stay, so an interactive shell still confirms the profile loaded.

## Projects folder and Dev Drive

Interactive shells `cd` into your projects folder at startup (unless the shell
was launched from VS Code, or you're already somewhere under a path containing
`projects`).

On Windows the folder is resolved by `Get-ProjectsPath` in the order below, so a
[Dev Drive](https://learn.microsoft.com/en-us/windows/dev-drive/) — a ReFS
volume tuned for developer workloads — wins over the user profile:

1. `$env:PROJECTS_PATH`, when set.
2. `<Dev Drive>\projects`, when a Dev Drive has one (for example `D:\projects`).
3. `%USERPROFILE%\projects`.

`run_once_before_00-setup.ps1` creates the folder on the Dev Drive when one is
present, so a fresh machine with a Dev Drive gets `D:\projects` instead of
`%USERPROFILE%\projects`.

!!! note "How the Dev Drive is detected"
`fsutil devdrv query` is the authoritative check, but it needs an elevated
shell — unusable from a profile. Fixed, ready ReFS volumes are used as the
heuristic instead. If that guesses wrong (a plain ReFS data volume, or
several Dev Drives), pin the right one with `$env:DEV_DRIVE`, or skip
detection entirely with `$env:PROJECTS_PATH`.

The Linux/macOS shell configs (`fish`, `bash`, `zsh`) always use
`$HOME/projects`; Dev Drive is a Windows-only feature.

## Windows PATH

On Windows, the setup adds `%OneDrive%\Portable Programs` to the user-scope
Expand Down
6 changes: 6 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ the dotfiles.
cd dotfiles
```

!!! tip "Using a Dev Drive"
If you have a [Dev Drive](https://learn.microsoft.com/en-us/windows/dev-drive/),
clone into `<Dev Drive>\projects` instead (for example `D:\projects`) —
that's the folder the PowerShell profile will `cd` into afterwards. See
[Projects folder and Dev Drive](customization.md#projects-folder-and-dev-drive).

To use SSH afterwards (e.g. with the 1Password SSH agent), install
1Password and enable the agent, then switch the remote to SSH:

Expand Down
26 changes: 25 additions & 1 deletion home/.chezmoiscripts/windows/run_once_before_00-setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,34 @@ $ErrorActionPreference = "Stop"

Write-Host ">> Running initial Windows setup..." -ForegroundColor Cyan

# Prefer a Dev Drive (fixed ReFS volume) over the user profile for source code.
# "fsutil devdrv query" needs elevation, so fixed ReFS volumes are the heuristic.
# Set DEV_DRIVE to pin a specific volume.
$devDriveRoot = $null

if (-not [string]::IsNullOrWhiteSpace($env:DEV_DRIVE) -and (Test-Path -LiteralPath $env:DEV_DRIVE.Trim().Trim('"'))) {
$devDriveRoot = $env:DEV_DRIVE.Trim().Trim('"')
}
else {
$devDriveRoot = [System.IO.DriveInfo]::GetDrives() |
Where-Object { $_.IsReady -and $_.DriveType -eq [System.IO.DriveType]::Fixed -and $_.DriveFormat -eq 'ReFS' } |
Sort-Object Name |
Select-Object -First 1 -ExpandProperty RootDirectory |
Select-Object -ExpandProperty FullName
}

$projectsPath = if ($devDriveRoot) {
Write-Host "Dev Drive detected: $devDriveRoot" -ForegroundColor Cyan
Join-Path $devDriveRoot "projects"
}
else {
Join-Path $env:USERPROFILE "projects"
}

# Create necessary directories
$directories = @(
"$env:USERPROFILE\bin",
"$env:USERPROFILE\projects"
$projectsPath
)

foreach ($dir in $directories) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
'Set-LocationUp'
'Set-LocationUpUp'

# Dev Drive / projects folder
'Test-DevDriveSupported'
'Get-DevDrivePath'
'Get-ProjectsPath'

# System utilities
'which'
'touch'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Dev Drive detection and projects folder resolution
#
# A Dev Drive is a Windows 11 ReFS volume tuned for developer workloads. It is
# a much better home for source code than the user profile (which is usually on
# the NTFS system drive and covered by real-time antivirus scanning).
#
# Note: "fsutil devdrv query" is the authoritative check, but it requires an
# elevated shell, so it can't be used from a profile. Fixed ReFS volumes are
# used as the heuristic instead - on a normal workstation those are Dev Drives.
# Set $env:DEV_DRIVE to pin a specific volume, or $env:PROJECTS_PATH to pin the
# projects folder outright.

function Test-DevDriveSupported {
<#
.SYNOPSIS
Returns $true when running on Windows, where Dev Drives exist.
#>
[CmdletBinding()]
[OutputType([bool])]
param()

if ($PSVersionTable.PSEdition -eq 'Desktop') {
return $true
}

return [bool](Get-Variable -Name IsWindows -ValueOnly -ErrorAction SilentlyContinue)
}

function Get-DevDrivePath {
<#
.SYNOPSIS
Returns the root paths of the Dev Drives on this machine.

.DESCRIPTION
Honours $env:DEV_DRIVE when set (and the path exists). Otherwise returns
every fixed, ready ReFS volume, sorted by drive letter. Returns an empty
array when no Dev Drive is present or the platform isn't Windows.

.EXAMPLE
Get-DevDrivePath
D:\
#>
[CmdletBinding()]
[OutputType([string[]])]
param()

if (-not [string]::IsNullOrWhiteSpace($env:DEV_DRIVE)) {
$override = $env:DEV_DRIVE.Trim().Trim('"')
if (Test-Path -LiteralPath $override) {
return @($override)
}

Write-Verbose "DEV_DRIVE is set to '$override' but that path does not exist; ignoring it."
}

if (-not (Test-DevDriveSupported)) {
return @()
}

$roots = @()

try {
foreach ($drive in [System.IO.DriveInfo]::GetDrives()) {
if (-not $drive.IsReady) { continue }
if ($drive.DriveType -ne [System.IO.DriveType]::Fixed) { continue }
if ($drive.DriveFormat -ne 'ReFS') { continue }

$roots += $drive.RootDirectory.FullName
}
}
catch {
Write-Verbose "Failed to enumerate drives: $_"
return @()
}

return @($roots | Sort-Object)
}

function Get-ProjectsPath {
<#
.SYNOPSIS
Resolves the folder that holds local source code checkouts.

.DESCRIPTION
Resolution order:
1. $env:PROJECTS_PATH, when set.
2. <Dev Drive>\projects, when a Dev Drive has one.
3. $env:USERPROFILE\projects (or $HOME/projects off Windows).

Without -CreateIfMissing the function never touches the file system, so
it is safe to call from the PowerShell profile on every startup.

.PARAMETER CreateIfMissing
Create the resolved folder when it does not exist yet. A Dev Drive is
preferred over the user profile when creating.

.EXAMPLE
Get-ProjectsPath
D:\projects
#>
[CmdletBinding(SupportsShouldProcess)]
[OutputType([string])]
param(
[switch]$CreateIfMissing
)

$userHome = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME }
$fallback = Join-Path $userHome 'projects'

if (-not [string]::IsNullOrWhiteSpace($env:PROJECTS_PATH)) {
$explicit = $env:PROJECTS_PATH.Trim().Trim('"')

if ($CreateIfMissing -and -not (Test-Path -LiteralPath $explicit)) {
if ($PSCmdlet.ShouldProcess($explicit, 'Create projects directory')) {
New-Item -ItemType Directory -Path $explicit -Force | Out-Null
}
}

return $explicit
}

$devDrives = @(Get-DevDrivePath)

foreach ($root in $devDrives) {
$candidate = Join-Path $root 'projects'
if (Test-Path -LiteralPath $candidate) {
return $candidate
}
}

if ($CreateIfMissing) {
$target = if ($devDrives.Count -gt 0) { Join-Path $devDrives[0] 'projects' } else { $fallback }

if (-not (Test-Path -LiteralPath $target)) {
if ($PSCmdlet.ShouldProcess($target, 'Create projects directory')) {
New-Item -ItemType Directory -Path $target -Force | Out-Null
}
}

return $target
}

return $fallback
}
20 changes: 13 additions & 7 deletions home/dot_config/powershell/profile.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,23 @@ if ($env:OneDrive) {
}
}

# Load DotfilesHelpers module (lazy-loadable via PSModulePath, explicit import for profile)
$dotfilesModulePath = Join-Path $PSScriptRoot "modules\DotfilesHelpers"
if (Test-Path $dotfilesModulePath) {
Import-Module $dotfilesModulePath -DisableNameChecking
}

# Set working directory to projects folder if not already there
# Get-ProjectsPath prefers a Dev Drive over $env:USERPROFILE when one exists
# Skip this if running in VS Code to preserve the opened folder location
if ($ENV:TERM_PROGRAM -ne "vscode") {
$currentPath = (Get-Location).Path
$projectsPath = Join-Path $env:USERPROFILE "projects"
$projectsPath = if (Get-Command Get-ProjectsPath -ErrorAction SilentlyContinue) {
Get-ProjectsPath
}
else {
Join-Path $env:USERPROFILE "projects"
}

# Check if current path contains 'projects' (case-insensitive)
if ($currentPath -notlike "*projects*") {
Expand All @@ -66,12 +78,6 @@ if ($ENV:TERM_PROGRAM -ne "vscode") {
}
}

# Load DotfilesHelpers module (lazy-loadable via PSModulePath, explicit import for profile)
$dotfilesModulePath = Join-Path $PSScriptRoot "modules\DotfilesHelpers"
if (Test-Path $dotfilesModulePath) {
Import-Module $dotfilesModulePath -DisableNameChecking
}

# Load aliases
. $PSScriptRoot\aliases.ps1

Expand Down
Loading