SwiftDialog interface (#22321)

SwiftDialog interface for use in orbit. Built for the upcoming out of box experience later this sprint

---------

Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
This commit is contained in:
Dante Catalfamo
2024-10-02 16:56:52 -04:00
committed by GitHub
co-authored by Jahziel Villasana-Espinoza
parent 05e271a449
commit cf8b3cd56e
2 changed files with 771 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
package swiftdialog
type SwiftDialogOptions struct {
// Set the Dialog title
Title string `json:"title,omitempty"`
// Text to use as subtitle when sending a system notification
Subtitle string `json:"subtitle,omitempty"`
// Set the dialog message
Message string `json:"message,omitempty"`
// Configure a pre-set window style
Style Style `json:"style,omitempty"`
// Set the message alignment
MessageAlignment Alignment `json:"messagealignment,omitempty"`
// Set the message position
MessagePosition Position `json:"messageposition,omitempty"`
// Enable help button with content
HelpMessage string `json:"helpmessage,omitempty"`
// Set the dialog icon, accepts file path, url, or builtin
// See https://github.com/swiftDialog/swiftDialog/wiki/Customising-the-Icon
Icon string `json:"icon"`
// Set the dialog icon size
IconSize uint `json:"iconsize,omitempty"`
// Set the dialog icon transparancy
IconAlpha uint `json:"iconalpha,omitempty"`
// Set an image to display as an overlay to Icon, accepts file path or url
OverlayIcon string
// Enable banner image, accepts file path or url
BannerImage string `json:"bannerimage,omitempty"`
// Enable title within banner area
BannerTitle string `json:"bannertitle,omitempty"`
// Set text to display in banner area
BannerText string `json:"bannertext,omitempty"`
// Set the label for Button1
Button1Text string `json:"button1text,omitempty"`
// Set the Button1 action, accepts url
Button1Action string `json:"button1action,omitempty"`
// Displays Button2 with text
Button2Text string `json:"button2text,omitempty"`
// Custom Actions For Button 2 Is Not Implemented
Button2Action string `json:"button2action,omitempty"`
// Displays info button with text
InfoButtonText string `json:"infobuttontext,omitempty"`
// Set the info button action, accepts URL
InfoButtonAction string `json:"infobuttonaction,omitempty"`
// Configure how the button area is displayed
ButtonStyle ButtonStyle `json:"buttonstyle,omitempty"`
// Select Lists and Radio Buttons
SelectItems []SelectItems `json:"selectitems,omitempty"`
// Lets you modify the title text of the dialog
TitleFont string `json:"titlefont,omitempty"`
// Set the message font of the dialog
MessageFont string `json:"messagefont,omitempty"`
// Enable a textfield with the specified label
TextField []TextField `json:"textfield,omitempty"`
// Enable a checkbox with the specified label
Checkbox []Checkbox `json:"checkbox,omitempty"`
// Change the appearance of checkboxes
CheckboxStyle CheckboxStyle `json:"checkboxstyle,omitempty"`
// Enable countdown timer (in seconds)
Timer uint `json:"timer,omitempty"`
// Enable interactive progress bar
Progress uint `json:"progress,omitempty"`
// Enable the progress text
ProgressText string `json:"progresstext,omitempty"`
// Display an image
Image []Image `json:"image,omitempty"`
// Set dialog window width
Width uint `json:"width,omitempty"`
// Set dialog window height
Height string `json:"height,omitempty"`
// Set a dialog background image, accepts file path
Background string `json:"background,omitempty"`
// Set background image transparancy
BackgroundAlpha uint `json:"bgalpha,omitempty"`
// Set background image position
BackgroundPosition FullPosition `json:"bgposition,omitempty"`
// Set background image fill type
BackgroundFill BackgroundFill `json:"bgfill,omitempty"`
// Enable background image scaling
BackgroundScale BackgroundFill `json:"bgscale,omitempty"`
// Set dialog window position
Position FullPosition `json:"position,omitempty"`
// Set dialog window position offset
PositionOffset uint `json:"positionoffset,omitempty"`
// Display a video, accepts file path or url
Video string `json:"video,omitempty"`
// Display a caption underneath a video
VideoCaption string `json:"videocaption,omitempty"`
// Enable a list item with the specified label
ListItem []ListItem `json:"listitem,omitempty"`
// Set list style
ListStyle ListStyle `json:"liststyle,omitempty"`
// Display <text> in place of info button
InfoText string `json:"infotext,omitempty"`
// Display <text> in info box
InfoBox string `json:"infobox,omitempty"`
// Set dialog quit key
QuitKey string `json:"quitkey,omitempty"`
// Display a web page, accepts url
WebContent string `json:"webcontent,omitempty"`
// Use the specified authentication key to allow dialog to launch
Key string `json:"key,omitempty"`
// Generate a SHA256 value
Checksum string `json:"checksum,omitempty"`
// Open a file and display the contents as it is being written, accepts file path
DisplayLog string `json:"displaylog,omitempty"`
// Change the order in which some items are displayed, comma separated list
ViewOrder string `json:"vieworder,omitempty"`
// Set the preferred window appearance
Appearance Appearance `json:"appearance,omitempty"`
// Disable Button1
Button1Disabled bool `json:"button1disabled,omitempty"`
// Disable Button2
Button2Disabled bool `json:"button2disabled,omitempty"`
// Displays Button2
Button2 bool `json:"button2,omitempty"`
// Displays info button
InfoButton bool `json:"infobutton,omitempty"`
// Print version string
Version string `json:"version,omitempty"`
// Hides the icon from view
HideIcon bool `json:"hideicon,omitempty"`
// Set icon to be in the centre
CentreIcon bool `json:"centreicon,omitempty"`
// Hide countdown timer if enabled
HideTimerBar bool `json:"hidetimerbar,omitempty"`
// Enable video autoplay
Autoplay bool `json:"autoplay,omitempty"`
// Blur screen content behind dialog window
BlurScreen bool `json:"blurscreen,omitempty"`
// Send a system notification
Notification string `json:"notification,omitempty"`
// Enable dialog to be moveable
Moveable bool `json:"moveable,omitempty"`
// Enable dialog to be always positioned on top of other windows
OnTop bool `json:"ontop,omitempty"`
// Enable 25% decrease in default window size
Small bool `json:"small,omitempty"`
// Enable 25% increase in default window size
Big bool `json:"big,omitempty"`
// Enable full screen view
Fullscreen bool `json:"fullscreen,omitempty"`
// Quit when info button is selected
QuitonInfo bool `json:"quitoninfo,omitempty"`
// Enable mini mode
Mini bool `json:"mini,omitempty"`
// Enable presentation mode
Presentation bool `json:"presentation,omitempty"`
// Enables window buttons [close,min,max]
WindowButtons string `json:"windowbuttons,omitempty"`
// Enable the dialog window to be resizable
Resizable *bool `json:"resizable,omitempty"`
// Enable the dialog window to appear on all screens
ShowOnAllScreens *bool `json:"showonallscreens,omitempty"`
// Enable the dialog window to be shown at login
LoginWindow bool `json:"loginwindow,omitempty"`
// Hides the default behaviour of Return ↵ and Esc ⎋ keys
HideDefaultKeyboardAction bool `json:"hidedefaultkeyboardaction,omitempty"`
}
type Style string
const (
StylePresentation Style = "presentation"
StyleMini Style = "mini"
StyleCentered Style = "centered"
StyleAlert Style = "alert"
StyleCaution Style = "caution"
StyleWarning Style = "warning"
)
type Alignment string
const (
AlignmentLeft Alignment = "left"
AlignmentCenter Alignment = "center"
AlignmentRight Alignment = "right"
)
type Position string
const (
PositionTop Position = "top"
PositionCenter Position = "center"
PositionBottom Position = "bottom"
)
type ButtonStyle string
const (
ButtonStyleCenter ButtonStyle = "center"
ButtonStyleStack ButtonStyle = "stack"
)
type Checkbox struct {
Label string `json:"label"`
Checked bool `json:"checked"`
Disabled bool `json:"disabled"`
Icon string `json:"icon,omitempty"`
EnableButton1 bool `json:"enableButton1,omitempty"`
}
type Image struct {
// ImageName is a file path or url
ImageName string `json:"imagename"`
Caption string `json:"caption"`
}
type FullPosition string
const (
FullPositionTopLeft FullPosition = "topleft"
FullPositionLeft FullPosition = "left"
FullPositionBottomLeft FullPosition = "bottomleft"
FullPositionTop FullPosition = "top"
FullPositionCenter FullPosition = "center"
FullPositionBottom FullPosition = "bottom"
FullPositionTopRight FullPosition = "topright"
FullPositionRight FullPosition = "right"
FullPositionBottomRight FullPosition = "bottomright"
)
type BackgroundFill string
const (
BackgroundFillFill BackgroundFill = "fill"
BackgroundFillFit BackgroundFill = "fit"
)
type ListStyle string
const (
ListStyleExpanded ListStyle = "expanded"
ListStyleCompact ListStyle = "compact"
)
type Appearance string
const (
AppearanceDark Appearance = "dark"
AppearanceLight Appearance = "light"
)
type ListItem struct {
Title string `json:"title"`
Icon string `json:"icon,omitempty"`
Status Status `json:"status,omitempty"`
StatusText string `json:"statustext,omitempty"`
}
type Status string
const (
StatusNone Status = ""
StatusWait Status = "wait"
StatusSuccess Status = "success"
StatusFail Status = "fail"
StatusError Status = "error"
StatusPending Status = "pending"
StatusProgress Status = "progress"
)
type TextField struct {
Title string `json:"title"`
Confirm bool `json:"confirm,omitempty"`
Editor bool `json:"editor,omitempty"`
FileSelect bool `json:"fileselect,omitempty"`
FileType string `json:"filetype,omitempty"`
Name string `json:"name,omitempty"`
Prompt string `json:"prompt,omitempty"`
Regex string `json:"regex,omitempty"`
RegexError string `json:"regexerror,omitempty"`
Required bool `json:"required,omitempty"`
Secure bool `json:"secure,omitempty"`
Value string `json:"value,omitempty"`
}
type CheckboxStyle struct {
Style string `json:"style"`
Size CheckboxStyleSize `json:"size"`
}
type CheckboxStyleStyle string
const (
CheckboxDefault CheckboxStyleStyle = "default"
CheckboxCheckbox CheckboxStyleStyle = "checkbox"
CheckboxSwitch CheckboxStyleStyle = "switch"
)
type CheckboxStyleSize string
const (
CheckboxMini CheckboxStyleSize = "mini"
CheckboxSmall CheckboxStyleSize = "small"
CheckboxRegular CheckboxStyleSize = "regular"
CheckboxLarge CheckboxStyleSize = "large"
)
type SelectItems struct {
Title string `json:"title"`
Values []string `json:"values"`
Default string `json:"default,omitempty"`
Style SelectItemsStyle `json:"style,omitempty"`
Required bool `json:"required,omitempty"`
}
type SelectItemsStyle string
const (
SelectItemsStyleDropdown SelectItemsStyle = ""
SelectItemsStyleRadio SelectItemsStyle = "radio"
)
+458
View File
@@ -0,0 +1,458 @@
package swiftdialog
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"strings"
)
// SwiftDialog really wants the command file to be mode 666 for some reason
// https://github.com/swiftDialog/swiftDialog/wiki/Gotchas
var CommandFilePerms = fs.FileMode(0666)
var ErrKilled = errors.New("process killed")
var ErrWindowClosed = errors.New("window closed")
type SwiftDialog struct {
cancel context.CancelCauseFunc
cmd *exec.Cmd
commandFile *os.File
context context.Context
output *bytes.Buffer
exitCode ExitCode
exitErr error
done chan struct{}
}
type SwiftDialogExit struct {
ExitCode ExitCode
Output map[string]any
}
type ExitCode int
const (
ExitButton1 ExitCode = 0
ExitButton2 ExitCode = 2
ExitInfoButton ExitCode = 3
ExitTimer ExitCode = 4
ExitQuitCommand ExitCode = 5
ExitQuitKey ExitCode = 10
ExitKeyAuthFailed ExitCode = 30
ExitImageResourceNotFound ExitCode = 201
ExitFileNotFound ExitCode = 202
)
func Create(ctx context.Context, swiftDialogBin string, options *SwiftDialogOptions) (*SwiftDialog, error) {
commandFile, err := os.CreateTemp("", "swiftDialogCommand")
if err != nil {
return nil, err
}
jsonBytes, err := json.Marshal(options)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancelCause(ctx)
if err := commandFile.Chmod(CommandFilePerms); err != nil {
commandFile.Close()
os.Remove(commandFile.Name())
cancel(errors.New("could not create command file"))
return nil, err
}
cmd := exec.CommandContext( //nolint:gosec
ctx,
swiftDialogBin,
"--jsonstring", string(jsonBytes),
"--commandfile", commandFile.Name(),
"--json",
)
outBuf := &bytes.Buffer{}
cmd.Stdout = outBuf
err = cmd.Start()
if err != nil {
cancel(errors.New("could not start swiftDialog"))
return nil, err
}
sd := &SwiftDialog{
cancel: cancel,
cmd: cmd,
commandFile: commandFile,
context: ctx,
done: make(chan struct{}),
output: outBuf,
}
go func() {
if err := cmd.Wait(); err != nil {
errExit := &exec.ExitError{}
if errors.As(err, &errExit) && strings.Contains(errExit.Error(), "exit status") {
sd.exitCode = ExitCode(errExit.ExitCode())
} else {
sd.exitErr = fmt.Errorf("waiting for swiftDialog: %w", err)
}
}
close(sd.done)
cancel(ErrWindowClosed)
}()
return sd, nil
}
func (s *SwiftDialog) finished() {
<-s.done
}
func (s *SwiftDialog) Kill() error {
s.cancel(ErrKilled)
s.finished()
if err := s.cleanup(); err != nil {
return fmt.Errorf("Close cleaning up after swiftDialog: %w", err)
}
return nil
}
func (s *SwiftDialog) cleanup() error {
s.cancel(nil)
cmdFileName := s.commandFile.Name()
err := s.commandFile.Close()
if err != nil {
return fmt.Errorf("closing swiftDialog command file: %w", err)
}
err = os.Remove(cmdFileName)
if err != nil {
return fmt.Errorf("removing swiftDialog command file: %w", err)
}
return nil
}
func (s *SwiftDialog) Wait() (*SwiftDialogExit, error) {
s.finished()
parsed := map[string]any{}
if s.output.Len() != 0 {
if err := json.Unmarshal(s.output.Bytes(), &parsed); err != nil {
return nil, fmt.Errorf("parsing swiftDialog output: %w", err)
}
}
if err := s.cleanup(); err != nil {
return nil, fmt.Errorf("Wait cleaning up after swiftDialog: %w", err)
}
return &SwiftDialogExit{
ExitCode: s.exitCode,
Output: parsed,
}, s.exitErr
}
func (s *SwiftDialog) sendCommand(command, arg string) error {
if err := s.context.Err(); err != nil {
return fmt.Errorf("could not send command: %w", context.Cause(s.context))
}
// For some reason swiftDialog needs us to open and close the file
// to detect a new command, just writing to the file doesn't cause
// a change
commandFile, err := os.OpenFile(s.commandFile.Name(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, CommandFilePerms)
if err != nil {
return fmt.Errorf("opening command file for writing: %w", err)
}
fullCommand := fmt.Sprintf("%s: %s", command, arg)
_, err = fmt.Fprintf(commandFile, "%s\n", fullCommand)
if err != nil {
return fmt.Errorf("writing command to file: %w", err)
}
err = commandFile.Close()
if err != nil {
return fmt.Errorf("closing command file: %w", err)
}
return nil
}
///////////
// Title //
///////////
// Updates the dialog title
func (s *SwiftDialog) UpdateTitle(title string) error {
return s.sendCommand("title", title)
}
// Hides the title area
func (s *SwiftDialog) HideTitle() error {
return s.sendCommand("title", "none")
}
/////////////
// Message //
/////////////
// Set the dialog messsage
func (s *SwiftDialog) SetMessage(text string) error {
return s.sendCommand("message", sanitize(text))
}
// Append to the dialog message
func (s *SwiftDialog) AppendMessage(text string) error {
return s.sendCommand("message", fmt.Sprintf("+ %s", sanitize(text)))
}
///////////
// Image //
///////////
// Displays the selected image
func (s *SwiftDialog) Image(pathOrUrl string) error {
return s.sendCommand("image", pathOrUrl)
}
// Displays the specified text underneath any displayed image
func (s *SwiftDialog) SetImageCaption(caption string) error {
return s.sendCommand("imagecaption", caption)
}
//////////////
// Progress //
//////////////
// When Dialog is initiated with the Progress option, this will update the progress value
func (s *SwiftDialog) UpdateProgress(progress uint) error {
return s.sendCommand("progress", fmt.Sprintf("%d", progress))
}
// Increments the progress by one
func (s *SwiftDialog) IncrementProgress() error {
return s.sendCommand("progress", "increment")
}
// Resets the progress bar to 0
func (s *SwiftDialog) ResetProgress() error {
return s.sendCommand("progress", "reset")
}
// Maxes out the progress bar
func (s *SwiftDialog) CompleteProgress() error {
return s.sendCommand("progress", "complete")
}
// Hide the progress bar
func (s *SwiftDialog) HideProgress() error {
return s.sendCommand("progress", "hide")
}
// Show the progress bar
func (s *SwiftDialog) ShowProgress() error {
return s.sendCommand("progress", "show")
}
// Will update the label associated with the progress bar
func (s *SwiftDialog) UpdateProgressText(text string) error {
return s.sendCommand("progresstext", text)
}
///////////
// Lists //
///////////
// Create a list
func (s *SwiftDialog) SetList(items []string) error {
return s.sendCommand("list", strings.Join(items, ","))
}
// Clears the list and removes it from display
func (s *SwiftDialog) ClearList() error {
return s.sendCommand("list", "clear")
}
// Add a new item to the end of the current list
func (s *SwiftDialog) AddListItem(item ListItem) error {
arg := fmt.Sprintf("add, title: %s", item.Title)
if item.Status != "" {
arg = fmt.Sprintf("%s, status: %s", arg, item.Status)
}
if item.StatusText != "" {
arg = fmt.Sprintf("%s, statustext: %s", arg, item.StatusText)
}
return s.sendCommand("listitem", arg)
}
// Delete an item by name
func (s *SwiftDialog) DeleteListItemByTitle(title string) error {
return s.sendCommand("listitem", fmt.Sprintf("delete, title: %s", title))
}
// Delete an item by index number (starting at 0)
func (s *SwiftDialog) DeleteListItemByIndex(index uint) error {
return s.sendCommand("listitem", fmt.Sprintf("delete, index: %d", index))
}
// Update a list item by name
func (s *SwiftDialog) UpdateListItemByTitle(title, statusText string, status Status, progressPercent ...uint) error {
argStatus := string(status)
if len(progressPercent) == 1 && status == StatusProgress {
argStatus = fmt.Sprintf("progress, progress: %d", progressPercent[0])
}
arg := fmt.Sprintf("title: %s, status: %s, statustext: %s", title, argStatus, statusText)
return s.sendCommand("listitem", arg)
}
// Update a list item by index number (starting at 0)
func (s *SwiftDialog) UpdateListItemByIndex(index uint, statusText string, status Status, progressPercent ...uint) error {
argStatus := string(status)
if len(progressPercent) == 1 && status == StatusProgress {
argStatus = fmt.Sprintf("progress, progress: %d", progressPercent[0])
}
arg := fmt.Sprintf("index: %d, status: %s, statustext: %s", index, argStatus, statusText)
return s.sendCommand("listitem", arg)
}
/////////////
// Buttons //
/////////////
// Enable or disable button 1
func (s *SwiftDialog) EnableButton1(enable bool) error {
arg := "disable"
if enable {
arg = "enable"
}
return s.sendCommand("button1", arg)
}
// Enable or disable button 2
func (s *SwiftDialog) EnableButton2(enable bool) error {
arg := "disable"
if enable {
arg = "enable"
}
return s.sendCommand("button2", arg)
}
// Changes the button 1 label
func (s *SwiftDialog) SetButton1Text(text string) error {
return s.sendCommand("button1text", text)
}
// Changes the button 2 label
func (s *SwiftDialog) SetButton2Text(text string) error {
return s.sendCommand("button2text", text)
}
// Changes the info button label
func (s *SwiftDialog) SetInfoButtonText(text string) error {
return s.sendCommand("infobuttontext", text)
}
//////////////
// Info box //
//////////////
// Update the content in the info box
func (s *SwiftDialog) SetInfoBoxText(text string) error {
return s.sendCommand("infobox", sanitize(text))
}
// Append to the conteit in the info box
func (s *SwiftDialog) AppendInfoBoxText(text string) error {
return s.sendCommand("infobox", fmt.Sprintf("+ %s", sanitize(text)))
}
//////////
// Icon //
//////////
// Changes the displayed icon
// See https://github.com/swiftDialog/swiftDialog/wiki/Customising-the-Icon
func (s *SwiftDialog) SetIconLocation(location string) error {
return s.sendCommand("icon", location)
}
// Moves the icon being shown
func (s *SwiftDialog) SetIconAlignment(alignment Alignment) error {
return s.sendCommand("icon", string(alignment))
}
// Hide the icon
func (s *SwiftDialog) HideIcon() error {
return s.sendCommand("icon", "hide")
}
// Changes the size of the displayed icon
func (s *SwiftDialog) SetIconSize(size uint) error {
return s.sendCommand("icon", fmt.Sprintf("size: %d", size))
}
////////////
// Window //
////////////
// Changes the width of the window maintaining the current position
func (s *SwiftDialog) SetWindowWidth(width uint) error {
return s.sendCommand("width", fmt.Sprintf("%d", width))
}
// Changes the height of the window maintaining the current position
func (s *SwiftDialog) SetWindowHeight(width uint) error {
return s.sendCommand("height", fmt.Sprintf("%d", width))
}
// Changes the window position
func (s *SwiftDialog) SetWindowPosition(position FullPosition) error {
return s.sendCommand("position", string(position))
}
// Display content from the specified URL
func (s *SwiftDialog) SetWebContent(url string) error {
return s.sendCommand("webcontent", url)
}
// Hide web content
func (s *SwiftDialog) HideWebContent() error {
return s.sendCommand("webcontent", "none")
}
// Display a video from the specified path or URL
func (s *SwiftDialog) SetVideo(location string) error {
return s.sendCommand("video", location)
}
// Enables or disables the blur window layer
func (s *SwiftDialog) BlurScreen(enable bool) error {
blur := "disable"
if enable {
blur = "enable"
}
return s.sendCommand("blurscreen", blur)
}
// Activates the dialog window and brings it to the forground
func (s *SwiftDialog) Activate() error {
return s.sendCommand("activate", "")
}
// Quits dialog with exit code 5 (ExitQuitCommand)
func (s *SwiftDialog) Quit() error {
return s.sendCommand("quit", "")
}
func sanitize(text string) string {
return strings.ReplaceAll(text, "\n", "\\n")
}