Hello Everyone! It's been a while since I last wrote a blog post, but I'm back with something fresh(at least for me) post. In this post, we'll be building a command-line TODO app using Goravel console.
But before we dive in, let me introduce you to a framework called Goravel.
What is Goravel?
You may have used Laravel (personally, I’ve used it a lot!). I love its developer-friendly experience and straightforward architecture. However, for the Go community, something similar was missing. Enter Goravel—a framework that makes life easier for PHP developers like me who want to work with Go.
Goravel borrows best practices and features from Laravel, bringing them into the Go ecosystem.
That’s enough introductions. Let’s dive into what we’re building today!
Prerequisites
Before we dive in, make sure you have:
- Go 1.21 or higher installed
- Basic understanding of Go syntax
- Familiarity with command-line interfaces
- SQLite (we'll use it for simplicity, but you can swap it for any database)
Don't worry if you're new to Goravel—I'll explain everything as we go!
What are we building?
This article focuses on learning Goravel's console, so we will implement only the following two features in about command line todo application:
- Add a new task.
- View all tasks
You can checkout source code of the todo application at kkumar-gcc/todo.
Project Structure
Project will be structured in following way where each folders servers their respective purpose as name sounds:
constants/ # Store constants or config for the application
└── version.go
console/
├── commands/ # Implement commands
│ └── add_task.go
└── kernel.go # Register all commands
models/
└── task.go # Model definition for a task
services/ # Handle business logic
└── task_service.go # Service for CRUD operations on tasks
repositories/ # Layer between the database and business logic
└── task_repository.go
database/
└── database.go # Logic for interacting with and resolving the database
main.go # Application entry point
Project Initialization
In the last section, we have outlined the project structure. Now, let's create these folders (files) and initialize a Go project named github.com/kkumar-gcc/todo
.
mkdir todo
cd todo
go mod init github.com/kkumar-gcc/todo
Install the Goravel framework:
go get github.com/goravel/framework
Let's create, the entry point of the application main.go
where all magical thing will happen.
package main
import (
"os"
goravelconsole "github.com/goravel/framework/console"
"github.com/goravel/framework/support/color"
"github.com/kkumar-gcc/todo/constants"
)
func main() {
name := "todo"
usage := "TODO"
usageText := "todo [global options] command [command options] [arguments...]"
// The last argument is false, which indicates that it is not an artisan command.
cli := goravelconsole.NewApplication(name, usage, usageText, constants.Version, false)
// run the any command called in os.Args
if err := cli.Run(os.Args, false); err != nil {
color.Red().Println(err)
}
}
Here, we’re creating and running a Goravel console instance with custom metadata for the TODO application. Running go run .
will show the default help message.
Create your first Goravel command.
Now that the initial setup is complete, let's create our first Goravel command by adding a file named add_task_command.go
inside the console/commands
directory. Every Goravel command must implement the github.com/goravel/framework/contracts/console/command.Command
interface.
Here's how you can define the AddTaskCommand
:
package commands
import (
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
)
type AddTaskCommand struct {}
// Signature The name and signature of the console command.
func (r *AddTaskCommand) Signature() string {
return "task:add"
}
// Description The console command description.
func (r *AddTaskCommand) Description() string {
return "Create a new task"
}
// Extend The console command extend.
func (r *AddTaskCommand) Extend() command.Extend {
return command.Extend{}
}
// Handle Execute the console command.
func (r *AddTaskCommand) Handle(ctx console.Context) (err error) {
ctx.Success("Hello world!")
return nil
}
Let's understand the command structure:
-
Signature
: Serves as a identifier for the command (must be unique) -
Description
: Displays the description when the help flag is used. -
Extend
: Defines theCategory
andFlags
for the command. -
Handle
: The entry point for the command. It accepts an instance ofgithub.com/goravel/framework/console.Context
, which offers many convenient methods for command I/O (Read More).
Register the command
To register a command, open main.go
and use the Register
method provided by the Goravel console:
// main.go
package main
import (
"os"
goravelconsole "github.com/goravel/framework/console"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/support/color"
"github.com/kkumar-gcc/todo/console/commands"
"github.com/kkumar-gcc/todo/constants"
)
func main() {
...
cli := goravelconsole.NewApplication(name, usage, usageText, constants.Version, false)
cli.Register([]console.Command{
&commands.AddTaskCommand{},
})
...
}
We have successfully registered the command. Let’s test it out by running the following in the terminal:
go run . task:add
Output:
Hurray!! We've created our first command successfully. However, as your application grows, you will likely have many commands - or commands that accepts dependencies. To keep things clean and maintainable, it's a good idea to centralize command registration in a dedicated console/kernel.go
file and then use the kernel in main.go
.
Define the Kernel
Create the console/kernel.go
file and register commands there:
package console
import (
"github.com/goravel/framework/contracts/console"
"github.com/kkumar-gcc/todo/console/commands"
)
type Kernel struct {
}
func (kernel *Kernel) Commands() []console.Command {
return []console.Command{
&commands.AddTaskCommand{},
}
}
Now let's update main.go
to use the kernel:
package main
...
func main() {
...
kernel := &console.Kernel{}
cli.Register(kernel.Commands())
...
}
Now that we have setup everything, we are ready to implement the actual logic of the task:add
command. But before we do that, let's define the Task
model, which will represent and store task data in the database.
package models
import "time"
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Status int `json:"status"` // Use constants: constants.StatusPending(1), constants.StatusInProgress(2), constants.StatusCompleted(3)
CreatedAt time.Time `json:"created_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
Priority int `json:"priority"` // Use constants: constants.PriorityLow(1), constants.PriorityMedium(2), constants.PriorityHigh(3)
Tags string `json:"tags"` // Tags for categorization
}
Handling User Input
Goravel console offers two ways to take named input from the terminal: the first is by defining Flags
(Options and the second is by prompting For Input in terminal. We will use both - flags as the primary input method and interactive prompts as a fallback.
We require the user to provide title
, status
, priority
and tags
(Optional) to create a task. Let's define flags for each input inside the Extend
method of AddTaskCommand
.
// add_task_command.go
package commands
...
func (r *AddTaskCommand) Extend() command.Extend {
return command.Extend{
Category: "tasks",
Flags: []command.Flag{
&command.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "The title of the task",
},
&command.StringFlag{
Name: "priority",
Aliases: []string{"p"},
Usage: "The priority of the task (low, medium, high)",
},
&command.StringFlag{
Name: "status",
Aliases: []string{"s"},
Usage: "The status of the task (pending, in-progress, completed)",
},
&command.StringFlag{
Name: "tags",
Aliases: []string{"g"},
Usage: "Tags for the task, separated by commas",
},
},
}
}
...
Accessing Flag Values
You can pass these flags using (--{name} {value}
or -{alias} {value}
) when executing the command . To access them, use ctx.Option("{name}")
in Handle
method.
package commands
...
func (r *AddTaskCommand) Handle(ctx console.Context) (err error) {
title := ctx.Option("title")
priority := ctx.Option("priority")
status := ctx.Option("status")
tags := ctx.Option("tags")
println(title, priority, status, tags)
return nil
}
Try running:
go run . task:add --title "write article" -p 1 -s 2
If you want the flag value in a specific type, Goravel provides methods like OptionInt
, OptionSlice
etc.
Interactive Prompts for Missing Input
We have handled input via flags, but what if some required fields are missing? Let's use interactive prompts to ensures title
, priority
and status
are always provided.
Goravel console provides various convenient methods to prompt the user.
package commands
...
func (r *AddTaskCommand) Handle(ctx console.Context) (err error) {
title := ctx.Option("title")
priority := ctx.Option("priority")
status := ctx.Option("status")
tags := ctx.Option("tags")
if title == "" {
title, err = ctx.Ask("What is the title of the task?", console.AskOption{
Placeholder: "E.g., Write article",
Prompt: "> ",
Validate: func(value string) error {
if strings.TrimSpace(value) == "" {
return errors.New("the task title is required")
}
return nil
},
})
if err != nil {
ctx.Error(err.Error())
return nil
}
}
if priority == "" {
choices := []console.Choice{
{Key: constants.PriorityColors[constants.PriorityLow], Value: strconv.Itoa(constants.PriorityLow)},
{Key: constants.PriorityColors[constants.PriorityMedium], Value: strconv.Itoa(constants.PriorityMedium)},
{Key: constants.PriorityColors[constants.PriorityHigh], Value: strconv.Itoa(constants.PriorityHigh)},
}
priority, err = ctx.Choice("Select the priority of the task:", choices, console.ChoiceOption{
Default: strconv.Itoa(constants.PriorityLow),
Description: "Choose a priority for the task",
})
if err != nil {
ctx.Error(err.Error())
return nil
}
}
if status == "" {
choices := []console.Choice{
{Key: constants.StatusColors[constants.StatusPending], Value: strconv.Itoa(constants.StatusPending)},
{Key: constants.StatusColors[constants.StatusInProgress], Value: strconv.Itoa(constants.StatusInProgress)},
{Key: constants.StatusColors[constants.StatusCompleted], Value: strconv.Itoa(constants.StatusCompleted)},
}
status, err = ctx.Choice("Select the status of the task:", choices, console.ChoiceOption{
Default: strconv.Itoa(constants.StatusPending),
Description: "Choose a priority for the task",
})
if err != nil {
ctx.Error(err.Error())
return nil
}
}
if tags == "" {
tags, err = ctx.Ask("Enter tags for the task (comma-separated):", console.AskOption{
Placeholder: "E.g., work,urgent",
Prompt: "> ",
})
if err != nil {
ctx.Error(err.Error())
return nil
}
}
priorityInt, err := strconv.Atoi(priority)
if err != nil {
ctx.Error(err.Error())
return nil
}
statusInt, err := strconv.Atoi(status)
if err != nil {
ctx.Error(err.Error())
return nil
}
println(priorityInt, statusInt)
return nil
}
You can refer to the docs for each method. For example, Ask
accepts a question and an optional second argument console.AskOption
struct. In our case, we added a Validate
function that runs whenever the user presses Enter
key. If the input is invalid, it will shows an error and waits for valid input.
Integrating the Service Layer
Now that we have collected all required fields, let's define a services.TaskService
interface to handle task creation and listing. Let's accept TaskService
as a struct level argument in AddTaskCommand
. Implementation of TaskService
is not part of this article if you want to check you can read source code.
// task_service.go
package services
type TaskService interface {
CreateTask(ctx context.Context, title string, status, priority int, tags string) error
GetAllTasks(ctx context.Context, status, priority int, sort string) ([]models.Task, error)
}
// update add_task_command.go
...
type AddTaskCommand struct {
TaskService services.TaskService
}
// update kernel.go
...
func (kernel *Kernel) Commands() []console.Command {
// init the service
taskService := services.NewTaskService()
return []console.Command{
&commands.AddTaskCommand{
TaskService: taskService,
},
}
}
Completing the Add Task Command
Now we can call TaskService.CreateTask()
to create the task using the collected input
package commands
...
func (r *AddTaskCommand) Handle(ctx console.Context) (err error) {
...
if err := r.TaskService.CreateTask(context.Background(), title, statusInt, priorityInt, tags); err != nil {
ctx.Error(err.Error())
return nil
}
ctx.Success("Task created successfully!")
return nil
}
Creating the List Tasks Command
Hurray!! we have completed our first full implementation of command. Now let's create another command, task:list
, to view tasks. We'll explore how to beautifully display output using Goravel's console methods.
// list_tasks_command.go
package commands
import (
"context"
"time"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/support/color"
"github.com/kkumar-gcc/todo/constants"
"github.com/kkumar-gcc/todo/models"
"github.com/kkumar-gcc/todo/services"
)
type ListTasksCommand struct {
TaskService services.TaskService
}
// Signature The name and signature of the console command.
func (r *ListTasksCommand) Signature() string {
return "task:list"
}
// Description The console command description.
func (r *ListTasksCommand) Description() string {
return "List all tasks"
}
// Extend The console command extend.
func (r *ListTasksCommand) Extend() command.Extend {
return command.Extend{
Category: "tasks",
Flags: []command.Flag{
&command.StringFlag{
Name: "sort",
Aliases: []string{"s"},
Usage: "Sort tasks by field (status or priority)",
},
&command.StringFlag{
Name: "status",
Aliases: []string{"st"},
Usage: "Filter tasks by status (pending, in_progress, completed)",
},
&command.StringFlag{
Name: "priority",
Aliases: []string{"p"},
Usage: "Filter tasks by priority (low, medium, high)",
},
},
}
}
// Handle Execute the console command.
func (r *ListTasksCommand) Handle(ctx console.Context) (err error) {
sort, status, priority := ctx.Option("sort"), ctx.Option("status"), ctx.Option("priority")
tasks, err := r.TaskService.GetAllTasks(context.Background(), constants.StatusMap[status], constants.PriorityMap[priority], sort)
if err != nil {
ctx.Error(err.Error())
return nil
}
if len(tasks) == 0 {
ctx.Info("No tasks found matching the given criteria.")
return nil
}
ctx.NewLine()
color.Println("<fg=blue;op=bold>Task List:</>")
ctx.NewLine()
ctx.TwoColumnDetail(color.Sprintf("<fg=cyan;op=bold>%s</>", "tasks"), "Details")
for _, task := range tasks {
idLabel := color.Sprintf("<fg=white;op=bold>%d</>", task.ID)
statusLabel := constants.StatusColors[task.Status]
priorityLabel := constants.PriorityColors[task.Priority]
tagsAndCreatedAt := color.Sprintf("<fg=gray>Tags: %s, Created At: %s</>", task.Tags, task.CreatedAt.Format(time.RFC822))
ctx.TwoColumnDetail(task.Title+" ("+idLabel+") "+tagsAndCreatedAt, statusLabel+" | "+priorityLabel)
}
ctx.NewLine()
return nil
}
console.Context
provides convenient methods like TwoColumnDetail
and Info
, Spinner
, WithProgressBar
etc. to display rich output in the terminal.
That's a wrap!
We've built a working TODO CLI app with Goravel Console. Pretty neat how easy it was, right? From commands to interactive prompts to colorful output - Goravel handled it all beautifully.
Want to take it further? Try adding update/delete commands or maybe due dates.
Check out the complete code at kkumar-gcc/todo.
Top comments (0)