scripts/win/system/note.ps1
# note.ps1 — BORG Notes (simple, title=filename, fzf picker) # Storage: %APPDATA%\Borg\notes # Commands: # borg note add "<title>" "<description>" # borg note search "<query>" # fzf picker; Enter -> print description # borg note show # fzf picker; Enter -> print description # borg note show "<title>" # direct by title; prints description # borg note edit "<title>" # open in editor # borg note rm "<title>" # delete note param( [Parameter(Mandatory, Position=0)] [ValidateSet('add','search','show','edit','rm')] [string]$Action, [Parameter(Position=1)] [string]$Arg1, # title | query [Parameter(Position=2)] [string]$Arg2 # description for add ) # ---------------- Paths & init ---------------- $NotesRoot = Join-Path $env:APPDATA "Borg\notes" function Initialize-Notes { if (-not (Test-Path $NotesRoot)) { New-Item -ItemType Directory -Path $NotesRoot -Force | Out-Null } } # ---------------- Utilities ---------------- function Sanitize-Title([string]$Title) { if ([string]::IsNullOrWhiteSpace($Title)) { return $null } $clean = $Title.Trim() # Replace invalid filename chars with underscore [char[]]$bad = [IO.Path]::GetInvalidFileNameChars() foreach ($c in $bad) { $clean = $clean.Replace($c, '_') } # Collapse whitespace $clean = ($clean -replace '\s+', ' ').Trim() return $clean } function Title-ToPath([string]$Title) { $clean = Sanitize-Title $Title if (-not $clean) { return $null } Join-Path $NotesRoot "$clean.md" } function Read-FrontMatter([string[]]$Lines) { $meta = [ordered]@{} if ($Lines.Count -ge 3 -and $Lines[0].Trim() -eq '---') { $end = $null for ($i=1; $i -lt $Lines.Count; $i++) { if ($Lines[$i].Trim() -eq '---') { $end = $i; break } } if ($end) { for ($j=1; $j -lt $end; $j++) { if ($Lines[$j] -match '^\s*([^:]+):\s*(.*)$') { $k = $matches[1].Trim(); $v = $matches[2].Trim() $meta[$k] = $v } } } } $meta } function Get-NoteBody([string[]]$Lines) { if ($Lines.Count -ge 3 -and $Lines[0].Trim() -eq '---') { $end = $null for ($i=1; $i -lt $Lines.Count; $i++) { if ($Lines[$i].Trim() -eq '---') { $end = $i; break } } if ($end -ne $null -and $end + 1 -lt $Lines.Count) { return ($Lines[($end+1)..($Lines.Count-1)] -join "`n").Trim() } } ($Lines -join "`n").Trim() } function Load-Note([string]$Path) { if (-not (Test-Path -LiteralPath $Path)) { return $null } $lines = Get-Content -LiteralPath $Path $meta = Read-FrontMatter $lines $body = Get-NoteBody $lines [pscustomobject]@{ Title = if ($meta['Title']) { $meta['Title'] } else { [IO.Path]::GetFileNameWithoutExtension($Path) } Created = $meta['CreatedUtc'] Updated = $meta['UpdatedUtc'] Path = $Path Body = $body } } function Get-AllNotes { Initialize-Notes Get-ChildItem -LiteralPath $NotesRoot -Filter '*.md' -File | ForEach-Object { Load-Note $_.FullName } | Where-Object { $_ -ne $null } } function Require-Fzf { if (-not (Get-Command fzf -ErrorAction SilentlyContinue)) { throw "fzf is required for interactive selection. Install it (winget install fzf) or use direct commands." } } function Pick-Note([pscustomobject[]]$Items, [string]$Prompt='notes> ') { Require-Fzf if (-not $Items -or $Items.Count -eq 0) { Write-Host 'No notes.'; return $null } # Build tab-separated lines: Title<TAB>Created<TAB>Path $lines = $Items | ForEach-Object { "{0}`t{1}`t{2}" -f $_.Title, $_.Created, $_.Path } $fzfArgs = @( '--delimiter', "`t", '--with-nth=1,2', '--prompt', $Prompt, '--height', '80%', '--layout', 'reverse' ) $selection = $lines | & fzf @fzfArgs if ([string]::IsNullOrWhiteSpace($selection)) { return $null } $parts = $selection -split "`t", 3 $selPath = $parts[2] return ($Items | Where-Object { $_.Path -eq $selPath } | Select-Object -First 1) } # ---------------- Core ops ---------------- function Note-Add([string]$Title, [string]$Description) { $Title = Sanitize-Title $Title if (-not $Title) { throw 'Missing title. Usage: borg note add "<title>" "<description>"' } if ($null -eq $Description) { $Description = '' } Initialize-Notes $path = Title-ToPath $Title if (Test-Path -LiteralPath $path) { throw "A note with this title already exists: '$Title'" } $created = (Get-Date -AsUTC).ToString('o') $content = @( '---' "Title: $Title" "CreatedUtc: $created" "UpdatedUtc: $created" '---' '' $Description ) -join "`n" Set-Content -LiteralPath $path -Value $content -NoNewline -Encoding UTF8 [pscustomobject]@{ Title=$Title; Path=$path } } function Note-Show([string]$TitleKey) { if ([string]::IsNullOrWhiteSpace($TitleKey)) { # Interactive picker of all notes $picked = Pick-Note (Get-AllNotes) 'show> ' if ($null -ne $picked) { $picked.Body } return } # Direct by title: exact filename match after sanitization $path = Title-ToPath $TitleKey if (-not (Test-Path -LiteralPath $path)) { # fallback: try case-insensitive title match across all notes $matches = Get-AllNotes | Where-Object { $_.Title -ieq $TitleKey -or $_.Title -ilike "*$TitleKey*" } if ($matches.Count -gt 1) { $picked = Pick-Note $matches 'show> ' if ($null -ne $picked) { $picked.Body } return } elseif ($matches.Count -eq 1) { $path = $matches[0].Path } else { throw "Note not found by title: '$TitleKey'" } } $note = Load-Note $path if ($null -eq $note) { throw "Note not found: '$TitleKey'" } $note.Body } function Note-Edit([string]$TitleKey) { if ([string]::IsNullOrWhiteSpace($TitleKey)) { throw 'Usage: borg note edit "<title>"' } $path = Title-ToPath $TitleKey if (-not (Test-Path -LiteralPath $path)) { throw "Note not found by title: '$TitleKey'" } $editor = if ($env:EDITOR) { $env:EDITOR } elseif (Get-Command 'code' -ErrorAction SilentlyContinue) { 'code' } elseif (Get-Command 'micro' -ErrorAction SilentlyContinue) { 'micro' } else { 'notepad.exe' } # IMPORTANT: quote the path so spaces are preserved as a single argument $qpath = '"{0}"' -f $path if ($editor -ieq 'code') { Start-Process -FilePath 'code' -ArgumentList @('-g', $qpath) | Out-Null } elseif ($editor -ieq 'micro') { Start-Process -FilePath 'micro' -ArgumentList @($qpath) | Out-Null } elseif ($editor -ieq 'notepad.exe') { Start-Process -FilePath 'notepad.exe' -ArgumentList $qpath | Out-Null } else { # generic editor: still pass the quoted path as a single arg Start-Process -FilePath $editor -ArgumentList $qpath | Out-Null } } function Note-Rm([string]$TitleKey) { if ([string]::IsNullOrWhiteSpace($TitleKey)) { throw 'Usage: borg note rm "<title>"' } $path = Title-ToPath $TitleKey if (-not (Test-Path -LiteralPath $path)) { throw "Note not found by title: '$TitleKey'" } Remove-Item -LiteralPath $path -Force "Removed note '$TitleKey'" } function Note-Search([string]$Query) { Initialize-Notes $all = Get-AllNotes $matches = if ([string]::IsNullOrWhiteSpace($Query)) { $all } else { $all | Where-Object { $_.Title -match [regex]::Escape($Query) -or $_.Body -match $Query } } if (-not $matches -or $matches.Count -eq 0) { Write-Host "No matches."; return } $picked = Pick-Note $matches 'search> ' if ($null -ne $picked) { $picked.Body } } # ---------------- Dispatcher ---------------- switch ($Action) { 'add' { Note-Add -Title $Arg1 -Description $Arg2 } 'search' { Note-Search -Query $Arg1 } 'show' { Note-Show -TitleKey $Arg1 } 'edit' { Note-Edit -TitleKey $Arg1 } 'rm' { Note-Rm -TitleKey $Arg1 } } |