Public/Get-HDTConsoleEditorState.ps1

function Get-HDTConsoleEditorState {
    <#
        .SYNOPSIS
            Everything the task sequence editor shows about a document it is
            part-way through editing: the tree, the selected row's options, and
            which actions are available.
 
        .DESCRIPTION
            THIS IS WHAT LETS THE WINDOW STAY BRANCH-FREE. Wiring the toolbar
            means deciding things - which buttons are live for the selected row,
            what the tree looks like after a splice, whether the edited text
            still parses - and this toolkit puts decisions in commands
            rather than in an adapter nothing tests. With this in place every
            handler in New-HDTConsoleHost is one call and one assignment.
 
            THE EDITED LINES ARE RE-READ THROUGH THE ENGINE, NOT TRACKED AS A
            MODEL. After each splice the text is handed to
            Import-HDTSequenceDocument through an in-memory IFileSystem, so what
            the tree draws is what the DEPLOYMENT would run - not a parallel
            model of it that could drift. It also means a splice that produced
            something unreadable is caught here, with the file on the share
            still intact, which is the same check
            Save-HDTSequenceDocument makes one press later.
 
            A BROKEN DOCUMENT REPORTS RATHER THAN THROWS. An editor that threw
            mid-edit would leave an administrator with a dialog they cannot
            dismiss and a window they cannot get their work out of. The status
            says what happened and every action goes dark, which is the same
            answer Get-HDTConsoleStepNode gives a sequence that will not parse.
 
            UP IS NOT ALWAYS AVAILABLE, AND THAT IS DELIBERATE.
            Move-HDTStep refuses to move the first step in a group past
            the group's own boundary, because "before the group" and "the last
            step of the group above" are both plausible and the console must not
            guess. A toolbar that
            offered Up there would turn an ordinary-looking press into an error
            box, so the button is dark instead - the refusal is the same, made
            one moment earlier and without the dialog.
 
            SIBLINGS ARE COUNTED IN THE DOCUMENT, NOT IN THE TREE. A group's
            neighbours are the other groups at its level; a step's are the steps
            sharing its GroupPath. Counting rows on screen would make the last
            step of one group look like the neighbour of the first step of the
            next, which is exactly the move that is refused.
 
            PASTE AND SAVE ARE ABOUT THE WINDOW, NOT THE DOCUMENT. Whether
            anything has been copied, and whether anything has been changed, are
            facts the window holds; they come in as switches so that this
            command still has no state of its own and can be called as often as
            the tree is rebuilt.
 
        .PARAMETER Line
            The document as it currently stands, already split into lines.
 
        .PARAMETER Path
            The sequence.yaml these lines came from. Used so any error the
            engine raises names the file the administrator is editing.
 
        .PARAMETER SelectedName
            The step or group the administrator has selected, if any.
 
        .PARAMETER HasClipboard
            Something has been copied, so Paste means something.
 
        .PARAMETER Dirty
            There are edits that have not been saved.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject:
 
              Status, Message, StatusText whether the text still reads, and what to say
              Node, Root the tree, flat and as roots
              Selected the row SelectedName names, or nothing
              Option Get-HDTConsoleStepOption for it, or nothing
              StepCount how many steps the document holds
              Dirty echoed back, so the window has one source
              CanRemove, CanCopy, CanMoveUp, CanMoveDown, CanPaste, CanSave
 
        .EXAMPLE
            $state = Get-HDTConsoleEditorState -Line $line -Path $path -SelectedName 'Apply OS'
            $state.CanMoveUp
 
        .EXAMPLE
            $line = Move-HDTStep -Line $line -Name 'Apply OS' -Direction Down
            $state = Get-HDTConsoleEditorState -Line $line -Path $path -SelectedName 'Apply OS' -Dirty
 
            The whole editing loop: splice, then re-read.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string[]] $Line,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(Position = 2)]
        [AllowEmptyString()]
        [AllowNull()]
        [string] $SelectedName,

        [Parameter()]
        [switch] $HasClipboard,

        [Parameter()]
        [switch] $Dirty
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $status = 'Ok'
    $message = ''
    $document = $null

    try {
        $reader = New-HDTFileSystemFromText -Path $Path -Text ($Line -join [System.Environment]::NewLine)
        $document = Import-HDTSequenceDocument -Path $Path -FileSystem $reader
    } catch {
        $status = 'Error'
        $message = [string] $_.Exception.Message
    }

    if ($status -eq 'Error') {
        return [pscustomobject] @{
            Status      = 'Error'
            Message     = $message
            StatusText  = ('This document cannot be read: {0}' -f $message)
            Node        = [pscustomobject[]] @()
            Root        = [pscustomobject[]] @()
            Selected    = $null
            Option      = $null
            StepCount   = 0
            Dirty       = [bool] $Dirty
            CanRemove   = $false
            CanCopy     = $false
            CanMoveUp   = $false
            CanMoveDown = $false
            CanPaste    = $false
            CanSave     = $false
        }
    }

    # -- the tree ----------------------------------------------------------
    #
    # Get-HDTConsoleStepNode reads a workspace's task sequence row rather than a
    # document, so the document is presented as one. Nothing is invented: every
    # field below is either the document's own or the path it was read from.
    $sequence = [pscustomobject] @{
        Id     = [string] $document.Id
        Name   = [string] $document.Name
        Path   = [string] $Path
        Status = 'Ok'
        Step   = @($document.Step)
        Group  = @($document.Group)
    }

    $header = [pscustomobject] @{
        Title      = '{0} - {1}' -f $document.Id, $document.Name
        Root       = [string] $Path
        DeployRoot = [string] $document.Id
    }

    $built = Get-HDTConsoleStepNode -Sequence $sequence -Header $header

    # -- the selected row, and what it makes possible ----------------------

    $selected = $null
    $option = $null

    $canMoveUp = $false
    $canMoveDown = $false

    if (-not [string]::IsNullOrEmpty($SelectedName)) {
        $step = @($document.Step | Where-Object { $_.Name -eq $SelectedName })
        $group = @($document.Group | Where-Object { @($_.Path).Count -gt 0 -and $_.Path[-1] -eq $SelectedName })

        $subject = $null
        $sibling = @()

        if (@($step).Count -gt 0) {
            $subject = $step[0]

            # A step's neighbours are the steps sharing its GroupPath - the last
            # step of one group is not the neighbour of the first step of the
            # next, however adjacent they look on screen.
            $key = @($subject.GroupPath) -join "`u{001F}"
            $sibling = @($document.Step | Where-Object { (@($_.GroupPath) -join "`u{001F}") -eq $key })
        } elseif (@($group).Count -gt 0) {
            $subject = $group[0]

            # A group's are the groups at its own level under the same parent.
            #
            # NOT $path: PowerShell variable names are case-insensitive, so that
            # would assign to this function's own [string] $Path parameter,
            # which coerces the array to a single string and takes .Count with
            # it. The failure surfaces two lines later as "the property 'Count'
            # cannot be found", naming neither the parameter nor the assignment.
            $groupPath = @($subject.Path)
            $depth = $groupPath.Count
            $parent = Get-HDTGroupParent -Path $groupPath

            $sibling = @($document.Group | Where-Object {
                    $other = @($_.Path)

                    $other.Count -eq $depth -and (Get-HDTGroupParent -Path $other) -eq $parent
                })
        }

        if ($null -ne $subject) {
            $at = [array]::IndexOf($sibling, $subject)

            $canMoveUp = ($at -gt 0)
            $canMoveDown = ($at -ge 0 -and $at -lt (@($sibling).Count - 1))

            $option = Get-HDTConsoleStepOption -Step $subject

            # The row on screen, so the window can put the selection back where
            # it was after a splice rebuilt the tree. Matched on Name rather
            # than Text: a step legitimately called '2. Reboot' would otherwise
            # be found by the wrong row, or by none.
            $selected = @($built.Node | Where-Object { $_.Name -eq $SelectedName })[0]

            # AND IT IS MARKED, so the tree comes back with it highlighted. The
            # window binds TreeViewItem.IsSelected to this and does nothing
            # else; a host that walked ItemContainerGenerator to find the row
            # again would be a decision in an adapter, and one only a person
            # looking at a screen could check.
            if ($null -ne $selected) { $selected.IsSelected = $true }
        }
    }

    $found = ($null -ne $option)

    $count = @($document.Step).Count

    $statusText = '{0} steps' -f $count
    if ($Dirty) { $statusText = '{0} steps - unsaved changes' -f $count }

    return [pscustomobject] @{
        Status      = 'Ok'
        Message     = ''
        StatusText  = $statusText
        Node        = [pscustomobject[]] @($built.Node)
        Root        = [pscustomobject[]] @($built.TopLevel)
        Selected    = $selected
        Option      = $option
        StepCount   = $count
        Dirty       = [bool] $Dirty
        CanRemove   = $found
        CanCopy     = $found
        CanMoveUp   = $canMoveUp
        CanMoveDown = $canMoveDown
        CanPaste    = ([bool] $HasClipboard -and $found)
        CanSave     = [bool] $Dirty
    }
}