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](/assets/blog/building-an-interactive-terminal-app-with-go-and-dragonfly/01-terminal-application.png)
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](/assets/blog/building-an-interactive-terminal-app-with-go-and-dragonfly/02-terminal-application-models.png)
BubbleTea Code Flow
Once we have defined our models, let's understand the code flow of the BubbleTea framework.
![BubbleTea Code Flow](/assets/blog/building-an-interactive-terminal-app-with-go-and-dragonfly/03-bubble-tea-framework-code-flow.png)
As shown in the diagram above:
- A new BubbleTea program
p
is initialized with a freshBoard
model. p.Run()
starts the application.- BubbleTea starts an event loop in a separate Go routine to continuously listen to user interactions (such as key presses) and other events.
- Inside this loop, BubbleTea calls the
Update
method of the model the user interacted with, passing the user interaction (Msg
) as an argument. TheMsg
can be of any type and is a result of the I/O that took place. TheUpdate
method processes the event, updating the model state or triggering further updates. - The
View
method is called to render the updated state of the model to the terminal. For example, when the user pressesn
on aColumn
model to add a new task, theColumn
'sUpdate
method creates a new form and calls theForm
'sUpdate
method, which in turn calls theForm
'sView
method, rendering an empty new form on the terminal. - When the BubbleTea program receives a signal to quit (
q
orCtrl+C
),p.Run()
returns. - When
p.Run()
returns, themain
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 ahash
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
andRemoveTask
functions defined in thecolumn.go
file are for adding tasks to and removing tasks from Dragonfly. - The
if f.index != APPEND {...}
check in theform.go
file is to know if the user has pressed thee
key or then
key. Theif editedTask != f.originalTask {...}
checks if the user has updated the details of an existing task after pressing thee
key. Another possibility is the user changing their mind and not editing a task after pressing thee
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!