PSMacToolkit.psm1

<#

PSMacToolkit

Copyright (C) 2025 Vincent Anso

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

#>


#Requires -Version 7.2

using module ".\PSMacToolkitLib.psm1"

if ( -Not $IsMacOS )
{
    Write-Warning "This module only runs on macOS."
    exit 0
}
else 
{
    [SPLocalization]::new() | Out-Null
}

if (Get-Module -ListAvailable | Where-Object Name -eq PowerShellOSA)
{
    Import-Module PowerShellOSA
}
else 
{
    Write-Warning "The PowerShellOSA module must be installed to use PSMacToolkit."
    exit 0
}

function Get-SystemInfo 
{
    <#
    
    .SYNOPSIS
    Get information about the system.

    #>

    
    Invoke-OSA "system info"
}

function Get-InfoFor 
{
    <#
    
    .SYNOPSIS
    Return information for a file or folder.

    #>


    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, Mandatory = $true)]
        [ValidateScript({ Test-Path -LiteralPath $_ })]
        [Alias("Path")]
        [uri]$DirectParameter,
        [switch]$Size
    )

    Invoke-OSA (New-AppleScriptCommand "info for" $PSBoundParameters) 
}

function Get-ListDisks
{
    <#
    
    .SYNOPSIS
    Return a list of the currently mounted volumes.

    #>

    
    Invoke-OSA "list disks"
}

function Get-ListFolder 
{
    <#
    
    .SYNOPSIS
        Return the contents of a specified folder.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [Alias("Path")]
        [Uri]$DirectParameter,
        [switch]$Invisibles
    )

    Invoke-OSA (New-AppleScriptCommand "list folder" $PSBoundParameters)
}

function Get-ClipboardInfo 
{
    <#
    
    .SYNOPSIS
    Return information about the clipboard.

    #>

    
    param (
       [string]$For
    )

    Invoke-OSA (New-AppleScriptCommand "clipboard info" $PSBoundParameters)
}

function Get-MacClipboard 
{
    <#
    
    .SYNOPSIS
    Return the contents of an application’s clipboard.

    #>

    
    param (
       [string]$ForApplication
    )

    if ($PSBoundParameters.ContainsKey("ForApplication"))
    {
        /usr/bin/osascript -e "tell application `"$ForApplication`" to activate"
    }

    Invoke-OSA (New-AppleScriptCommand "the clipboard" $PSBoundParameters @("ForApplication"))
}

function Set-MacClipboard 
{
    <#
    
    .SYNOPSIS
    Place data on an application’s clipboard.

    #>


    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias("Value")]
        $DirectParameter,
        [string]$ForApplication
    )

    if ($PSBoundParameters.ContainsKey("ForApplication"))
    {
        /usr/bin/osascript -e "tell application `"$ForApplication`" to activate"
    }
    
    Invoke-OSA (New-AppleScriptCommand "set the clipboard to" $PSBoundParameters @("ForApplication"))
    
}

function Get-VolumeSettings
{
    <#
    
    .SYNOPSIS
    Get the sound output and input volume settings.

    #>

    
    Invoke-OSA "get volume settings"
}

function Set-Volume
{
    <#
    
    .SYNOPSIS
    Set the sound output and/or input volume.

    #>

    
    [CmdletBinding(DefaultParameterSetName = "VolumeDeprecated")]
    param(
        [Parameter(ParameterSetName = 'VolumeDeprecated')]
        [ValidateRange(0, 7)]
        [Parameter(Position = 0)]
        [Byte]$DirectParameter,
        [Parameter(ParameterSetName = 'Volume')]
        [ValidateRange(0, 100)]
        [Byte]$OutputVolume,
        [Parameter(ParameterSetName = 'Volume')]
        [ValidateRange(0, 100)]
        [Byte]$InputVolume,
        [Parameter(ParameterSetName = 'Volume')]
        [ValidateRange(0, 100)]
        [Byte]$AlertVolume,
        [Parameter(ParameterSetName = 'Volume')]
        [Boolean]$OutputMuted
    )

    Invoke-OSA (New-AppleScriptCommand "set volume" $PSBoundParameters)
}

function Get-SystemAttribute 
{
    <#
    
    .SYNOPSIS
    Test attributes of this computer.

    #>

    
    param (
        [string]$DirectParameter,
        [int]$Has
    )
    
    Invoke-OSA (New-AppleScriptCommand "system attribute" $PSBoundParameters)
}


function Select-Color 
{
    <#
    
    .SYNOPSIS
    Choose a color.

    #>

    
    param (
        [array]$DefaultColor
    )
    
    Invoke-OSA (New-AppleScriptCommand "choose color" $PSBoundParameters)
}

