Book Image

Hands-On GUI Application Development in Go

By : Andrew Williams
Book Image

Hands-On GUI Application Development in Go

By: Andrew Williams

Overview of this book

Go is often compared to C++ when it comes to low-level programming and implementations that require faster processing, such as Graphical User Interfaces (GUIs). In fact, many claim that Go is superior to C++ in terms of its concurrency and ease of use. Most graphical application toolkits, though, are still written using C or C++, and so they don't enjoy the benefits of using a modern programming language such as Go. This guide to programming GUIs with Go 1.11 explores the various toolkits available, including UI, Walk, Shiny, and Fyne. The book compares the vision behind each project to help you pick the right approach for your project. Each framework is described in detail, outlining how you can build performant applications that users will love. To aid you further in creating applications using these emerging technologies, you'll be able to easily refer to code samples and screenshots featured in the book. In addition to toolkit-specific discussions, you'll cover more complex topics, such as how to structure growing graphical applications, and how cross-platform applications can integrate with each desktop operating system to create a seamless user experience. By delving into techniques and best practices for organizing and scaling Go-based graphical applications, you'll also glimpse Go's impressive concurrency system. In the concluding chapters, you'll discover how to distribute to the main desktop marketplaces and distribution channels. By the end of this book, you'll be a confident GUI developer who can use the Go language to boost the performance of your applications.
Table of Contents (25 chapters)
Title Page
Copyright and Credits
About Packt
Contributors
Preface
Comparison of GUI Toolkits
Index

Creating a server provider


The following code outlines the contents of the gmail.gofile that's available in the client package of the repository for this book. If you want to jump straight to trying out this functionality, then simply copy your credentials.json file to the current directory and skip to the next section, Update an example to use Gmail.

We start by adding the necessary OAuth2 setup and token storage by copying the getClient(), getTokenFromWeb(), tokenFromFile(), and saveToken() functions from Google's Gmail quickstart Go file at github.com/gsuitedevs/go-samples/blob/master/gmail/quickstart/quickstart.go. These are very similar to the OAuth2 code that was created before but works better with the Google libraries.

Downloading inbox messages

Next, we need to set up the client from the credentials file that has been saved (in the current directory). We add a new function to parse the data, set up the authentication, and configure *gmail.Service using the following code:

func setupService() *gmail.Service {
   b, err := ioutil.ReadFile("credentials.json")
if err != nil {
      log.Fatalf("Unable to read client secret file: %v", err)
   }

   config, err := google.ConfigFromJSON(b, gmail.GmailReadonlyScope,
      gmail.GmailComposeScope)
if err != nil {
      log.Fatalf("Unable to parse client secret file to config: %v", err)
   }
   client := getClient(config)

   srv, err := gmail.New(client)
if err != nil {
      log.Fatalf("Unable to retrieve Gmail client: %v", err)
   }

return srv
}

 

 

 

 

The service returned from this function will be used for each subsequent call to the Gmail API as it contains the authentication configuration and credentials. Next, we need to prepare the email list by downloading all of the messages in the user's inbox. The INBOX LabelID is used to filter messages that haven't been archived. This function requests the message list and iterates through the metadata to initiate the full download of each message. For a full implementation, we would need to add paging support (the response contains nextPageToken, which indicates when more data is available), but this example will handle up to 100 messages:

func downloadMessages(srv *gmail.Service) {
   req := srv.Users.Messages.List(user)
   req.LabelIds("INBOX")
   resp, err := req.Do()
if err != nil {
      log.Fatalf("Unable to retrieve Inbox items: %v", err)
   }

var emails []*EmailMessage
for _, message := range resp.Messages {
      email := downloadMessage(srv, message)
      emails = append(emails, email)
   }
}

To download each individual message, we need to implement the downloadMessage() function referenced previously. For the specified message, we download the full content using the Gmail Go API. From the resulting data, we extract the information we need from the message headers. As well as parsing the Date header, we need to decode the message body, which is in a serialized, Base64 encoded format:

