Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: notification daemon #31

Merged
merged 4 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dockercmd/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,15 @@ func (dc DockerClient) PingDocker() error {
return err
}

// used for testing only
func NewMockCli(cli *MockApi) DockerClient {
return DockerClient{
cli: cli,
containerListArgs: container.ListOptions{},
}
}

// util
func (dc DockerClient) GetListOptions() *container.ListOptions {
return &dc.containerListArgs
}
120 changes: 107 additions & 13 deletions tui/mainModel.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ type MainModel struct {
imageIdToNameMap map[string]string
// we use this error channel to report error for possibly long running tasks, like pruning
possibleLongRunningOpErrorChan chan error

// Channels for sending and receiving notifications, we use these to update list status messages
notificationChan chan notificationMetadata
}

// this ticker enables us to update Docker lists items every 500ms (unless set to different value in config)
Expand All @@ -87,6 +90,7 @@ func (m MainModel) Init() tea.Cmd {
fmt.Printf("Error connecting to Docker daemon.\nInfo: %s\n", err.Error())
os.Exit(1)
}

// initialize clipboard
// TODO: handle error
clipboard.Init()
Expand Down Expand Up @@ -128,6 +132,7 @@ func NewModel() MainModel {
mu: &sync.Mutex{},
},
imageIdToNameMap: make(map[string]string),
notificationChan: make(chan notificationMetadata, 10),
}
}

Expand All @@ -136,6 +141,17 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

var cmds []tea.Cmd

notificationLoop:
for {
select {
case notifcation := <-m.notificationChan:
cmd := (&m.TabContent[notifcation.listId].list).NewStatusMessage(notifcation.msg)
cmds = append(cmds, cmd)
default:
break notificationLoop
}
}

//check if error exists on error channel, if yes show the error in new dialog
select {
case newErr := <-m.possibleLongRunningOpErrorChan:
Expand Down Expand Up @@ -263,6 +279,11 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if err != nil {
m.possibleLongRunningOpErrorChan <- err
}

// send notification
imageId = strings.TrimPrefix(imageId, "sha256:")
notificationMsg := listStatusMessageStyle.Render(fmt.Sprintf("Run %s", imageId[:8]))
m.notificationChan <- NewNotification(m.activeTab, notificationMsg)
}()
}
case key.Matches(msg, ImageKeymap.Delete):
Expand All @@ -280,10 +301,10 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
curItem := m.getSelectedItem()

if curItem != nil {
containerId := curItem.(dockerRes).getId()
imageId := curItem.(dockerRes).getId()

if containerId != "" {
err := m.dockerClient.DeleteImage(containerId, image.RemoveOptions{
if imageId != "" {
err := m.dockerClient.DeleteImage(imageId, image.RemoveOptions{
Force: true,
PruneChildren: false,
})
Expand All @@ -292,6 +313,10 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
}
// send notification
imageId = strings.TrimPrefix(imageId, "sha256:")
msg := fmt.Sprintf("Deleted %s", imageId[:8])
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}
}

Expand Down Expand Up @@ -334,6 +359,9 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
copyToClipboard(id)
timeout_cmd := m.getActiveList().NewStatusMessage(listStatusMessageStyle.Render("ID copied!"))
cmds = append(cmds, timeout_cmd)

// send notification
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render("ID Copied"))
}

case key.Matches(msg, ImageKeymap.RunAndExec):
Expand Down Expand Up @@ -370,30 +398,64 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, ContainerKeymap.ToggleListAll):
m.dockerClient.ToggleContainerListAll()

listOpts := m.dockerClient.GetListOptions()

notifMsg := ""
if listOpts.All {
notifMsg = "List all enabled!"
} else {
notifMsg = "List all disabled!"
}

m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(notifMsg))

case key.Matches(msg, ContainerKeymap.ToggleStartStop):
curItem := m.getSelectedItem()
if curItem != nil {
containerId := curItem.(dockerRes).getId()
containerInfo := curItem.(containerItem)
containerId := containerInfo.getId()
err := m.dockerClient.ToggleStartStopContainer(containerId)

if err != nil {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
}

// send notification
msg := ""
if containerInfo.getState() == "running" {
msg = fmt.Sprintf("Stopped %s", containerId[:8])
} else {
msg = fmt.Sprintf("Started %s", containerId[:8])
}

m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}

case key.Matches(msg, ContainerKeymap.TogglePause):
curItem := m.getSelectedItem()
if curItem != nil {

containerId := curItem.(dockerRes).getId()
containerInfo := curItem.(containerItem)
containerId := containerInfo.getId()
err := m.dockerClient.TogglePauseResume(containerId)

if err != nil {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
}

// send notification
msg := ""
if containerInfo.getState() == "running" {
msg = "Paused " + containerId[:8]
} else {
msg = "Resumed " + containerId[:8]
}

m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}