enum EAlT {
    Critical
    Informational
    Warning
}

function Show-Alert
{
    <#
    
    .SYNOPSIS
    Display an alert.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [Alias("Text")]
        [string]$DirectParameter,
        [string]$Message,
        [EAlT]$As = "Informational",
        [Array]$Buttons,
        [string]$DefaultButton,
        [string]$CancelButton,
        [int]$GivingUpAfter
    )

    Invoke-OSA (New-AppleScriptCommand "display alert" $PSBoundParameters)
}

enum Stic {
    Stop
    Note
    Caution
}

function Show-Dialog
{
    <#
    
    .SYNOPSIS
    Display a dialog box, optionally requesting user input.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [Alias("Text")]
        [string]$DirectParameter,
        [string]$DefaultAnswer,
        [switch]$HiddenAnswer,
        [string[]]$Buttons,
        [string]$DefaultButton,
        [string]$CancelButton,
        [string]$WithTitle,
        [Stic]$WithIcon,
        [int]$GivingUpAfter,
        [switch]$AsSecureString
    )

    $result = Invoke-OSA (New-AppleScriptCommand "display dialog" $PSBoundParameters -IgnoreParameters @("AsSecureString"))

    if ($HiddenAnswer -and $AsSecureString)
    {
        if ($result.TextReturned)
        {
            $encryptedString = ConvertTo-SecureString $($result.TextReturned) -AsPlainText
            
            $result.TextReturned = $encryptedString
        }
    }

    $result
}

function Select-FromList 
{
    <#
    
    .SYNOPSIS
    Choose one or more items from a list.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]    
        [Alias("List")]
        [array]$DirectParameter,
        [string]$WithTitle,
        [string]$WithPrompt,
        [array]$DefaultItems,
        [string]$OkButtonName,
        [string]$CancelButtonName,
        [switch]$MultipleSelectionsAllowed,
        [switch]$EmptySelectionAllowed

    )
    
    Invoke-OSA (New-AppleScriptCommand "choose from list" $PSBoundParameters)
}


function Show-Notification
{
    <#
    
    .SYNOPSIS
    Display a notification. At least one of the body text and the title must be specified.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]    
        [Alias("Text")]
        [string]$DirectParameter,
        [string]$WithTitle,
        [string]$Subtitle,
        [string]$SoundName
    )
    
    Invoke-OSA (New-AppleScriptCommand "display notification" $PSBoundParameters)
}

function Delay 
{
    <#
    
    .SYNOPSIS
    Pause for a fixed amount of time.

    #>

    
    param (
        [Alias("Seconds")]
        [int]$DirectParameter
    )

    Invoke-OSA (New-AppleScriptCommand "delay" $PSBoundParameters)
}

function Invoke-Beep
{
    <#
    
    .SYNOPSIS
    Beep 1 or more times.

    #>

    
    param (
        [Alias("Number")]
        [int]$DirectParameter
    )

    Invoke-OSA (New-AppleScriptCommand "beep" $PSBoundParameters)
}

function Select-RemoteApplication 
{
    <#
    
    .SYNOPSIS
    Choose a running application on a remote machine or on this machine.

    #>

    
    param (
        [string]$WithTitle,
        [string]$WithPrompt
    )

    Invoke-OSA (New-AppleScriptCommand "choose remote application" $PSBoundParameters)
}

enum ChooseApplicationType {
   Application
   Alias
}

function Select-Application 
{
    <#
    
    .SYNOPSIS
    Choose an application on this machine or the network.

    #>

    
    param (
        [string]$WithTitle,
        [string]$WithPrompt,
        [switch]$MultipleSelectionsAllowed,
        [ChooseApplicationType]$As
    )
    
    Invoke-OSA (New-AppleScriptCommand "choose application" $PSBoundParameters)
}


function Select-File
{
    <#
    
    .SYNOPSIS
    Choose a file on a disk or server.

    #>

    
    param (
        [string]$WithPrompt,
        [array]$OfType,
        [ValidateScript({ Test-Path -LiteralPath $_ })]
        [Uri]$DefaultLocation,
        [switch]$Invisibles,
        [switch]$MultipleSelectionsAllowed,
        [switch]$ShowingPackageContents
    )
    
    Invoke-OSA (New-AppleScriptCommand "choose file" $PSBoundParameters)
}

