267 lines
7.6 KiB
Go
267 lines
7.6 KiB
Go
package app
|
|
|
|
// StatusKind gives presentation layers semantics without requiring them to
|
|
// inspect user-facing status text.
|
|
type StatusKind uint8
|
|
|
|
const (
|
|
StatusProgress StatusKind = iota + 1
|
|
StatusSuccess
|
|
StatusError
|
|
StatusBlocked
|
|
)
|
|
|
|
// OperationStatus is concise status text related to an optional Target.
|
|
type OperationStatus struct {
|
|
Kind StatusKind
|
|
Summary string
|
|
TargetID string
|
|
}
|
|
|
|
// Confirmation is a structured, safe-default decision. Presentation layers
|
|
// render these fields but never infer confirmation state from their wording.
|
|
type Confirmation struct {
|
|
Title string
|
|
Summary string
|
|
Details []string
|
|
ConfirmLabel string
|
|
CancelLabel string
|
|
HighImpact bool
|
|
}
|
|
|
|
// TargetProgressPhase describes one Target's part in an operation stream.
|
|
type TargetProgressPhase uint8
|
|
|
|
const (
|
|
TargetProgressRunning TargetProgressPhase = iota + 1
|
|
TargetProgressSucceeded
|
|
TargetProgressFailed
|
|
)
|
|
|
|
// OperationState is a closed tagged set. Its concrete value makes an update
|
|
// exactly one of running, confirmation, succeeded, failed, or closed.
|
|
type OperationState interface {
|
|
operationState()
|
|
}
|
|
|
|
type operationRunning struct {
|
|
status OperationStatus
|
|
progress []TargetProgress
|
|
}
|
|
|
|
func (operationRunning) operationState() {}
|
|
|
|
type operationConfirmation struct{ request Confirmation }
|
|
|
|
func (operationConfirmation) operationState() {}
|
|
|
|
type operationSucceeded struct {
|
|
status OperationStatus
|
|
progress []TargetProgress
|
|
}
|
|
|
|
func (operationSucceeded) operationState() {}
|
|
|
|
type operationFailed struct {
|
|
status OperationStatus
|
|
progress []TargetProgress
|
|
firstFailedTargetID string
|
|
err error
|
|
}
|
|
|
|
func (operationFailed) operationState() {}
|
|
|
|
type operationClosed struct{ status OperationStatus }
|
|
|
|
func (operationClosed) operationState() {}
|
|
|
|
// OperationUpdate is the common progressive contract used by interactive
|
|
// detection, Target mutation, and Active Profile transition operations. Build
|
|
// values with the constructors below so contradictory states are unrepresentable.
|
|
type OperationUpdate struct {
|
|
Snapshot Snapshot
|
|
State OperationState
|
|
}
|
|
|
|
func RunningOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress) OperationUpdate {
|
|
status.Kind = StatusProgress
|
|
return OperationUpdate{Snapshot: snapshot, State: operationRunning{status: status, progress: cloneProgress(progress)}}
|
|
}
|
|
|
|
func ConfirmationOperationUpdate(snapshot Snapshot, request Confirmation) OperationUpdate {
|
|
return OperationUpdate{Snapshot: snapshot, State: operationConfirmation{request: request}}
|
|
}
|
|
|
|
func SucceededOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress) OperationUpdate {
|
|
status.Kind = StatusSuccess
|
|
return OperationUpdate{Snapshot: snapshot, State: operationSucceeded{status: status, progress: cloneProgress(progress)}}
|
|
}
|
|
|
|
func FailedOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress, firstFailedTargetID string, err error) OperationUpdate {
|
|
status.Kind = StatusError
|
|
return OperationUpdate{Snapshot: snapshot, State: operationFailed{
|
|
status: status, progress: cloneProgress(progress), firstFailedTargetID: firstFailedTargetID, err: err,
|
|
}}
|
|
}
|
|
|
|
// ClosedOperationUpdate reports a request that cannot start, such as an
|
|
// overlapping interactive operation. It is terminal and has no progress or
|
|
// error payload.
|
|
func ClosedOperationUpdate(snapshot Snapshot, status OperationStatus) OperationUpdate {
|
|
status.Kind = StatusBlocked
|
|
return OperationUpdate{Snapshot: snapshot, State: operationClosed{status: status}}
|
|
}
|
|
|
|
func cloneProgress(progress []TargetProgress) []TargetProgress {
|
|
return append([]TargetProgress(nil), progress...)
|
|
}
|
|
|
|
func (u OperationUpdate) Status() OperationStatus {
|
|
switch state := u.State.(type) {
|
|
case operationRunning:
|
|
return state.status
|
|
case operationSucceeded:
|
|
return state.status
|
|
case operationFailed:
|
|
return state.status
|
|
case operationClosed:
|
|
return state.status
|
|
default:
|
|
return OperationStatus{}
|
|
}
|
|
}
|
|
|
|
func (u OperationUpdate) Progress() []TargetProgress {
|
|
switch state := u.State.(type) {
|
|
case operationRunning:
|
|
return cloneProgress(state.progress)
|
|
case operationSucceeded:
|
|
return cloneProgress(state.progress)
|
|
case operationFailed:
|
|
return cloneProgress(state.progress)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (u OperationUpdate) Confirmation() *Confirmation {
|
|
state, ok := u.State.(operationConfirmation)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
request := state.request
|
|
request.Details = append([]string(nil), request.Details...)
|
|
return &request
|
|
}
|
|
|
|
func (u OperationUpdate) Err() error {
|
|
if state, ok := u.State.(operationFailed); ok {
|
|
return state.err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u OperationUpdate) FirstFailedTargetID() string {
|
|
if state, ok := u.State.(operationFailed); ok {
|
|
return state.firstFailedTargetID
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (u OperationUpdate) IsRunning() bool {
|
|
_, ok := u.State.(operationRunning)
|
|
return ok
|
|
}
|
|
|
|
func (u OperationUpdate) IsConfirmation() bool {
|
|
_, ok := u.State.(operationConfirmation)
|
|
return ok
|
|
}
|
|
|
|
func (u OperationUpdate) IsSucceeded() bool {
|
|
_, ok := u.State.(operationSucceeded)
|
|
return ok
|
|
}
|
|
|
|
func (u OperationUpdate) IsFailed() bool {
|
|
_, ok := u.State.(operationFailed)
|
|
return ok
|
|
}
|
|
|
|
func (u OperationUpdate) IsTerminal() bool { return !u.IsRunning() }
|
|
|
|
// OperationStream exposes the useful initial frame synchronously and all
|
|
// subsequent structured changes through Updates. Updates closes after a
|
|
// terminal update.
|
|
type OperationStream struct {
|
|
Snapshot Snapshot
|
|
Updates <-chan OperationUpdate
|
|
}
|
|
|
|
// InteractiveModule is the presentation-independent Application seam used by
|
|
// the TUI. Module.Dispatch remains available for synchronous callers.
|
|
type InteractiveModule interface {
|
|
Module
|
|
StartOperation(Intent) OperationStream
|
|
}
|
|
|
|
// StartOperation returns before dispatch serialization is acquired. This keeps
|
|
// Bubble Tea Update non-blocking even when a synchronous caller currently owns
|
|
// the serialized Application entry.
|
|
func (a *Application) StartOperation(intent Intent) OperationStream {
|
|
initial := a.Snapshot()
|
|
a.interactiveMu.Lock()
|
|
if a.interactiveOperation {
|
|
a.interactiveMu.Unlock()
|
|
updates := make(chan OperationUpdate, 1)
|
|
updates <- ClosedOperationUpdate(initial, OperationStatus{
|
|
Summary: "Another system operation is already in progress",
|
|
})
|
|
close(updates)
|
|
return OperationStream{Snapshot: initial, Updates: updates}
|
|
}
|
|
a.interactiveOperation = true
|
|
a.interactiveMu.Unlock()
|
|
|
|
updates := make(chan OperationUpdate, 2*len(a.targets)+2)
|
|
go func() {
|
|
defer close(updates)
|
|
defer a.finishInteractiveOperation()
|
|
final := a.dispatch(intent, func(result Result) {
|
|
updates <- runningOperationUpdate(result)
|
|
})
|
|
updates <- terminalOperationUpdate(final)
|
|
}()
|
|
return OperationStream{Snapshot: initial, Updates: updates}
|
|
}
|
|
|
|
func (a *Application) finishInteractiveOperation() {
|
|
a.interactiveMu.Lock()
|
|
a.interactiveOperation = false
|
|
a.interactiveMu.Unlock()
|
|
}
|
|
|
|
func runningOperationUpdate(result Result) OperationUpdate {
|
|
if result.ConfirmationRequest != nil {
|
|
return ConfirmationOperationUpdate(result.Snapshot, *result.ConfirmationRequest)
|
|
}
|
|
return RunningOperationUpdate(result.Snapshot, result.Status, result.Progress)
|
|
}
|
|
|
|
func terminalOperationUpdate(result Result) OperationUpdate {
|
|
if result.ConfirmationRequest != nil {
|
|
return ConfirmationOperationUpdate(result.Snapshot, *result.ConfirmationRequest)
|
|
}
|
|
status := result.Status
|
|
if result.Err != nil {
|
|
if status.Summary == "" {
|
|
status.Summary = "Operation failed"
|
|
}
|
|
return FailedOperationUpdate(result.Snapshot, status, result.Progress, result.FirstFailedTargetID, result.Err)
|
|
}
|
|
if status.Summary == "" {
|
|
status.Summary = result.Notice
|
|
}
|
|
return SucceededOperationUpdate(result.Snapshot, status, result.Progress)
|
|
}
|