feat: unify interactive operation coordination
This commit is contained in:
+123
-405
@@ -31,64 +31,22 @@ type formState struct {
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
application app.Module
|
||||
presentation terminalPresentation
|
||||
snapshot app.Snapshot
|
||||
page page
|
||||
width int
|
||||
height int
|
||||
selected int
|
||||
selectedTargetID string
|
||||
dashboardFocus dashboardPane
|
||||
showUnavailable bool
|
||||
detailsOffset int
|
||||
form formState
|
||||
confirm bool
|
||||
targetConfirm *app.Intent
|
||||
profileConfirm *app.Intent
|
||||
pendingActivation app.Intent
|
||||
status string
|
||||
operationStatus app.OperationStatus
|
||||
currentError *app.OperationStatus
|
||||
errorExpanded bool
|
||||
confirmation *app.Confirmation
|
||||
operationActive bool
|
||||
operationKind app.IntentKind
|
||||
operationIntent app.Intent
|
||||
operationUpdates <-chan app.OperationUpdate
|
||||
operationProgress map[string]app.TargetProgressPhase
|
||||
applying bool
|
||||
refreshing bool
|
||||
detectionUpdates <-chan app.TargetDetectionUpdate
|
||||
profileTransition bool
|
||||
progressTarget string
|
||||
quitRequested bool
|
||||
detect func(context.Context, string) (probe.Capabilities, error)
|
||||
}
|
||||
|
||||
type targetOperationDone struct {
|
||||
result app.Result
|
||||
}
|
||||
|
||||
type refreshDone struct {
|
||||
result app.Result
|
||||
}
|
||||
|
||||
type targetDetectionProgress struct {
|
||||
update app.TargetDetectionUpdate
|
||||
updates <-chan app.TargetDetectionUpdate
|
||||
closed bool
|
||||
}
|
||||
|
||||
type profileOperationProgress struct {
|
||||
result app.Result
|
||||
updates <-chan app.Result
|
||||
}
|
||||
|
||||
type operationProgress struct {
|
||||
update app.OperationUpdate
|
||||
updates <-chan app.OperationUpdate
|
||||
closed bool
|
||||
application app.InteractiveModule
|
||||
presentation terminalPresentation
|
||||
snapshot app.Snapshot
|
||||
page page
|
||||
width int
|
||||
height int
|
||||
selected int
|
||||
selectedTargetID string
|
||||
dashboardFocus dashboardPane
|
||||
showUnavailable bool
|
||||
detailsOffset int
|
||||
form formState
|
||||
confirm bool
|
||||
status string
|
||||
operation operationCoordinator
|
||||
detect func(context.Context, string) (probe.Capabilities, error)
|
||||
}
|
||||
|
||||
// probeDone carries one finished Protocol Probe back to the form. address is
|
||||
@@ -102,54 +60,23 @@ type probeDone struct {
|
||||
gen uint64
|
||||
}
|
||||
|
||||
func waitForTargetDetection(updates <-chan app.TargetDetectionUpdate) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
update, ok := <-updates
|
||||
if !ok {
|
||||
return targetDetectionProgress{updates: updates, closed: true}
|
||||
}
|
||||
return targetDetectionProgress{update: update, updates: updates}
|
||||
}
|
||||
}
|
||||
|
||||
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 waitForProfileProgress(updates <-chan app.Result) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
result, ok := <-updates
|
||||
if !ok {
|
||||
return profileOperationProgress{result: app.Result{Complete: true}, updates: updates}
|
||||
}
|
||||
return profileOperationProgress{result: result, updates: updates}
|
||||
}
|
||||
}
|
||||
|
||||
func NewModel(application app.Module, configure ...ModelOption) Model {
|
||||
func NewModel(application app.InteractiveModule, configure ...ModelOption) Model {
|
||||
options := defaultModelOptions()
|
||||
for _, option := range configure {
|
||||
option(&options)
|
||||
}
|
||||
snapshot := application.Snapshot()
|
||||
model := Model{
|
||||
application: application,
|
||||
presentation: terminalPresentation{theme: newSemanticTheme(options.theme, options.output)},
|
||||
snapshot: snapshot,
|
||||
selectedTargetID: newTargetCatalog(snapshot, false, "").firstID(),
|
||||
width: 80,
|
||||
height: 24,
|
||||
operationProgress: make(map[string]app.TargetProgressPhase),
|
||||
detect: probe.Detect,
|
||||
application: application,
|
||||
presentation: terminalPresentation{theme: newSemanticTheme(options.theme, options.output)},
|
||||
snapshot: snapshot,
|
||||
selectedTargetID: newTargetCatalog(snapshot, false, "").firstID(),
|
||||
width: 80,
|
||||
height: 24,
|
||||
operation: newOperationCoordinator(),
|
||||
detect: probe.Detect,
|
||||
}
|
||||
_, progressiveDetection := application.(app.ProgressiveDetectionModule)
|
||||
if targetsChecking(snapshot) || progressiveDetection {
|
||||
if targetsChecking(snapshot) {
|
||||
model.startOperation(app.Intent{Kind: app.RefreshTargets})
|
||||
}
|
||||
return model
|
||||
@@ -165,8 +92,8 @@ func targetsChecking(snapshot app.Snapshot) bool {
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
if m.operationUpdates != nil {
|
||||
return waitForOperation(m.operationUpdates)
|
||||
if m.operation.updates != nil {
|
||||
return waitForOperation(m.operation.updates)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -174,16 +101,14 @@ func (m Model) Init() tea.Cmd {
|
||||
// busy is the single-operation guard for asynchronous Application work. Every
|
||||
// operation start checks it, so a completion can only belong to the one current
|
||||
// operation rather than racing another operation for shared model state.
|
||||
func (m Model) busy() bool {
|
||||
return m.quitRequested || m.operationInFlight() || m.pendingActivation.Kind != 0
|
||||
}
|
||||
func (m Model) busy() bool { return m.operation.busy() }
|
||||
|
||||
func (m Model) operationInFlight() bool {
|
||||
return m.operationActive || m.applying || m.refreshing || m.profileTransition
|
||||
}
|
||||
func (m Model) operationInFlight() bool { return m.operation.active }
|
||||
|
||||
func (m Model) dashboardNavigationAvailable() bool {
|
||||
return !m.quitRequested
|
||||
// A deferred exit rejects new work, not read-only browsing. Selection,
|
||||
// details, and Help remain useful until the current mutation is safe.
|
||||
return true
|
||||
}
|
||||
|
||||
func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -197,139 +122,41 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// been consumed. Until Bubble Tea processes that QuitMsg, keep the model
|
||||
// terminal so intervening input cannot cancel a confirmation or start a
|
||||
// second mutation.
|
||||
if m.quitRequested && !m.operationInFlight() {
|
||||
if m.operation.quitRequested && !m.operationInFlight() {
|
||||
return m, nil
|
||||
}
|
||||
if key, ok := message.(tea.KeyMsg); ok {
|
||||
exitRequest := key.String() == "ctrl+c" || (m.page == mainPage && key.String() == "q" && m.targetConfirm == nil && m.profileConfirm == nil)
|
||||
// Dashboard q always means exit, even when confirmation is visible.
|
||||
// Escape remains the deterministic cancel/back key.
|
||||
exitRequest := key.String() == "ctrl+c" || (m.page == mainPage && key.String() == "q")
|
||||
if exitRequest {
|
||||
if m.operationInFlight() {
|
||||
m.quitRequested = true
|
||||
return m, nil
|
||||
}
|
||||
return m, tea.Quit
|
||||
return m, m.operation.requestQuit()
|
||||
}
|
||||
}
|
||||
if progress, ok := message.(operationProgress); ok {
|
||||
if !m.operationActive || progress.updates != m.operationUpdates {
|
||||
if !m.operation.active || progress.updates != m.operation.updates {
|
||||
return m, nil
|
||||
}
|
||||
if progress.closed {
|
||||
m.snapshot = m.application.Snapshot()
|
||||
m.retainTargetSelection()
|
||||
m.finishOperation()
|
||||
return m, m.deferredQuit()
|
||||
m.operation.unexpectedClose()
|
||||
return m, m.operation.deferredQuit()
|
||||
}
|
||||
m.applyOperationUpdate(progress.update)
|
||||
if isTerminalOperationPhase(progress.update.Phase) {
|
||||
m.finishOperation()
|
||||
return m, m.deferredQuit()
|
||||
m.snapshot = progress.update.Snapshot
|
||||
m.retainTargetSelection()
|
||||
if firstFailed := m.operation.apply(progress.update); firstFailed != "" {
|
||||
m.selectedTargetID = firstFailed
|
||||
m.detailsOffset = 0
|
||||
}
|
||||
if m.operation.confirmation != nil {
|
||||
m.page = mainPage
|
||||
}
|
||||
if progress.update.IsTerminal() {
|
||||
return m, m.operation.deferredQuit()
|
||||
}
|
||||
return m, waitForOperation(progress.updates)
|
||||
}
|
||||
if progress, ok := message.(targetDetectionProgress); ok {
|
||||
if !m.refreshing || progress.updates != m.detectionUpdates {
|
||||
return m, nil
|
||||
}
|
||||
if progress.closed {
|
||||
// A stale Application generation closes without claiming an
|
||||
// authoritative completion snapshot. Reconcile with the Application
|
||||
// so this Model does not strand rows in its superseded Checking frame.
|
||||
formerCatalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
formerIndex := formerCatalog.index(m.selectedTargetID)
|
||||
m.snapshot = m.application.Snapshot()
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID).retain(m.selectedTargetID, formerIndex)
|
||||
m.refreshing = false
|
||||
m.detectionUpdates = nil
|
||||
return m, m.deferredQuit()
|
||||
}
|
||||
formerCatalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
formerIndex := formerCatalog.index(m.selectedTargetID)
|
||||
m.snapshot = progress.update.Snapshot
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID).retain(m.selectedTargetID, formerIndex)
|
||||
if progress.update.Complete {
|
||||
m.refreshing = false
|
||||
m.detectionUpdates = nil
|
||||
m.status = "Target availability and state checks completed"
|
||||
return m, m.deferredQuit()
|
||||
}
|
||||
return m, waitForTargetDetection(progress.updates)
|
||||
}
|
||||
if completed, ok := message.(refreshDone); ok {
|
||||
if !m.refreshing {
|
||||
return m, nil
|
||||
}
|
||||
m.refreshing = false
|
||||
formerCatalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
formerIndex := formerCatalog.index(m.selectedTargetID)
|
||||
m.snapshot = completed.result.Snapshot
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID).retain(m.selectedTargetID, formerIndex)
|
||||
if completed.result.Err != nil {
|
||||
m.status = "Error: " + completed.result.Err.Error()
|
||||
} else {
|
||||
m.status = completed.result.Notice
|
||||
}
|
||||
return m, m.deferredQuit()
|
||||
}
|
||||
if progress, ok := message.(profileOperationProgress); ok {
|
||||
if !m.profileTransition {
|
||||
return m, nil
|
||||
}
|
||||
m.snapshot = progress.result.Snapshot
|
||||
m.retainTargetSelection()
|
||||
m.progressTarget = ""
|
||||
if len(progress.result.Progress) > 0 {
|
||||
item := progress.result.Progress[len(progress.result.Progress)-1]
|
||||
if item.Applying {
|
||||
m.progressTarget = item.TargetID
|
||||
}
|
||||
}
|
||||
if progress.result.Confirmation != "" {
|
||||
m.applying = false
|
||||
m.profileTransition = false
|
||||
m.page = profilesPage
|
||||
intent := m.pendingActivation
|
||||
m.profileConfirm = &intent
|
||||
m.status = progress.result.Confirmation
|
||||
return m, m.deferredQuit()
|
||||
}
|
||||
if progress.result.Err != nil {
|
||||
m.status = "Error: " + progress.result.Err.Error()
|
||||
} else if progress.result.Notice != "" {
|
||||
m.status = progress.result.Notice
|
||||
}
|
||||
if progress.result.Complete {
|
||||
m.applying = false
|
||||
m.profileTransition = false
|
||||
m.progressTarget = ""
|
||||
m.pendingActivation = app.Intent{}
|
||||
return m, m.deferredQuit()
|
||||
}
|
||||
return m, waitForProfileProgress(progress.updates)
|
||||
}
|
||||
if completed, ok := message.(targetOperationDone); ok {
|
||||
if !m.applying || m.profileTransition {
|
||||
return m, nil
|
||||
}
|
||||
m.applying = false
|
||||
m.snapshot = completed.result.Snapshot
|
||||
m.retainTargetSelection()
|
||||
if completed.result.Confirmation != "" {
|
||||
intent := app.Intent{Kind: app.SetTargetDesired}
|
||||
if m.targetConfirm != nil {
|
||||
intent = *m.targetConfirm
|
||||
}
|
||||
m.targetConfirm = &intent
|
||||
m.status = completed.result.Confirmation
|
||||
} else if completed.result.Err != nil {
|
||||
m.targetConfirm = nil
|
||||
m.status = "Error: " + completed.result.Err.Error()
|
||||
} else {
|
||||
m.targetConfirm = nil
|
||||
m.status = completed.result.Notice
|
||||
}
|
||||
return m, m.deferredQuit()
|
||||
}
|
||||
if done, ok := message.(probeDone); ok {
|
||||
if !m.form.probing || done.gen != m.form.probeGen {
|
||||
return m, nil
|
||||
@@ -396,172 +223,93 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *Model) startOperation(intent app.Intent) tea.Cmd {
|
||||
if m.quitRequested {
|
||||
return nil
|
||||
snapshot, command, started := m.operation.start(m.application, intent)
|
||||
if started {
|
||||
m.snapshot = snapshot
|
||||
m.retainTargetSelection()
|
||||
}
|
||||
if m.operationActive {
|
||||
m.operationStatus = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before starting another modification"}
|
||||
return nil
|
||||
}
|
||||
interactive, ok := m.application.(app.InteractiveModule)
|
||||
if !ok {
|
||||
m.operationStatus = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Interactive operation streaming is unavailable"}
|
||||
return nil
|
||||
}
|
||||
stream := interactive.StartOperation(intent)
|
||||
m.snapshot = stream.Snapshot
|
||||
m.retainTargetSelection()
|
||||
m.operationActive = true
|
||||
m.operationKind = intent.Kind
|
||||
m.operationIntent = intent
|
||||
m.operationUpdates = stream.Updates
|
||||
m.operationProgress = make(map[string]app.TargetProgressPhase)
|
||||
m.applying = intent.Kind == app.SetTargetDesired || intent.Kind == app.ActivateProfile
|
||||
m.refreshing = intent.Kind == app.RefreshTargets
|
||||
m.profileTransition = intent.Kind == app.ActivateProfile
|
||||
m.progressTarget = ""
|
||||
m.operationStatus = app.OperationStatus{Kind: app.StatusProgress, Summary: operationStartingSummary(intent.Kind)}
|
||||
return waitForOperation(stream.Updates)
|
||||
return command
|
||||
}
|
||||
|
||||
func operationStartingSummary(kind app.IntentKind) string {
|
||||
switch kind {
|
||||
case app.RefreshTargets:
|
||||
return "Checking Target availability and state…"
|
||||
case app.ActivateProfile:
|
||||
return "Changing Active Profile…"
|
||||
type mutationAvailability struct {
|
||||
available bool
|
||||
reason string
|
||||
compactReason string
|
||||
}
|
||||
|
||||
// targetMutationAvailability is the sole policy used by both input handling
|
||||
// and rendering, so the displayed disabled reason cannot drift from behavior.
|
||||
func targetMutationAvailability(selected app.TargetSnapshot, selectedOK bool, activeProfile string, operationBlocked bool) mutationAvailability {
|
||||
switch {
|
||||
case operationBlocked:
|
||||
return mutationAvailability{reason: "Wait for the current mutation to finish before starting another modification", compactReason: "mutation in progress"}
|
||||
case !selectedOK:
|
||||
return mutationAvailability{reason: "Target mutation is disabled because no Target is selected", compactReason: "no Target selected"}
|
||||
case selected.DetectionPhase == app.TargetChecking:
|
||||
return mutationAvailability{reason: "Target mutation is disabled while this Target is still being checked", compactReason: "checking Target"}
|
||||
case selected.DetectionPhase == app.TargetDetectionFailed:
|
||||
return mutationAvailability{reason: "Target mutation is disabled because this Target check failed; refresh Targets to retry", compactReason: "Target check failed"}
|
||||
case !selected.Available:
|
||||
return mutationAvailability{reason: "Target mutation is disabled because this Target is unavailable", compactReason: "Target unavailable"}
|
||||
case (selected.State == target.Disabled || selected.State == target.External) && activeProfile == "":
|
||||
return mutationAvailability{reason: "Target mutation is disabled because there is no Active Profile; open Profile management first", compactReason: "no Active Profile"}
|
||||
default:
|
||||
return "Starting Target mutation…"
|
||||
return mutationAvailability{available: true}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) finishOperation() {
|
||||
m.operationActive = false
|
||||
m.operationKind = 0
|
||||
m.operationIntent = app.Intent{}
|
||||
m.operationUpdates = nil
|
||||
m.applying = false
|
||||
m.refreshing = false
|
||||
m.profileTransition = false
|
||||
m.progressTarget = ""
|
||||
}
|
||||
|
||||
func isTerminalOperationPhase(phase app.OperationPhase) bool {
|
||||
return phase == app.OperationAwaitingConfirmation || phase == app.OperationSucceeded || phase == app.OperationFailed || phase == app.OperationBlocked
|
||||
}
|
||||
|
||||
func (m *Model) applyOperationUpdate(update app.OperationUpdate) {
|
||||
m.snapshot = update.Snapshot
|
||||
m.retainTargetSelection()
|
||||
for _, progress := range update.Progress {
|
||||
m.operationProgress[progress.TargetID] = progress.Phase
|
||||
if progress.Phase == app.TargetProgressRunning {
|
||||
m.progressTarget = progress.TargetID
|
||||
} else if m.progressTarget == progress.TargetID {
|
||||
m.progressTarget = ""
|
||||
}
|
||||
}
|
||||
if update.Status.Kind != 0 {
|
||||
m.operationStatus = update.Status
|
||||
}
|
||||
if update.Status.Kind == app.StatusError {
|
||||
current := update.Status
|
||||
if current.Detail == "" && update.Err != nil {
|
||||
current.Detail = update.Err.Error()
|
||||
}
|
||||
m.currentError = ¤t
|
||||
m.errorExpanded = false
|
||||
} else if update.Phase == app.OperationSucceeded && m.currentError != nil && update.Status.TargetID != "" && update.Status.TargetID == m.currentError.TargetID {
|
||||
m.currentError = nil
|
||||
m.errorExpanded = false
|
||||
}
|
||||
if update.FirstFailedTargetID != "" {
|
||||
m.selectedTargetID = update.FirstFailedTargetID
|
||||
m.detailsOffset = 0
|
||||
}
|
||||
if isTerminalOperationPhase(update.Phase) && update.Phase != app.OperationAwaitingConfirmation {
|
||||
m.pendingActivation = app.Intent{}
|
||||
}
|
||||
if update.Phase == app.OperationAwaitingConfirmation && update.Confirmation != nil {
|
||||
intent := m.operationIntent
|
||||
m.confirmation = update.Confirmation
|
||||
m.page = mainPage
|
||||
if intent.Kind == app.ActivateProfile {
|
||||
m.pendingActivation = intent
|
||||
m.profileConfirm = &intent
|
||||
} else {
|
||||
m.targetConfirm = &intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isQuitKey(key tea.KeyMsg) bool {
|
||||
switch key.String() {
|
||||
case "q", "esc", "ctrl+c":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) deferredQuit() tea.Cmd {
|
||||
if m.quitRequested {
|
||||
return tea.Quit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.targetConfirm != nil || m.profileConfirm != nil {
|
||||
intent := m.targetConfirm
|
||||
if intent == nil {
|
||||
intent = m.profileConfirm
|
||||
}
|
||||
if m.operation.confirmation != nil {
|
||||
switch key.String() {
|
||||
case "y":
|
||||
confirmed := *intent
|
||||
confirmed.Confirm = true
|
||||
m.targetConfirm = nil
|
||||
m.profileConfirm = nil
|
||||
m.pendingActivation = app.Intent{}
|
||||
m.confirmation = nil
|
||||
confirmed, ok := m.operation.confirmedIntent()
|
||||
if !ok {
|
||||
return m, nil
|
||||
}
|
||||
m.status = ""
|
||||
return m, m.startOperation(confirmed)
|
||||
case "enter", "n", "esc", "q":
|
||||
m.targetConfirm = nil
|
||||
m.profileConfirm = nil
|
||||
m.pendingActivation = app.Intent{}
|
||||
m.confirmation = nil
|
||||
m.operationStatus = app.OperationStatus{}
|
||||
case "enter", "n", "esc":
|
||||
m.operation.cancelConfirmation()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if key.String() == "e" && m.currentError != nil {
|
||||
m.errorExpanded = !m.errorExpanded
|
||||
if key.String() == "e" && m.operation.currentError != nil {
|
||||
m.operation.errorExpanded = !m.operation.errorExpanded
|
||||
m.operation.errorOffset = 0
|
||||
return m, nil
|
||||
}
|
||||
if key.String() == "x" && m.currentError != nil {
|
||||
m.currentError = nil
|
||||
m.errorExpanded = false
|
||||
if key.String() == "x" && m.operation.currentError != nil {
|
||||
m.operation.acknowledgeError()
|
||||
return m, nil
|
||||
}
|
||||
if m.operation.errorExpanded {
|
||||
switch key.String() {
|
||||
case "pgup":
|
||||
m.operation.errorOffset = max(0, m.operation.errorOffset-5)
|
||||
return m, nil
|
||||
case "pgdown":
|
||||
m.operation.errorOffset += 5
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
catalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
switch key.String() {
|
||||
case "p":
|
||||
if m.operationActive || m.quitRequested {
|
||||
m.operationStatus = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before opening Profile management"}
|
||||
if m.operation.active || m.operation.quitRequested {
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before opening Profile management"}
|
||||
} else {
|
||||
m.page = profilesPage
|
||||
m.selected = 0
|
||||
m.status = ""
|
||||
if m.operation.status.Kind != app.StatusError {
|
||||
m.operation.status = app.OperationStatus{}
|
||||
}
|
||||
}
|
||||
case "?":
|
||||
if !m.quitRequested {
|
||||
m.page = helpPage
|
||||
}
|
||||
m.page = helpPage
|
||||
case "r":
|
||||
if m.operationActive || m.quitRequested {
|
||||
m.operationStatus = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before refreshing Targets"}
|
||||
if m.operation.active || m.operation.quitRequested {
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before refreshing Targets"}
|
||||
} else {
|
||||
return m, m.startOperation(app.Intent{Kind: app.RefreshTargets})
|
||||
}
|
||||
@@ -607,41 +355,21 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.detailsOffset += 5
|
||||
}
|
||||
case " ":
|
||||
if m.operationActive || m.quitRequested {
|
||||
m.operationStatus = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before starting another modification"}
|
||||
break
|
||||
}
|
||||
selected, ok := catalog.selected(m.selectedTargetID)
|
||||
if !ok || !selected.Available || selected.DetectionPhase != app.TargetReady {
|
||||
availability := targetMutationAvailability(selected, ok, m.snapshot.ActiveProfile, m.operation.active || m.operation.quitRequested)
|
||||
if !availability.available {
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: availability.reason}
|
||||
break
|
||||
}
|
||||
desired := selected.State == target.Disabled || selected.State == target.External
|
||||
intent := app.Intent{Kind: app.SetTargetDesired, TargetID: selected.ID, Enabled: desired}
|
||||
return m, m.startOperation(intent)
|
||||
case "esc":
|
||||
if !m.operationActive {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateProfiles(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.profileConfirm != nil {
|
||||
switch key.String() {
|
||||
case "y":
|
||||
intent := *m.profileConfirm
|
||||
intent.Confirm = true
|
||||
m.profileConfirm = nil
|
||||
m.pendingActivation = app.Intent{}
|
||||
return m.beginProfileActivation(intent)
|
||||
case "n", "esc", "q":
|
||||
m.profileConfirm = nil
|
||||
m.pendingActivation = app.Intent{}
|
||||
m.status = ""
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.confirm {
|
||||
switch key.String() {
|
||||
case "y":
|
||||
@@ -778,10 +506,9 @@ func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m Model) beginProfileActivation(intent app.Intent) (tea.Model, tea.Cmd) {
|
||||
if m.busy() {
|
||||
m.operationStatus = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before changing the Active Profile"}
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before changing the Active Profile"}
|
||||
return m, nil
|
||||
}
|
||||
m.pendingActivation = intent
|
||||
m.page = mainPage
|
||||
m.selected = 0
|
||||
return m, m.startOperation(intent)
|
||||
@@ -790,11 +517,6 @@ func (m Model) beginProfileActivation(intent app.Intent) (tea.Model, tea.Cmd) {
|
||||
func (m *Model) apply(intent app.Intent) {
|
||||
result := m.application.Dispatch(intent)
|
||||
m.snapshot = result.Snapshot
|
||||
if result.Confirmation != "" {
|
||||
m.profileConfirm = &intent
|
||||
m.status = result.Confirmation
|
||||
return
|
||||
}
|
||||
if m.selected >= len(m.snapshot.Profiles) && m.selected > 0 {
|
||||
m.selected--
|
||||
}
|
||||
@@ -842,19 +564,15 @@ func (m Model) View() string {
|
||||
detailsOffset: m.detailsOffset,
|
||||
form: m.form,
|
||||
confirm: m.confirm,
|
||||
targetConfirm: m.targetConfirm != nil,
|
||||
profileConfirm: m.profileConfirm != nil,
|
||||
confirmation: m.confirmation,
|
||||
confirmation: m.operation.confirmation,
|
||||
status: m.status,
|
||||
operationStatus: m.operationStatus,
|
||||
currentError: m.currentError,
|
||||
errorExpanded: m.errorExpanded,
|
||||
exitDeferred: m.quitRequested && m.operationInFlight(),
|
||||
operationActive: m.operationActive,
|
||||
operationKind: m.operationKind,
|
||||
applying: m.applying,
|
||||
profileTransition: m.profileTransition,
|
||||
progressTarget: m.progressTarget,
|
||||
operationStatus: m.operation.status,
|
||||
currentError: m.operation.currentError,
|
||||
errorExpanded: m.operation.errorExpanded,
|
||||
errorOffset: m.operation.errorOffset,
|
||||
exitDeferred: m.operation.quitRequested && m.operationInFlight(),
|
||||
operationActive: m.operation.active,
|
||||
operationProgress: m.operation.progress,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user