function Select-Folder
{
    <#
    
    .SYNOPSIS
    Choose a folder on a disk or server.

    #>

    
    param (
        [string]$WithPrompt,
        [Uri]$DefaultLocation,
        [switch]$Invisibles,
        [switch]$MultipleSelectionsAllowed,
        [switch]$ShowingPackageContents
    )
    
    Invoke-OSA (New-AppleScriptCommand "choose folder" $PSBoundParameters)
}

enum ServerType {
    WebServers
    FTPServers
    TelnetHosts
    FileServers
    NewsServers
    DirectoryServices
    MediaServers
    RemoteApplications
}

function Select-URL 
{
    <#
    
    .SYNOPSIS
    Choose a service on the Internet.

    #>

    
    param (
        [ServerType]$Showing,
        [switch]$EditableUrl
    )
    
    Invoke-OSA (New-AppleScriptCommand "choose URL" $PSBoundParameters)
}

function Select-FileName 
{
    <#
    
    .SYNOPSIS
    Get a new file reference from the user, without creating the file.

    #>

    
    param (
        [string]$WithPrompt,
        [string]$DefaultName,
        [ValidateScript({ Test-Path -LiteralPath $_ })]
        [Uri]$DefaultLocation
    )

    Invoke-OSA (New-AppleScriptCommand "choose file name" $PSBoundParameters)
}

function Invoke-Say 
{
    <#
    
    .SYNOPSIS
    Speak the given text.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]    
        [Alias("Text")]
        [string]$DirectParameter,
        [string]$Displaying,
        [string]$Using,
        [int]$SpeakingRate,
        [ValidateRange(0, 127)]
        [byte]$Pitch,
        [ValidateRange(0, 127)]
        [byte]$Modulation,
        [ValidateRange(0, 1)]
        [double]$Volume,
        [switch]$StoppingCurrentSpeech,
        [switch]$WaitingUntilCompletion,
        [Uri]$SavingTo
    )

    Invoke-OSA (New-AppleScriptCommand "say" $PSBoundParameters)
}

function Mount-Volume 
{
    <#
    
    .SYNOPSIS
    Mount the specified server volume.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]    
        [Alias("Path")]
        [string]$DirectParameter,
        [string]$OnServer,
        [string]$InAppletalkZone,
        [string]$AsUserName,
        [securestring]$WithPassword
    )
    
    Invoke-OSA (New-AppleScriptCommand "mount volume" $PSBoundParameters)
}

enum FolderName {
    CurrentApplication
    FrontmostApplication
    Me
    It
    ApplicationSupport
    ApplicationsFolder
    Desktop
    DesktopPicturesFolder
    DocumentsFolder
    DownloadsFolder
    FavoritesFolder
    FolderActionScripts
    Fonts
    Help
    HomeFolder
    InternetPlugins
    KeychainFolder
    LibraryFolder
    ModemScripts
    MoviesFolder
    MusicFolder
    PicturesFolder
    Preferences
    PrinterDescriptions
    PublicFolder
    ScriptingAdditionsFolder
    ScriptsFolder
    ServicesFolder
    SharedDocuments
    SharedLibraries
    SitesFolder
    StartupDisk
    StartupItems
    SystemFolder
    SystemPreferences
    TemporaryItems
    Trash
    UsersFolder
    UtilitiesFolder
    WorkflowsFolder
    Voices
    AppleMenu
    ControlPanels
    ControlStripModules
    Extensions
    LauncherItemsFolder
    PrinterDrivers
    PrintMonitor
    ShutdownFolder
    SpeakableItems
    Stationery
}

enum DomainType {
    SystemDomain
    LocalDomain
    NetworkDomain
    UserDomain
    ClassicDomain
}

function Get-PathTo
{
    <#
    
    .SYNOPSIS
    Return the full path to the specified application, script or folder.

    #>

    
    [CmdletBinding(DefaultParameterSetName = "PathTo")]
    param (
        [Parameter(ParameterSetName = 'PathToApplication')]  
        [String]$Application,
        [Parameter(ParameterSetName = 'PathTo')]
        [Parameter(Position = 0, Mandatory = $false)]    
        [Alias("Folder")]
        [FolderName]$DirectParameter,
        [Parameter(ParameterSetName = 'PathTo')]
        [DomainType]$From,
        [Parameter(ParameterSetName = 'PathTo')]
        [switch]$FolderCreation
    )

    Invoke-OSA (New-AppleScriptCommand "path to" $PSBoundParameters)
}

function Get-PathToResource 
{
    <#
    
    .SYNOPSIS
    Return the full path to the specified resource.

    #>

    
    param (
        [Parameter(Position = 0, Mandatory = $true)]    
        [Alias("Text")]
        [string]$DirectParameter,
        [Uri]$InBundle,
        [string]$InDirectory
    )

    Invoke-OSA (New-AppleScriptCommand "path to resource" $PSBoundParameters)
}