case key.Matches(msg, ContainerKeymap.Restart):
curItem := m.getSelectedItem()
if curItem != nil {
Expand All @@ -404,7 +466,11 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
}

msg := fmt.Sprintf("Restarted %s", containerId[:8])
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}

case key.Matches(msg, ContainerKeymap.Delete):
curItem := m.getSelectedItem()
if containerInfo, ok := curItem.(dockerRes); ok {
Expand All @@ -427,6 +493,9 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
}

msg := fmt.Sprintf("Deleted %s", containerInfo.getId()[:8])
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}

case key.Matches(msg, ContainerKeymap.Prune):
Expand Down Expand Up @@ -474,8 +543,7 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
id := dres.getId()
copyToClipboard(id)

timeout_cmd := m.getActiveList().NewStatusMessage(listStatusMessageStyle.Render("ID copied!"))
cmds = append(cmds, timeout_cmd)
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render("Copied ID"))
}
}

Expand Down Expand Up @@ -508,8 +576,7 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
dres := currentItem.(dockerRes)
name := dres.getId()
copyToClipboard(name)
timeout_cmd := m.getActiveList().NewStatusMessage(listStatusMessageStyle.Render("ID copied!"))
cmds = append(cmds, timeout_cmd)
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render("Copied ID"))
}
}
}
Expand All @@ -533,7 +600,15 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if err != nil {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
/*
INFO: break from switch statement if there is an error, not doing so will continue exectuion
and send a notification, which is not valid behaviour
*/
break
}

msg := fmt.Sprintf("Deleted %s", containerId[:8])
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}

case dialogPruneContainers:
Expand All @@ -542,11 +617,16 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if userChoice["confirm"] == "Yes" {
// prune containers on a separate goroutine, since UI gets stuck otherwise(since this may take sometime)
go func() {
_, err := m.dockerClient.PruneContainers()
report, err := m.dockerClient.PruneContainers()

if err != nil {
m.possibleLongRunningOpErrorChan <- err
return
}

// send notification
msg := fmt.Sprintf("Pruned %d containers", len(report.ContainersDeleted))
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}()
}

Expand All @@ -556,12 +636,15 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if userChoice["confirm"] == "Yes" {
// run on a different go routine, same reason as above (for Prune containers)
go func() {
_, err := m.dockerClient.PruneImages()
// TODO: show report on screen
report, err := m.dockerClient.PruneImages()

if err != nil {
m.possibleLongRunningOpErrorChan <- err
return
}

msg := fmt.Sprintf("Pruned %d images", len(report.ImagesDeleted))
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}()
}

Expand All @@ -571,10 +654,14 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if userChoice["confirm"] == "Yes" {
// same reason as above, again
go func() {
_, err := m.dockerClient.PruneVolumes()
report, err := m.dockerClient.PruneVolumes()
if err != nil {
m.possibleLongRunningOpErrorChan <- err
return
}

msg := fmt.Sprintf("Pruned %d volumes", len(report.VolumesDeleted))
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}()
}

Expand All @@ -589,7 +676,10 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if err != nil {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
break
}

m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render("Deleted"))
}

case dialogRemoveImage:
Expand All @@ -607,7 +697,11 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if err != nil {
m.activeDialog = teadialog.NewErrorDialog(err.Error(), m.width)
m.showDialog = true
break
}
imageId = strings.TrimPrefix(imageId, "sha256:")
msg := fmt.Sprintf("Deleted %s", imageId[:8])
m.notificationChan <- NewNotification(m.activeTab, listStatusMessageStyle.Render(msg))
}
}

Expand Down
21 changes: 21 additions & 0 deletions tui/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package tui

import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
)

type notificationMetadata struct {
listId tabId
msg string
}

func NotifyList(list *list.Model, msg string) tea.Cmd {
return list.NewStatusMessage(msg)
}

func NewNotification(id tabId, msg string) notificationMetadata {
return notificationMetadata{
id, msg,
}
}
45 changes: 45 additions & 0 deletions tui/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package tui

import (
"strings"
"testing"

"github.com/ajayd-san/gomanagedocker/dockercmd"
"github.com/docker/docker/api/types"
"gotest.tools/v3/assert"
)

func TestNotifyList(t *testing.T) {
api := dockercmd.MockApi{}

containers := []types.Container{
{
Names: []string{"a"},
ID: "1",
SizeRw: 1e+9,
SizeRootFs: 2e+9,
State: "running",
Status: "",
},
}

api.SetMockContainers(containers)

mockcli := dockercmd.NewMockCli(&api)

CONTAINERS = 0
model := MainModel{
dockerClient: mockcli,
activeTab: 0,
TabContent: []listModel{
InitList(0),
},
}

t.Run("Notify test", func(t *testing.T) {
NotifyList(model.getActiveList(), "Kiryu")
got := model.View()
contains := "Kiryu"
assert.Check(t, strings.Contains(got, contains))
})
}
Binary file added vhs/gifs/notifications.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading