November 7th: Exploring an 80% lower cost hosted Redis alternative - register

Building an Interactive Terminal Application with Go & Dragonfly

Learn to build an interactive terminal UI application using Go, BubbleTea, and Dragonfly. Check out our first guest author blog post!

Building an Interactive Terminal Application with Go & Dragonfly

Introduction

Hi everyone, my name is Annu Singh. I am a software engineer and developer advocate. I am excited to share my journey of building a terminal application with Go and Dragonfly. This post is featured as the first guest article for the Dragonfly Quill Program.

Terminal Application

In this article, I will discuss the details of the terminal application I created using Go and Dragonfly. The application offers the following features:

  • Renders an engaging user interface using the BubbleTea framework.
  • Allows adding, editing, and deleting tasks interactively from the terminal.
  • Updates task status with a single button press.
  • Persists data to Dragonfly.
  • Lists tasks according to their approaching deadlines, utilizing a sorted-set data structure.

How Does It Work?

The application features a terminal user interface (TUI) with a colorful Kanban board for personal task management, offering intuitive controls built using the BubbleTea framework. The BubbleTea framework creates event-driven TUI apps that can listen to user inputs and respond to them accordingly, making them interactive.

Models of the Terminal Application

BubbleTea enables this interactivity by storing the application's state in models (an interface type in Go) and updating it based on the user's commands via the model's Update method. After the state is updated, the application renders the UI to reflect the latest state via the model's View method, keeping the UI in sync with the state.

We can divide our application into different parts and store their states in corresponding models. Our application comprises three models:

  • Board: The top-level model of the application.
  • Column: The model that stores task lists for displaying them in the UI.
  • Form: The model that stores form data while adding or editing an individual task.
Models of the Terminal Application

BubbleTea Code Flow

Once we have defined our models, let's understand the code flow of the BubbleTea framework.

BubbleTea Code Flow

As shown in the diagram above:

  1. A new BubbleTea program p is initialized with a fresh Board model.
  2. p.Run() starts the application.
  3. BubbleTea starts an event loop in a separate Go routine to continuously listen to user interactions (such as key presses) and other events.
  4. Inside this loop, BubbleTea calls the Update method of the model the user interacted with, passing the user interaction (Msg) as an argument. The Msg can be of any type and is a result of the I/O that took place. The Update method processes the event, updating the model state or triggering further updates.
  5. The View method is called to render the updated state of the model to the terminal. For example, when the user presses n on a Column model to add a new task, the Column's Update method creates a new form and calls the Form's Update method, which in turn calls the Form's View method, rendering an empty new form on the terminal.
  6. When the BubbleTea program receives a signal to quit (q or Ctrl+C), p.Run() returns.
  7. When p.Run() returns, the main function executes the remaining code, and the program eventually exits.

Integration with Dragonfly

To render tasks on a priority basis according to their deadlines, I used Dragonfly's sorted-set to store and retrieve tasks. Each task is a member of the sorted-set. As the name suggests, a sorted-set is a collection with unique members in order. Each member of the sorted-set is a unique string with an associated score. New members settle in their sorted places as they arrive based on their scores. In Dragonfly, a sorted-set uses B+ tree as the underlying data structure.

💡 Read this post for more details on how Dragonfly implemented a sorted-set using B+ tree and the resulting benefits.

Also, according to this comprehensive article by ScyllaDB, B-tree-based data structures can potentially reduce CPU cache misses when searching for a key. The search occurs in two steps: intra-node search and tree descent. During the search, the program accesses data and loads the entire cache line containing it into the CPU cache. It means all keys stored contiguously in a node are loaded into the cache together. The CPU first checks for the target key in this cache, reducing the need to load multiple cache lines.

B+ tree, as an important variation of B-tree, has some additional advantages. It has all the data in the leaf nodes organized in a linked list. This design allows fast sequential access without changing levels (less I/O calls) in the in-order tree traversal, such as in range queries where it's required to output all data in one fast scan.

Below are some code details to integrate Dragonfly with the application:

  • We store each task as a value linked to a key (taskID) in a hash data type.
  • To organize tasks by their deadlines in the terminal, we store each deadline as a score in a sorted-set data type.
  • We are tracking the task ID in a separate incremental key (taskIDCounter) to get the ID of the last added task. This counter calculates the new task's ID whenever the user adds a task.
  • Parse the deadline of a task from the submitted Form:
dl, err := time.Parse("2006-01-02", f.deadline.Value())
if err != nil {
  fmt.Println("Error parsing deadline:", err)
}
  • The AddTask and RemoveTask functions defined in the column.go file are for adding tasks to and removing tasks from Dragonfly.
  • The if f.index != APPEND {...} check in the form.go file is to know if the user has pressed the e key or the n key. The if editedTask != f.originalTask {...} checks if the user has updated the details of an existing task after pressing the e key. Another possibility is the user changing their mind and not editing a task after pressing the e key. Based on this confirmation, the application replaces the deprecated task with the updated task in Dragonfly or does nothing if the user exits the edit screen without editing the task.
  • To keep the data store state in sync with the application state, it is required to place the code for Dragonfly operations in the Update method of the models to be able to respond to the user's commands.

Conclusion

Building a terminal application with Go and Dragonfly was an exciting and rewarding journey. Leveraging the BubbleTea framework allowed for creating an interactive and visually appealing user interface, while Dragonfly's powerful sorted-set and hash data types ensured flexible and efficient data storage.

I hope this guide inspires you to explore BubbleTea and Dragonfly for your own projects. Feel free to check out the complete source code on GitHub of this application, and don't hesitate to join the Dragonfly community for more exclusive content, events, and support. Happy coding!

Stay up to date on all things Dragonfly

Join our community for unparalleled support and insights

Join

Switch & save up to 80% 

Dragonfly is fully compatible with the Redis ecosystem and requires no code changes to implement. Instantly experience up to a 25X boost in performance and 80% reduction in cost