function Get-LocalizedString 
{
    <#
    
    .SYNOPSIS
    Return the localized string for the specified key.

    #>

    
    [CmdletBinding()]
    param (
        [Alias("Text")]    
        [string]$DirectParameter,
        [string]$FromTable,
        [Uri]$InBundle
    )

    Invoke-OSA (New-AppleScriptCommand "localized string" $PSBoundParameters)
}

function Get-ScriptingComponents
{
    <#
    
    .SYNOPSIS
    Return a list of all scripting components (e.g. AppleScript).

    #>

    
    Invoke-OSA "scripting components"
}


function Disconnect-UserSession
{
    <#
    
    .SYNOPSIS
    Log out the current user.

    #>

    
    param(
        [switch]$Force
    )

    $kCoreEventClass = 'aevt'

    $kAELogOut = 'logo'
    $kAEReallyLogOut = 'rlgo'

    $eventId = $Force ? $kAEReallyLogOut : $kAELogOut

    /usr/bin/osascript -e "tell application `"loginwindow`" to «event $kCoreEventClass$eventId»"
}

function Restart-MacComputer
{
    <#
    
    .SYNOPSIS
    Restart the computer.

    #>

    
    param(
        [switch]$Force
    )
    
    $kCoreEventClass = 'aevt'

    $kAEShowRestartDialog = 'rrst'
    $kAERestart = 'rest'

    $eventId = $Force ? $kAERestart : $kAEShowRestartDialog

    /usr/bin/osascript -e "tell application `"loginwindow`" to «event $kCoreEventClass$eventId»"
}


function Stop-MacComputer
{
    <#
    
    .SYNOPSIS
    Shut Down the computer.

    #>


    param(
        [switch]$Force
    )

    $kCoreEventClass = 'aevt'

    $kAEShowShutdownDialog = 'rsdn'
    $kAEShutDown = 'shut'

    $eventId = $Force ? $kAEShutDown : $kAEShowShutdownDialog

    /usr/bin/osascript -e "tell application `"loginwindow`" to «event $kCoreEventClass$eventId»"
}


function Suspend-MacComputer
{
    <#
    
    .SYNOPSIS
    Put the computer to sleep.

    #>

    
    $kCoreEventClass = 'aevt'
    $kAESleep = 'slep'

    $eventId = $kAESleep

    /usr/bin/osascript -e "tell application `"loginwindow`" to «event $kCoreEventClass$eventId»"
}

function Lock-Screen
{
    <#
    
    .SYNOPSIS
    Lock the screen.

    #>

    
    param(
        [switch]$WithScreenSaver
    )

    if ($WithScreenSaver)
    {
        $command = "launch application `"ScreenSaverEngine`""
    }
    else 
    {
        $command = "tell application `"System Events`" to keystroke `"q`" using {control down, command down}"
    }

    /usr/bin/osascript -e $command
}

function Get-UserCredential
{
    <#
    
    .SYNOPSIS
    Prompt the user to enter their credentials.

    #>

    
    param (
        [string]$WithTitle = "PowerShell credential request",
        [string]$WithPrompt = "Enter your credentials.",
        [string]$AsUserName,
        [switch]$UsingKeychainItem,
        [uri]$WithIconFile
    )

    $askCredentialsArguments = New-AppleScriptCommand "" $PSBoundParameters

    $script = "tell application `"`"PowerShellOSAUI`"`"
        
        set credentials to ask credentials $askCredentialsArguments

        { UserName: user name of credentials, ¬
         |Password|: encrypt secure text (password of credentials), ¬
         |KeychainItem|: keychain item of credentials, ¬
         |VerifiedPassword|:verified password of credentials }

    end tell"


    $result = Invoke-OSA $script

    if ($result.OSAScriptErrorNumberKey) { $result ; break }

    [PSCustomObject]@{
        UserName         = $result.UserName
        Password         = $result.Password ? $(ConvertTo-SecureString -String $result.Password) : $null
        KeychainItem     = $result.KeychainItem 
        VerifiedPassword = $result.VerifiedPassword
    }
}


function Get-SPLocalizedString
{
    <#
    
    .SYNOPSIS
    Gets a localized string for system_profiler.

    #>

    
    param(
        [String]$String,
        [string]$DataType,
        [string]$Language,
        [switch]$PascalCase
    )

    $localizedString = [SPLocalization]::LocalizedString($String, $DataType, $Language)

    if ($PSBoundParameters.ContainsKey("PascalCase"))
    {
        ConvertTo-PascalCase $localizedString
    }
    else 
    {
        $localizedString
    }
}


