176 lines
4.7 KiB
Go
176 lines
4.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"charm.land/bubbles/v2/key"
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/ui"
|
|
)
|
|
|
|
type manageAction int
|
|
|
|
const (
|
|
actionNone manageAction = iota
|
|
actionBuild
|
|
actionDeleteImage
|
|
actionDeleteData
|
|
actionRemoveContainer
|
|
actionPrune
|
|
)
|
|
|
|
// confirmation is the armed destructive action awaiting a yes or no.
|
|
type confirmation struct {
|
|
action manageAction
|
|
subject target
|
|
prompt string
|
|
detail string
|
|
}
|
|
|
|
// handleManageKey is the second half of the `m` chord. Non-destructive actions
|
|
// run immediately; anything that deletes state goes through a confirmation, so
|
|
// a mistyped chord cannot destroy an instance.
|
|
func (m Model) handleManageKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
|
if key.Matches(msg, m.keys.Quit) {
|
|
m.tasks.CancelAll()
|
|
m.logs.Close()
|
|
return m, tea.Quit
|
|
}
|
|
if key.Matches(msg, m.keys.ManageAbort) || key.Matches(msg, m.keys.Manage) {
|
|
m.mode = modeNormal
|
|
m.status, m.statusLevel = "manage mode closed", levelInfo
|
|
m.relayout()
|
|
return m, nil
|
|
}
|
|
|
|
subject, found := m.currentTarget()
|
|
if !found {
|
|
m.mode = modeNormal
|
|
m.relayout()
|
|
return m, nil
|
|
}
|
|
|
|
switch {
|
|
case key.Matches(msg, m.keys.ManageBuild):
|
|
m.mode = modeNormal
|
|
m.relayout()
|
|
return m.buildLocally(subject)
|
|
|
|
case key.Matches(msg, m.keys.ManageImage):
|
|
if subject.localImage == "" {
|
|
m.status, m.statusLevel = subject.label+" has no local image to delete", levelWarn
|
|
return m, nil
|
|
}
|
|
return m.arm(actionDeleteImage, subject)
|
|
|
|
case key.Matches(msg, m.keys.ManageData):
|
|
if subject.volume == "" {
|
|
m.status, m.statusLevel = subject.label+" has no data volume to delete", levelWarn
|
|
return m, nil
|
|
}
|
|
return m.arm(actionDeleteData, subject)
|
|
|
|
case key.Matches(msg, m.keys.ManageStop):
|
|
return m.arm(actionRemoveContainer, subject)
|
|
|
|
case key.Matches(msg, m.keys.ManagePrune):
|
|
return m.arm(actionPrune, subject)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// arm moves into the confirmation mode with a prompt describing exactly what is
|
|
// about to be destroyed.
|
|
func (m Model) arm(action manageAction, subject target) (Model, tea.Cmd) {
|
|
confirm := confirmation{action: action, subject: subject}
|
|
switch action {
|
|
case actionDeleteImage:
|
|
confirm.prompt = "Delete image " + subject.localImage + "?"
|
|
confirm.detail = "The image is removed from Docker. Rebuild it later with R."
|
|
case actionDeleteData:
|
|
confirm.prompt = "Delete data volume " + subject.volume + "?"
|
|
confirm.detail = "Boards, users and integrations in this instance are lost. The container is removed first."
|
|
if size := m.volumeSize(subject.volume); size != "" {
|
|
confirm.detail += " (" + size + " reclaimed)"
|
|
}
|
|
case actionRemoveContainer:
|
|
confirm.prompt = "Remove container " + subject.container + "?"
|
|
confirm.detail = "The container is force-removed. Its data volume is kept."
|
|
case actionPrune:
|
|
stopped := 0
|
|
for _, container := range m.containers {
|
|
if !container.Running() {
|
|
stopped++
|
|
}
|
|
}
|
|
confirm.prompt = fmt.Sprintf("Remove %d stopped Homarr containers?", stopped)
|
|
confirm.detail = "Data volumes are kept."
|
|
default:
|
|
return m, nil
|
|
}
|
|
m.confirm = confirm
|
|
m.mode = modeConfirm
|
|
// The confirmation is the only prompt that should be speaking.
|
|
m.status = ""
|
|
m.relayout()
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) volumeSize(name string) string {
|
|
for _, volume := range m.volumes {
|
|
if volume.Name == name {
|
|
return volume.Size
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m Model) handleConfirmKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
|
switch {
|
|
case key.Matches(msg, m.keys.ManageAbort):
|
|
m.mode = modeNormal
|
|
m.confirm = confirmation{}
|
|
m.status, m.statusLevel = "canceled", levelInfo
|
|
m.relayout()
|
|
return m, nil
|
|
|
|
case key.Matches(msg, m.keys.ManageConfirm):
|
|
confirm := m.confirm
|
|
m.mode = modeNormal
|
|
m.confirm = confirmation{}
|
|
m.relayout()
|
|
switch confirm.action {
|
|
case actionDeleteImage:
|
|
return m.deleteImage(confirm.subject.localImage)
|
|
case actionDeleteData:
|
|
return m.deleteData(confirm.subject)
|
|
case actionRemoveContainer:
|
|
return m.removeContainer(confirm.subject.container)
|
|
case actionPrune:
|
|
return m.pruneStopped()
|
|
}
|
|
return m, nil
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// manageBar is the hint strip shown while the chord is armed.
|
|
func (m Model) manageBar() string {
|
|
subject, found := m.currentTarget()
|
|
label := "selection"
|
|
if found {
|
|
label = subject.label
|
|
}
|
|
return clip(ui.ModeBadge("MANAGE", ui.Warning)+" "+
|
|
ui.Body.Render(label)+" "+
|
|
m.helpView.ShortHelpView(m.keys.ManageHelp()), m.width)
|
|
}
|
|
|
|
// confirmBar is the destructive-action prompt. It is deliberately loud.
|
|
func (m Model) confirmBar() string {
|
|
prompt := ui.ModeBadge("CONFIRM", ui.Danger) + " " + ui.Heading.Render(m.confirm.prompt)
|
|
detail := ui.Help.Render(m.confirm.detail + " y confirm · n cancel")
|
|
return clip(prompt, m.width) + "\n" + clip(detail, m.width)
|
|
}
|