203 lines
5.4 KiB
Go
203 lines
5.4 KiB
Go
package tui
|
|
|
|
import (
|
|
"errors"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"proxyctl/internal/app"
|
|
)
|
|
|
|
type operationError struct {
|
|
status app.OperationStatus
|
|
detail string
|
|
}
|
|
|
|
// operationCoordinator owns the complete interactive operation lifecycle. The
|
|
// root Model delegates stream consumption, progress, confirmation, current
|
|
// error, and deferred-exit state here instead of mirroring those states.
|
|
type operationCoordinator struct {
|
|
active bool
|
|
intent app.Intent
|
|
updates <-chan app.OperationUpdate
|
|
progress map[string]app.TargetProgressPhase
|
|
status app.OperationStatus
|
|
currentError *operationError
|
|
errorExpanded bool
|
|
errorOffset int
|
|
confirmation *app.Confirmation
|
|
confirmationDialog confirmationDialog
|
|
confirmationIntent *app.Intent
|
|
quitRequested bool
|
|
}
|
|
|
|
type operationProgress struct {
|
|
update app.OperationUpdate
|
|
updates <-chan app.OperationUpdate
|
|
closed bool
|
|
}
|
|
|
|
func newOperationCoordinator() operationCoordinator {
|
|
return operationCoordinator{progress: make(map[string]app.TargetProgressPhase)}
|
|
}
|
|
|
|
func waitForOperation(updates <-chan app.OperationUpdate) tea.Cmd {
|
|
return func() tea.Msg {
|
|
update, ok := <-updates
|
|
if !ok {
|
|
return operationProgress{updates: updates, closed: true}
|
|
}
|
|
return operationProgress{update: update, updates: updates}
|
|
}
|
|
}
|
|
|
|
func (o *operationCoordinator) start(application app.InteractiveModule, intent app.Intent) (app.Snapshot, tea.Cmd, bool) {
|
|
if o.quitRequested || o.active || o.confirmation != nil {
|
|
o.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current operation to finish before starting another modification"}
|
|
return application.Snapshot(), nil, false
|
|
}
|
|
stream := application.StartOperation(intent)
|
|
o.active = true
|
|
o.intent = intent
|
|
o.updates = stream.Updates
|
|
o.progress = make(map[string]app.TargetProgressPhase)
|
|
if intent.TargetID != "" {
|
|
o.progress[intent.TargetID] = app.TargetProgressRunning
|
|
}
|
|
o.status = app.OperationStatus{Kind: app.StatusProgress, Summary: operationStartingSummary(intent.Kind)}
|
|
return stream.Snapshot, waitForOperation(stream.Updates), true
|
|
}
|
|
|
|
func operationStartingSummary(kind app.IntentKind) string {
|
|
switch kind {
|
|
case app.RefreshTargets:
|
|
return "Checking Target availability and state…"
|
|
case app.ActivateProfile:
|
|
return "Changing Active Profile…"
|
|
default:
|
|
return "Starting Target mutation…"
|
|
}
|
|
}
|
|
|
|
func (o *operationCoordinator) apply(update app.OperationUpdate) (firstFailedTargetID string) {
|
|
status := update.Status()
|
|
progress := update.Progress()
|
|
for _, item := range progress {
|
|
o.progress[item.TargetID] = item.Phase
|
|
if item.Phase == app.TargetProgressSucceeded {
|
|
o.resolveTargetError(item.TargetID)
|
|
}
|
|
}
|
|
if status.Summary != "" {
|
|
o.status = status
|
|
}
|
|
if update.IsSucceeded() {
|
|
o.resolveTargetError(status.TargetID)
|
|
}
|
|
if update.IsFailed() {
|
|
o.currentError = &operationError{status: status, detail: errorDetail(update.Err())}
|
|
o.errorExpanded = false
|
|
o.errorOffset = 0
|
|
firstFailedTargetID = update.FirstFailedTargetID()
|
|
}
|
|
if request := update.Confirmation(); request != nil {
|
|
intent := o.intent
|
|
o.confirmation = request
|
|
o.confirmationDialog = newConfirmationDialog(*request)
|
|
o.confirmationIntent = &intent
|
|
}
|
|
if update.IsTerminal() {
|
|
o.finish()
|
|
}
|
|
return firstFailedTargetID
|
|
}
|
|
|
|
func errorDetail(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
func (o *operationCoordinator) resolveTargetError(targetID string) {
|
|
if targetID == "" || o.currentError == nil || o.currentError.status.TargetID != targetID {
|
|
return
|
|
}
|
|
o.currentError = nil
|
|
o.errorExpanded = false
|
|
o.errorOffset = 0
|
|
}
|
|
|
|
func (o *operationCoordinator) finish() {
|
|
o.active = false
|
|
o.intent = app.Intent{}
|
|
o.updates = nil
|
|
}
|
|
|
|
func (o *operationCoordinator) unexpectedClose() {
|
|
o.status = app.OperationStatus{Kind: app.StatusError, Summary: "Operation ended without a final result"}
|
|
o.currentError = &operationError{
|
|
status: o.status,
|
|
detail: "The Application operation stream closed before publishing a terminal result.",
|
|
}
|
|
o.errorExpanded = false
|
|
o.errorOffset = 0
|
|
o.finish()
|
|
}
|
|
|
|
func (o *operationCoordinator) acknowledgeError() {
|
|
o.currentError = nil
|
|
o.errorExpanded = false
|
|
o.errorOffset = 0
|
|
if o.status.Kind == app.StatusError {
|
|
o.status = app.OperationStatus{}
|
|
}
|
|
}
|
|
|
|
func (o *operationCoordinator) cancelConfirmation() {
|
|
o.confirmation = nil
|
|
o.confirmationDialog = confirmationDialog{}
|
|
o.confirmationIntent = nil
|
|
o.status = app.OperationStatus{}
|
|
}
|
|
|
|
func (o *operationCoordinator) confirmedIntent() (app.Intent, bool) {
|
|
if o.confirmationIntent == nil {
|
|
return app.Intent{}, false
|
|
}
|
|
intent := *o.confirmationIntent
|
|
intent.Confirm = true
|
|
o.confirmation = nil
|
|
o.confirmationDialog = confirmationDialog{}
|
|
o.confirmationIntent = nil
|
|
return intent, true
|
|
}
|
|
|
|
func (o operationCoordinator) busy() bool {
|
|
return o.quitRequested || o.active || o.confirmation != nil
|
|
}
|
|
|
|
func (o operationCoordinator) deferredQuit() tea.Cmd {
|
|
if o.quitRequested && !o.active {
|
|
return tea.Quit
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *operationCoordinator) requestQuit() tea.Cmd {
|
|
o.quitRequested = true
|
|
if o.active {
|
|
return nil
|
|
}
|
|
return tea.Quit
|
|
}
|
|
|
|
func (o *operationCoordinator) fail(err error, summary string) {
|
|
if err == nil {
|
|
err = errors.New(summary)
|
|
}
|
|
o.status = app.OperationStatus{Kind: app.StatusError, Summary: summary}
|
|
o.currentError = &operationError{status: o.status, detail: err.Error()}
|
|
o.finish()
|
|
}
|