function SPRewriteName
{
    param(
        $name,
        $dataType,
        $language,
        $level
    )

    if ([SPSettings]::Raw)
    {
        return $name
    }
   
    $prefix = $dataType -replace "DataType", ""

    $localizedString = $(Get-SPLocalizedString $name $dataType $([SPSettings]::Language))

    ($localizedString.Contains("_")) ? $localizedString -replace $prefix,"" : $(ConvertTo-PascalCase $localizedString) -replace $prefix, ""
    
}

function SPRewriteValue 
{
    param (
        $name,
        $value,
        $dataType,
        $language
    )

    if ([SPSettings]::Raw)
    {
        return $value
    }

    ($value -is [string]) ? $(return Get-SPLocalizedString $value $dataType $language) : $(return $value)
}

function Add-SPTransformedMember 
{
    param(
        [psobject]$TargetObject,
        [string]$Name,
        [object]$Value,
        [string]$DataType,
        [string]$Language
    )

    if (-Not ($Name.StartsWith("_")) -or ( @("_name", "_items") -contains $Name))
    {

    if ($Value -is [array]) 
    {                
        if ($Value.Length -gt 0 -and @([int], [int64], [string]) -contains $Value[0].GetType()) 
        { 
            $TargetObject | Add-Member -MemberType NoteProperty -Name $(SPRewriteName $Name $DataType $Language 0) -Value $(SPRewriteValue $Name $Value $DataType $Language) -Force
        }
        else 
        {
            $processedArray = foreach ($item in $Value)
            {
                $arrayObject = [PSCustomObject]@{}

                foreach ($property in $item.PSObject.Properties) 
                {
                    $localizedName = $(SPRewriteName $property.Name $DataType $Language 1)

                    if ($localizedName -eq "Items" -and [SPSettings]::ResolveItemsName)
                    {
                        $resolvedItem = $($item.PSObject.Properties['_name'].Value)
                        $localizedName = Get-SPLocalizedString $resolvedItem $DataType $Language -PascalCase
                    }

                    Add-SPTransformedMember -TargetObject $arrayObject -Name $localizedName -Value $(SPRewriteValue $property.Name $property.Value $DataType $Language) -Force
                }
            
                $arrayObject
            }

            $localizedName = $(SPRewriteName $Name $DataType $Language  2)

            if ($localizedName -eq "Items" -and [SPSettings]::ResolveItemsName)
            {
                $localizedName = $(Get-SPLocalizedString "_name" $dataType $language -PascalCase)
            }

            $TargetObject | Add-Member -MemberType NoteProperty -Name $localizedName -Value $processedArray -Force
        }
    }
    elseif ($Value -is [PSCustomObject]) 
    {      
        $customObject = [PSCustomObject]@{}

        foreach ($property in $Value.PSObject.Properties) 
        {
            Add-SPTransformedMember -TargetObject $customObject -Name $(SPRewriteName $property.Name $DataType $Language 3) -Value $(SPRewriteValue $property.Name $property.Value $DataType $Language) -Force
        }

        $localizedName = $(SPRewriteName $Name $DataType $Language 4)

        if ($localizedName -eq "Items")
        {
            $localizedName = ConvertTo-PascalCase $localizedName
        }

        $TargetObject | Add-Member -MemberType NoteProperty -Name $localizedName -Value $customObject -Force
       
    }
    else 
    {        
        $key = $(SPRewriteName $name $dataType $Language)
        $localizedValue = $(SPRewriteValue $Name $Value $DataType $Language)

        $TargetObject | Add-Member -MemberType NoteProperty -Name $key -Value $localizedValue -Force 
    }

    }
}


function Get-SPAvailableDataTypes 
{
    <#
    
    .SYNOPSIS
    Lists the available datatypes for Get-SystemInformation.

    #>

    
    [SPSettings]::ListDataTypes()
}


function Get-SPAvailableLocalizations 
{
    <#
    
    .SYNOPSIS
    Lists the available localizations for Get-SystemInformation.

    #>

    
    [SPSettings]::ListAvailableLocalizations()
}