func downloadMessage(srv *gmail.Service, message *gmail.Message) *EmailMessage {
   mail, err := srv.Users.Messages.Get(user, message.Id).Do()
if err != nil {
      log.Fatalf("Unable to retrieve message payload: %v", err)
   }

var subject string
var to, from Email
var date time.Time

   content := decodeBody(mail.Payload)
for _, header := range mail.Payload.Headers {
switch header.Name {
case "Subject":
         subject = header.Value
case "To":
         to = Email(header.Value)
case "From":
         from = Email(header.Value)
case "Date":
         value := strings.Replace(header.Value, "(UTC)", "", -1)
         date, err = time.Parse("Mon, _2 Jan 2006 15:04:05 -0700",
            strings.TrimSpace(value))
if err != nil {
            log.Println("Error: Could not parse date", value)
            date = time.Now()
         } else {
            log.Println("date", header.Value)
         }
      }
   }

return NewMessage(subject, content, to, from, date)
}

The decodeBody() function is as shown in the following. For plain text emails, the content is in the Body.Data field. For multi-part messages (where the body is empty), we access the first of the multiple parts and decode that instead. Decoding the Base64 content is handled by the standard library decoder:

func decodeBody(payload *gmail.MessagePart) string {
   data := payload.Body.Data
if data == "" {
      data = payload.Parts[0].Body.Data
   }
   content, err := base64.StdEncoding.DecodeString(data)
if err != nil {
      fmt.Println("Failed to decode body", err)
   }

return string(content)
}

The final step in preparing this code is to complete the EmailServer interface methods. The ListMessages() function will return the result of downloadMessages(), and we can set up CurrentMessage() to return the email at the top of the list. Full implementation is in this book's code repository.

 

 

Sending messages

To send a message, we have to package up the data in a raw format to send through the API. We'll re-use the ToGMailEncoding() function from the Post example in Chapter 12, Concurrency, Networking, and Cloud Services. Before encoding the email, we set an appropriate "From" email address (be sure to use the email address of the account you are signed in with or a registered alias) and the current date for the time of sending. After encoding, we set the data to the Raw field of a gmail.Message type and pass it to the Gmail Send() function:

func (g *gMailServer) Send(email *EmailMessage) {
   email.From = "YOUR EMAIL ADDRESS"
email.Date = time.Now()

   data := email.ToGMailEncoding()
   msg := &gmail.Message{Raw:data}

   srv.Users.Messages.Send(user, msg).Do()
}

This minimal code will be enough to implement sending a message. All of the hard work has been done by the earlier setup code—which provided the srv object.

Listening for new messages

Although Google provides the ability to use push messaging, the setup is very complicated—so instead, we'll poll for new messages. Every 10 seconds, we should download any new messages that have arrived. To do this, we can use the history API, which returns any messages that appeared after a specific point in history (set using StartHistoryId()). HistoryId is a chronological number that marks the order that messages arrived in. Before we can use the history API, we need to have a valid HistoryId—we can do this by adding the following line to the downloadMessage() function:

g.history = uint64(math.Max(float64(g.history), float64(mail.HistoryId)))

Once we have a point in history to query from, we need a new function that can download any messages since this point in time. The following code is similar to downloadMessages() in the preceding code but will only download new messages:

func (g *gMailServer) downloadNewMessages(srv *gmail.Service) []*EmailMessage{
   req := srv.Users.History.List(g.user)
   req.StartHistoryId(g.history)
   req.LabelId("INBOX")
   resp, err := req.Do()
if err != nil {
      log.Fatalf("Unable to retrieve Inbox items: %v", err)
   }

var emails []*EmailMessage
for _, history := range resp.History {
for _, message := range history.Messages {
         email := downloadMessage(srv, message)
         emails = append(emails, email)
      }
   }

return emails
}

To complete the functionality, we update our Incoming() method so that it sets up the channel and starts a thread to poll for new messages. Every 10 seconds, we'll download any new messages that have appeared and pass each to the in channel that was created:

func (g *gMailServer) Incoming() chan *EmailMessage {
   in := make(chan *EmailMessage)

go func() {
for {
         time.Sleep(10 * time.Second)

for _, email := range downloadNewMessages(srv) {
            g.emails = append([]*EmailMessage{email}, g.emails...)
            in <- email
         }
      }
   }()

return in
}

The complete code can be found in the client package of this book's code repository. Let's look at how to use this new email server in our previous examples.