Skip to content
Open
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
2 changes: 1 addition & 1 deletion cmd/confirmation.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ var confirmationShowCmd = &cobra.Command{
}

func confirmerShow(c *protobuf.Confirmer, for_ string) {
empty := false
empty := true
if c.Confirmed != "" {
empty = false
fmt.Printf("Confirmation submitted for %s: %s\n", for_, c.Confirmed)
Expand Down
208 changes: 198 additions & 10 deletions cmd/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@ package cmd

import (
"fmt"
"log"
"time"

"fyne.io/systray"
"github.com/dustin/go-humanize"
"github.com/gen2brain/beeep"
"github.com/nlewo/comin/cmd/icons"
"github.com/nlewo/comin/internal/builder"
"github.com/nlewo/comin/internal/client"
"github.com/nlewo/comin/pkg/protobuf"
"github.com/nlewo/comin/internal/manager"
"github.com/nlewo/comin/internal/store"
"github.com/nlewo/comin/pkg/protobuf"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var title string

const buildTitle = "build"
const deployTitle = "deploy"

var desktopCmd = &cobra.Command{
Use: "desktop",
Short: "Send desktop notifications ",
Expand All @@ -42,8 +48,14 @@ var desktopCmd = &cobra.Command{
if err != nil {
logrus.Fatal(err)
}
err = client.Events(handler)
log.Fatalf("failed to consume to the event stream: %s", err)
tray, _ := (cmd.Flags().GetBool("tray"))
if tray {
logrus.Println("Loading tray.")
systray.Run(func() { onTrayReady(client) }, onTrayExit)
} else {
err = client.Events(desktopEventHandler)
logrus.Fatalf("failed to consume to the event stream: %s", err)
}
}
},
}
Expand All @@ -55,33 +67,35 @@ func scenario() {
SelectedBranchName: "main",
}
e := protobuf.Event{Type: &protobuf.Event_BuildStartedType{BuildStartedType: &protobuf.Event_BuildStarted{Generation: &g}}}
handler(&e) // nolint: errcheck
desktopEventHandler(&e) // nolint: errcheck
time.Sleep(time.Second)

d := protobuf.Deployment{
Status: store.StatusToString(store.Init),
}
e = protobuf.Event{Type: &protobuf.Event_DeploymentStartedType{DeploymentStartedType: &protobuf.Event_DeploymentStarted{Deployment: &d}}}
handler(&e) // nolint: errcheck
desktopEventHandler(&e) // nolint: errcheck
time.Sleep(time.Second)

d = protobuf.Deployment{
Status: store.StatusToString(store.Done),
}
e = protobuf.Event{Type: &protobuf.Event_DeploymentFinishedType{DeploymentFinishedType: &protobuf.Event_DeploymentFinished{Deployment: &d}}}
handler(&e) // nolint: errcheck
desktopEventHandler(&e) // nolint: errcheck

time.Sleep(time.Second)
e = protobuf.Event{Type: &protobuf.Event_RebootRequired_{RebootRequired: &protobuf.Event_RebootRequired{Deployment: &d}}}
handler(&e) // nolint: errcheck
desktopEventHandler(&e) // nolint: errcheck
}

func handler(event *protobuf.Event) error {
func generateEventMessage(event *protobuf.Event) string {
var message string
logrus.Debugf("received event: %s", event)
switch v := event.Type.(type) {
default:
logrus.Errorf("unexpected type %T", v)
case *protobuf.Event_Fetched_:
case *protobuf.Event_ManagerState_:
case *protobuf.Event_Suspend_:
message = "The agent is suspended."
case *protobuf.Event_Resume_:
Expand Down Expand Up @@ -124,19 +138,193 @@ func handler(event *protobuf.Event) error {
}
case *protobuf.Event_RebootRequired_:
message = "The machine needs to be rebooted to take the deployment into account."
case *protobuf.Event_ConfirmationSubmittedType:
uuid := event.Type.(*protobuf.Event_ConfirmationSubmittedType).ConfirmationSubmittedType.GetUuid()
message = fmt.Sprintf("A confirmation request was submitted. %s", uuid)
}
return message
}

func desktopEventHandler(event *protobuf.Event) error {
message := generateEventMessage(event)
displayNotification(message)
return nil
}

func displayNotification(message string) {
if message != "" {
logrus.Println(fmt.Sprintf("Displaying notification: %s", message))
err := beeep.Notify(title, message, []byte{})
if err != nil {
logrus.Errorf("failed to send the notification: %s", err)
}
}
return nil
}

func onTrayReady(client client.Client) {
systray.SetIcon(icons.GetIcon(client))
systray.SetTitle("comin")
systray.SetTooltip("comin")
go refreshAvailableConfirmationsLoop(client)
go client.Events(trayEventHandler(client)) // nolint: errcheck
}

func onTrayExit() {}

func trayEventHandler(client client.Client) func(*protobuf.Event) error {
return func(event *protobuf.Event) error {
// Reuse the notification handler
go desktopEventHandler(event) // nolint: errcheck
// Watch the events for specifically the confirmation events so we can update the tray icon.
switch event.Type.(type) {
case *protobuf.Event_ConfirmationSubmittedType:
systray.SetIcon(icons.GetIcon(client))
case *protobuf.Event_ConfirmationConfirmedType:
systray.SetIcon(icons.GetIcon(client))
case *protobuf.Event_ConfirmationCancelledType:
systray.SetIcon(icons.GetIcon(client))
}
return nil
}
}

func refreshAvailableConfirmationsLoop(client client.Client) {
buildItem, deployItem := initializeTrayMenu(client)
// Reload statuses when the tray is opened.
for range systray.TrayOpenedCh {
status, err := client.GetManagerState()
if err != nil {
logrus.Errorln("Failed to retrieve manager state.")
}
logrus.Debugln(fmt.Sprintf("status is %s", status))
if status == nil {
updatePendingItem(nil, buildTitle, buildItem)
updatePendingItem(nil, deployTitle, deployItem)
} else {
updatePendingItem(status.BuildConfirmer, buildTitle, buildItem)
updatePendingItem(status.DeployConfirmer, deployTitle, deployItem)
}
// Also refresh the icon in case it's desynced for any reason.
systray.SetIcon(icons.GetIcon(client))
}
}

func generateTrayMessage(c *protobuf.Confirmer, for_ string) (string, bool) {
if c.Confirmed != "" {
return fmt.Sprintf("%s: Confirmed: %s.", for_, c.Confirmed), false
} else if c.Submitted != "" {
base := fmt.Sprintf("%s: Confirmation needed: %s.", for_, c.Submitted)
if c.Mode == int64(manager.Auto) {
return fmt.Sprintf("%s\nAuto confirmation in %s\n",
base,
humanize.Time(c.AutoconfirmStartedAt.AsTime().Add(time.Duration(c.AutoconfirmDuration*int64(time.Second))))),
true
} else {
return base, true
}
} else {
return fmt.Sprintf("%s: No confirmations needed.", for_), false
}
}

// Initialize the items in the tray menu.
func initializeTrayMenu(client client.Client) (*systray.MenuItem, *systray.MenuItem) {
// Header
addMenuItem("comin", "", func() {}).Disable()
systray.AddSeparator()

// Status items
var buildItem *systray.MenuItem
var deployItem *systray.MenuItem
status, err := client.GetManagerState()
if err != nil {
logrus.Errorln("Failed to retrieve manager state.")
}
var buildConfirmer *protobuf.Confirmer
var deployConfirmer *protobuf.Confirmer
// If the status can't be loaded, we still want to create the tray items.
if status != nil {
buildConfirmer = status.BuildConfirmer
deployConfirmer = status.DeployConfirmer
}
buildItem = makePendingItem(buildConfirmer, buildTitle, func() {
status, err := client.GetManagerState()
if err != nil {
displayNotification("Failed to confirm build.")
logrus.Errorln("Failed to retrieve manager state.")
} else {
if status.BuildConfirmer.Submitted != "" {
client.Confirm(status.BuildConfirmer.Submitted, buildTitle) // nolint: errcheck
}
}
})
deployItem = makePendingItem(deployConfirmer, deployTitle, func() {
status, err := client.GetManagerState()
if err != nil {
displayNotification("Failed to confirm deployment.")
logrus.Errorln("Failed to retrieve manager state.")
} else {
if status.DeployConfirmer.Submitted != "" {
client.Confirm(status.DeployConfirmer.Submitted, deployTitle) // nolint: errcheck
}
}
})
// Quit item
systray.AddSeparator()
addMenuItem("Quit", "Quit the tray application", func() {
logrus.Println("Quit was pressed. Closing.")
systray.Quit()
})
return buildItem, deployItem
}

func updatePendingItem(c *protobuf.Confirmer, for_ string, item *systray.MenuItem) {
if c == nil {
item.SetTitle(fmt.Sprintf("%s: Failed to connect to comin service.", for_))
item.Disable()
return
}
title, shouldEnable := generateTrayMessage(c, for_)
item.SetTitle(title)
if shouldEnable {
item.Enable()
} else {
item.Disable()
}
}

func makePendingItem(c *protobuf.Confirmer, for_ string, callback func()) *systray.MenuItem {
if c == nil {
item := addMenuItem(fmt.Sprintf("%s: Failed to connect to comin service.", for_), "", callback)
item.Disable()
return item
}
title, shouldEnable := generateTrayMessage(c, for_)
item := addMenuItem(title, "", callback)
if shouldEnable {
item.Enable()
} else {
item.Disable()
}
return item
}

// Helper function to create a tray item with a callback.
// The goroutine is never stopped, so only use for items that will not be removed from the tray.
func addMenuItem(title string, tooltip string, callback func()) *systray.MenuItem {
item := systray.AddMenuItem(title, tooltip)
go func() {
for range item.ClickedCh {
callback()
}
}()
return item
}

func init() {
desktopCmd.Flags().StringVarP(&title, "title", "", "comin", "the notification title")
desktopCmd.Flags().StringP("unix-socket-path", "", "/var/lib/comin/grpc.sock", "the GRPC Unix socket path")
desktopCmd.Flags().BoolP("test", "", false, "do not get events from the agent but from predefined scenari")
desktopCmd.Flags().BoolP("tray", "", false, "Show in the system tray")
rootCmd.AddCommand(desktopCmd)
}
Binary file added cmd/icons/bare.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions cmd/icons/icons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package icons

import (
_ "embed"

"github.com/nlewo/comin/internal/client"
"github.com/sirupsen/logrus"
)

type Icon []byte

//go:embed bare.png
var Bare Icon

//go:embed notify.png
var Notify Icon

func GetIcon(client client.Client) Icon {
status, err := client.GetManagerState()
if err != nil {
logrus.Errorln("Failed to retrieve manager state.")
return Notify
}

if status.BuildConfirmer.Submitted != "" || status.DeployConfirmer.Submitted != "" {
return Notify
}
return Bare
}
Binary file added cmd/icons/notify.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions docs/generated-module-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,35 @@ string



## services\.comin\.desktop\.tray



Whether to enable Whether to run the tray with the desktop service\. This tray item allows confirming deployments and builds…



*Type:*
boolean



*Default:*

```nix
false
```



*Example:*

```nix
true
```



## services\.comin\.exporter


Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (

require (
dario.cat/mergo v1.0.0 // indirect
fyne.io/systray v1.12.0 // indirect
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
Expand Down
4 changes: 4 additions & 0 deletions internal/manager/confirmer.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ func (c *Confirmer) start() {
switch Mode(c.state.Mode) {
case Manual:
logrus.Infof("confirmer: generation %s has been submitted", command.uuid)
e := &protobuf.Event_ConfirmationSubmitted{Uuid: command.uuid}
c.broker.Publish(&protobuf.Event{Type: &protobuf.Event_ConfirmationSubmittedType{ConfirmationSubmittedType: e}})
case Without:
logrus.Infof("confirmer: generation %s has been submitted and confirmed", command.uuid)
c.state.Confirmed = command.uuid
Expand All @@ -129,6 +131,8 @@ func (c *Confirmer) start() {
timer = c.timer.C
c.state.AutoconfirmStarted = wrapperspb.Bool(true)
c.state.AutoconfirmStartedAt = timestamppb.New(time.Now().UTC())
e := &protobuf.Event_ConfirmationSubmitted{Uuid: command.uuid}
c.broker.Publish(&protobuf.Event{Type: &protobuf.Event_ConfirmationSubmittedType{ConfirmationSubmittedType: e}})
}
case "confirm":
logrus.Infof("confirmer: generation %s has been confirmed", command.uuid)
Expand Down
1 change: 1 addition & 0 deletions nix/module-options.nix
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ in
default = "comin";
description = "The notification title.";
};
tray = mkEnableOption "Whether to run the tray with the desktop service. This tray item allows confirming deployments and builds.";
};
retention = mkOption {
description = "The deployments and profiles retention policyes.";
Expand Down
Loading