function Get-SystemInformation
{
    <#
    
    .SYNOPSIS
    Reports system hardware and software configuration.

    #>

    
    [CmdletBinding(DefaultParameterSetName = "DataTypes")]
    param(
        [Parameter(ParameterSetName = "ListDataTypes")]
        [Switch]$ListDataTypes,
        [Parameter(ParameterSetName = "DataTypes", Position = 0)]
        [Parameter(ParameterSetName = "Raw", Position = 0)]
        [Parameter(ParameterSetName = "Fast", Position = 0)]
        [ValidateScript({ $(Get-SPAvailableDataTypes) -ccontains $_}, ErrorMessage = "SPDataType for {0} is not available in Get-SPAvailableDataTypes" )]
        [ArgumentCompletions({ $(Get-SPAvailableDataTypes) })]
        [string[]]$DataTypes = $([SPSettings]::ListDataTypes()),
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Raw")]
        [ValidateScript({$(Get-SPAvailableDataTypes) -ccontains $_ }, ErrorMessage = "SPDataType for {0} is not available in {1} Get-SPAvailableDataTypes" )]
        [ArgumentCompletions({ $(Get-SPAvailableDataTypes) })]
        [string[]]$ExcludeDataTypes,
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Raw")]
        [Parameter(ParameterSetName = "Fast")]
        [ValidateSet("Mini", "Basic", "Full")]
        [string]$DetailLevel = "Full",
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Fast")]
        [ValidateScript({ $(Get-SPAvailableLocalizations) -ccontains $_ }, ErrorMessage = "Localization for {0} is not available in {1} Get-SPAvailableLocalizations" )]
        [ArgumentCompletions({ $(Get-SPAvailableLocalizations) })]
        [string]$Language = "en",
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Raw")]
        [Parameter(ParameterSetName = "Fast")]
        [switch]$Extended,
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Raw")]
        [Parameter(ParameterSetName = "Fast")]
        [switch]$Raw,
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Raw")]
        [Parameter(ParameterSetName = "Fast")]
        [ValidateRange(0,180)]
        [int]$Timeout = 180,
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Fast")]
        [switch]$ResolveItemsName,
        [Parameter(ParameterSetName = "DataTypes")]
        [Parameter(ParameterSetName = "Raw")]
        [Parameter(ParameterSetName = "Fast")]
        [switch]$Fast
    )

    if ($Fast)
    {
        $ExcludeDataTypes = @("SPExtensionsDataType", "SPFontsDataType", "SPDiagnosticsDataType", "SPApplicationsDataType", "SPFrameworksDataType", "SPInstallHistoryDataType", "SPPrefPaneDataType", "SPLogsDataType", "SPRawCameraDataType", "SPSyncServicesDataType", "SPManagedClientDataType", "SPPrefPaneDataType", "SPConfigurationProfileDataType")
    }

    if ($ExcludeDataTypes)
    {
        $DataTypes = [Linq.Enumerable]::Except($DataTypes, $ExcludeDataTypes)
    }

    if ($Language)
    {
        [SPSettings]::CustomLanguage = $Language

        [SPSettings]::FallbackLanguage = [Globalization.CultureInfo]::new($Language).EnglishName
    }

    if ($ResolveItemsName)
    {
        [SPSettings]::ResolveItemsName = $ResolveItemsName
    }

    if ($Raw)
    {
        [SPSettings]::Raw = $true
    }

    if ($PSCmdlet.ParameterSetName -eq "ListDataTypes")
    {
        return $([SPSettings]::ListDataTypes())
    }

    foreach ($dataType in $dataTypes) 
    {
        [SPSettings]::DataType = $dataType
        
        Write-Progress -Activity "Processing DataType: $dataType"

        $jsonOutput = /usr/sbin/system_profiler -json $dataType -detailLevel $($DetailLevel.ToLower()) -Timeout $Timeout

        $checkJsonOutput = $jsonOutput | ConvertFrom-Json | Select-Object -ExpandProperty $dataType

        if (-Not ($checkJsonOutput))
        {
            Write-Verbose "$(Get-SPLocalizedString "no_info_found" $dataType $language) ($dataType)"
        }

      # Process JSON data into a custom PSObject
        $jsonOutput | ConvertFrom-Json | Select-Object -ExpandProperty $dataType | ForEach-Object {
        
            # Create the root object for this data type

            if ($dataType -eq "SPSecureElementDataType")
            {
                $content = $_.PSObject.Properties

                $SPSecureElementDataType = @"
[
    {
      "se_info" : {
        "se_plt" : "$($content['se_plt'].Value)",
        "se_id" : "$($content['se_id'].Value)",
        "se_os_id" : "$($content['se_os_id'].Value)",
        "se_device" : "$($content['se_device'].Value)",
        "se_prod_signed" : "$($content['se_prod_signed'].Value)",
        "se_in_restricted_mode" : "$($content['se_in_restricted_mode'].Value)",
        "se_hw" : "$($content['se_hw'].Value)",
        "se_fw" : "$($content['se_fw'].Value)",
        "se_os_version" : "$($content['se_os_version'].Value)"
      },
      "ctl_info" : {
        "ctl_hw" : "$($content['ctl_hw'].Value)",
        "ctl_fw" : "$($content['ctl_fw'].Value)",
        "ctl_mw" : "$($content['ctl_mw'].Value)"
      }
    }
]
"@

                $properties = ($SPSecureElementDataType | ConvertFrom-Json).PSObject.Properties
            }
            elseif ($dataType -eq "SPiBridgeDataType")
            {
                $content = $_.PSObject.Properties

                $SPiBridgeDataType = @"
[
    {
        "ibridge_boot_uuid" : "$($content['ibridge_boot_uuid'].Value)",
        "ibridge_build" : "$($content['ibridge_build'].Value)",
        "ibridge_model_identifier_top" : "$($content['ibridge_model_identifier_top'].Value)",
        "ibridge_extra_boot_policies" : {
        "ibridge_secure_boot" : "$($content['ibridge_secure_boot'].Value)",
        "ibridge_sb_sip" : "$($content['ibridge_sb_sip'].Value)",
        "ibridge_sb_ssv" : "$($content['ibridge_sb_ssv'].Value)",
        "ibridge_sb_ctrr" : "$($content['ibridge_sb_ctrr'].Value)",
        "ibridge_sb_boot_args" : "$($content['ibridge_sb_boot_args'].Value)",
        "ibridge_sb_other_kext" : "$($content['ibridge_sb_other_kext'].Value)",
        "ibridge_sb_manual_mdm" : "$($content['ibridge_sb_manual_mdm'].Value)",
        "ibridge_sb_device_mdm" : "$($content['ibridge_sb_device_mdm'].Value)"
        }
    }
]
"@

            $properties = ($SPiBridgeDataType | ConvertFrom-Json).PSObject.Properties
            }
            else
            {
                $properties = $_.PSObject.Properties
            }

            $type = Get-SPLocalizedString $dataType $dataType $([SPSettings]::CustomLanguage)

            $rootObject = [pscustomobject]@{
                SPDataType = $dataType
                SPType     = $type
            }

            $strings = {
                $dataType = [SPSettings]::DataType
                $language = [SPSettings]::CustomLanguage

                Write-Output $([SPLocalization]::LocalisedStrings[$dataType][$language]) }

            $rootObject | Add-Member -MemberType ScriptMethod -Name GetStrings -Value $strings

        # Process each top-level property of the current data type

        if ($dataType -eq "SPAudioDataType")
        {
            $ResolveItemsName = $true
        }

        $resolvedName = $false
            
        if (-Not [SPSettings]::Raw -and $ResolveItemsName)
        {
            if (($properties.Name | Where-Object { $_ -eq "_name" }) -and ($properties.Name | Where-Object { $_ -eq "_items" })) 
            {
                $newName = $($properties['_name'].Value)
                $newValue = $($properties['_items'].Value)
                $resolvedName = $true
            }
        }

        foreach ($property in $properties) 
        {
            if ($resolvedName)
            {
                Add-SPTransformedMember -TargetObject $rootObject -Name $newName -Value $newValue -DataType $dataType -Language $Language
            }
            else 
            {
                Add-SPTransformedMember -TargetObject $rootObject -Name $property.Name -Value $property.Value -DataType $dataType -Language $Language
            }     
        }

        Write-Progress "Done." -Completed

        # Output the final, transformed object for this data type
        $rootObject
        }
    }
}

enum DataDetectionType {
    AllTypes
    Link
    PhoneNumber
    Address 
    Date
    TransitInformation 
}

function Invoke-DataDetection
{
    <#
    
    .SYNOPSIS
    Find dates, addresses, links, phone numbers, and transit information in natural language text.

    #>


    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$InputObject,
        [DataDetectionType]$DataType = "AllTypes"
    )

    if ($DataType -ne "AllTypes")
    {
        $Type = "Type$DataType"
    }
    else 
    {
        $Type = "AllSystemTypes"
    }

    $script = "
    use framework `"`"Foundation`"`"
    use scripting additions

    property NSString : a reference to current application's NSString
    property NSDataDetector : a reference to current application's NSDataDetector

    property NSTextCheckingTypeLink : a reference to current application's NSTextCheckingTypeLink
    property NSTextCheckingTypePhoneNumber : a reference to current application's NSTextCheckingTypePhoneNumber
    property NSTextCheckingTypeAddress : a reference to current application's NSTextCheckingTypeAddress
    property NSTextCheckingTypeDate : a reference to current application's NSTextCheckingTypeDate
    property NSTextCheckingTypeTransitInformation : a reference to current application's NSTextCheckingTypeTransitInformation

    property NSTextCheckingAllSystemTypes : a reference to current application's NSTextCheckingAllSystemTypes

    set stringToCheck to `"`"$InputObject`"`"

    set theString to NSString's stringWithString:stringToCheck

    set {theDetector, theError} to NSDataDetector's dataDetectorWithTypes:NSTextChecking$Type |error|:(reference)

    set searchRange to {location:0, |length|:theString's |length|()}

    set matches to theDetector's matchesInString:theString options:0 range:searchRange

    if (count of matches) = 0 then
        return
    end if

    set resultList to {}

    repeat with match in matches
        
        set resultType to match's resultType as integer
        
        set range to match's range as record as list
        
        if resultType = NSTextCheckingTypeLink as integer then
            
            set end of resultList to {DataDetectionType:`"`"Link`"`", |Range|:range, |Result|:match's |URL|'s |absoluteString| as text}
            
        else if resultType = NSTextCheckingTypeDate as integer then
            
            set end of resultList to {DataDetectionType:`"`"Date`"`", |Range|:range, |Result|:match's |date| as date}
            
        else if resultType = NSTextCheckingTypePhoneNumber as integer then
            
            set end of resultList to {DataDetectionType:`"`"PhoneNumber`"`", |Range|:range, |Result|:match's |phoneNumber| as text}
            
        else if resultType = NSTextCheckingTypeAddress as integer then
            
            set end of resultList to {DataDetectionType:`"`"Address`"`", |Range|:range, |Result|:match's addressComponents as record}
            
        else if resultType = NSTextCheckingTypeTransitInformation as integer then
            
            set end of resultList to {DataDetectionType:`"`"TransitInformation`"`", |Range|:range, |Result|:match's components as record}

        end if
        
    end repeat

    return resultList
"

    Invoke-OSA $script
}

Set-Alias -Name Choose-Application       -Value Select-Application                                        
Set-Alias -Name Choose-Color             -Value Select-Color                                    
Set-Alias -Name Choose-File              -Value Select-File                                
Set-Alias -Name Choose-FileName          -Value Select-FileName
Set-Alias -Name Choose-Folder            -Value Select-Folder                          
Set-Alias -Name Choose-FromList          -Value Select-FromList                             
Set-Alias -Name Choose-RemoteApplication -Value Select-RemoteApplication                         
Set-Alias -Name Choose-URL               -Value Select-URL                                                                                                  
Set-Alias -Name Display-Alert            -Value Show-Alert                                            
Set-Alias -Name Display-Dialog           -Value Show-Dialog                                      
Set-Alias -Name Display-Notification     -Value Show-Notification
Set-Alias -Name Log-Out                  -Value Disconnect-UserSession
Set-Alias -Name Sleep-MacComputer        -Value Suspend-MacComputer

 Export-ModuleMember -Alias @(
    'Choose-Application',
    'Choose-Color',
    'Choose-File',
    'Choose-FileName',
    'Choose-Folder',
    'Choose-FromList',
    'Choose-RemoteApplication',
    'Choose-URL',
    'Display-Alert',
    'Display-Dialog',
    'Display-Notification',
    'Log-Out',
    'Sleep-MacComputer'
    )

Export-ModuleMember -Function @(
    'Select-Application',
    'Select-Color',
    'Select-File',
    'Select-FileName',
    'Select-Folder',
    'Select-FromList',
    'Select-RemoteApplication',
    'Select-URL',
    'Get-ClipboardInfo',
    'Get-InfoFor',
    'Get-ListDisks',
    'Get-ListFolder',
    'Get-LocalizedString',
    'Get-LocalizedStrings',
    'Get-MacClipboard',
    'Get-PathTo',
    'Get-PathToResource',
    'Get-ScriptingComponents',
    'Get-SPAvailableDataTypes',
    'Get-SPAvailableLocalizations',
    'Get-SPLocalizedString'
    'Get-SystemAttribute',
    'Get-SystemInfo',
    'Get-SystemInformation',
    'Get-UserCredential',
    'Get-VolumeSettings',
    'Invoke-Beep',
    'Invoke-DataDetection',
    'Invoke-Delay',
    'Invoke-Say',
    'Lock-Screen',
    'Disconnect-UserSession',
    'Mount-Volume',
    'Restart-MacComputer',
    'Set-MacClipboard',
    'Set-Volume',
    'Show-Alert',
    'Show-Dialog',
    'Show-Notification',
    'Stop-MacComputer',
    'Suspend-MacComputer'
    )