How to manage your time when freelancing

2025-02-24
Updated: 2025-02-28

Task 1

  • Make a task manager to manage you daily tasks while learning Golang.

Lot's of "free" time

If you don't have a fixed schedule, time slips away unnoticed, like socks in a dryer1. The absence of structure often leads to distractions and procrastination, making it easy to lose track of your priorities and deadlines. So, tasks that could be completed in minutes may take hours (of course always taking into consideration the planning fallacy bias), as the day progresses without a clear plan2. This lack of organization not only diminishes productivity but can also create a sense of overwhelm, as the weight of unfinished tasks accumulates. Ultimately, without a schedule, the precious hours of the day vanish, leaving you with a lingering sense of unfulfillment and regret.

When you start freelancing you have to manage your time in a way that it is productive and easy to organize.

There are many cli applications that can be used to manage my tasks. just to name a few:

However, I though it would be a good learning opportunity to make a small app in Golang, as it would help me to understand the language and prepare me for future projects, because if I can’t manage my tasks, at least I can manage to make a cool app about it! And the -real- project to add extra functionality is sxctl cli app of Stagex

Learning go? - Part 1

setting it up

So first thing I did was to check the main documentation, the guide about building-an-awesome-cli-app-in-go-oscon, and the cobra-cli.

Of course I had installed GO (on Debian) and populated my .bashrc with:

#.bashrc 
# Set up go 
if [ -d /usr/local/go/bin ] ; then 
export PATH="$PATH:/usr/local/go/bin"
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
fi

then making the directory for the new cli app

mkdir todoer && cd todoer
go install github.com/spf13/cobra-cli@latest
git init && go mod init codeberg.org/conyel/todoer
cobra-cli init . -a ConYel --viper
go run main go

so this is the directory

 tree
.
├── cmd
   └── root.go
├── go.mod
├── go.sum
├── LICENSE
├── main.go
└── todoer
time to code

First let's add the add command following the tutorial

cobra add add 

and then in cmd/add.go we change a bit the command to print the specific tasks

/*
Copyright © 2025 itsyou 
*/
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

// addCmd represents the add command
var addCmd = &cobra.Command{
	Use:   "add",
	Short: "Add a new task",
	Long:  `Add a new task to the list of your tasks.`,
	Run: func(cmd *cobra.Command, args []string) { // <- from here
		for _, x := range args {
			fmt.Println(x)
		}                                      // <- to here
	},
}

Then we move on to create the first struct that will include the task. After some thinking I chose the Tom's Obvious, Minimal Language (and friends). I searched for any go lib that parses TOML and found two that are still maintained, so I just picked go-toml. Why go-toml instead of the BurntSushi/toml?? Eh well, randomly for now.
Let's see if I'll have to change it in the future if it's not simple enough (or many bugs).

Anyway, let's make a dir for the models, install it and make our struct.

> mkdir todot 
> touch cmds/todot.go 
> go get github.com/pelletier/go-toml

and then in cmds/todot.go

package todot //ToDoT Tasks fail name for a struct?

import "time"

type Task struct {
	//Version int
	Name    string
	Created time.Time
	DueBy   time.Time
}

Ok so now we have our first structure which will be the task added/modified/removed. Let's add a print in add.go to see how it works.

var addCmd = &cobra.Command{
	Use:   "add",
	Short: "Add a new task",
	Long:  `Add a new task to the list of your tasks.`,
	Run: func(cmd *cobra.Command, args []string) {
		tasks := []todot.Task{}
		for _, x := range args {
			tasks = append(tasks,
				todot.Task{Name: x, Created: time.Now()})
		}
		fmt.Println(tasks)
	},
}

and build again (from now on I will show only output )

go build 
> ./todoer add "one two" three
[{0 one two 0001-01-01 00:00:00 +0000 UTC} {0 three 0001-01-01 00:00:00 +0000 UTC}] 

hmm I don't like it.
Let's change the default print of it and add a Due To so we have a deadline3. In add.go add

	Run: func(cmd *cobra.Command, args []string) {
		var tasks = []todot.Task{}
		for _, x := range args {
			tasks = append(tasks,
				todot.Task{
					Name:    x,
					Created: time.Now(),
					DueBy:   time.Now().Local().Add(time.Minute * time.Duration(100))})
		}
		todot.SaveTasks("x", tasks)
		todot.ToString(tasks[0])

and in todot.go

import "fmt"
import "github.com/pelletier/go-toml/v2"

func SaveTasks(filename string, tasks []Task) error {
	b, err := toml.Marshal(tasks)
	if err != nil {
		return err
	}
	fmt.Println(string(b))
	return nil
}

func ToString(t Task) {
	fmt.Printf("Name: %s\nCreated: %s\nDue to: %s",
		t.Name,
		t.Created.Format("2006/01 02 Mon 15:00"),
		t.DueBy.Format("2006/01 02 Mon 15:00"))
}

In that way we make a function on how to print (and later put on TOML file) that will show a useful info on each task. (In a bit we will see how wrong is what I made :P)

Let's run it and see what we get!

> ./todoer add "one two" three
[[]]
Name = 'one two'
Created = '2025/02 22 Fri 14:11'
DueBy = '2025/02 22 Fri 14:16'

[[]]
Name = 'three'
Created = '2025/02 22 Fri 14:11'
DueBy = '2025/02 22 Fri 14:16'

Name: one two
Created: 2025/02 22 Fri 14:11
Due to: 2025/02 22 Fri 14:16

ok it seems fine, no? Time to save to a file! changing todot.go

func SaveTasks(filename string, tasks []Task) error {
	b, err := toml.Marshal(tasks)
	err = os.WriteFile(filename, b, 0644)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
	return nil
}

and then adding in add.go:

		err := todot.SaveTasks("./x.toml", tasks)
		if err != nil {
			panic(err)
		}

and running it, we get a file x.toml And here is the time to read a bit more about how to import a TOML file and see why it doesn't work importing that file.

  1. The Toml file has bad format.
  2. The struct seem to be badly made.
  3. The format to print in the file is incorrect

Ok, fix the struct first. on todot.go

// import "time" // comment time as we will change it to string (for now at least :P )
type Task struct {
	//Version int
	Name    string `toml:"Name"`
	Created string `toml:"Created"`
	DueBy   string `toml:"DueBy"`
}

type Config struct {
	Tasks []Task `toml:"task"` // add a parent struct to keep all tasks (maybe later we will change it if there is a better way) 
}

// extend time  // this to add more ways to print time as string
const (
	// YYYY-MM-DD: 2022-03-23
	YYYYMMDD = "2006-01-02"
	// 24h hh:mm:ss: 14:23:20
	HHMMSS24h = "15:04:05"
	// 12h hh:mm:ss: 2:23:20 PM
	HHMMSS12h = "3:04:05 PM"
	// text date: March 23, 2022
	TextDate = "January 2, 2006"
	// text date with weekday: Wednesday, March 23, 2022
	TextDateWithWeekday = "Monday, January 2, 2006"
	// abbreviated text date: Mar 23 Wed
	AbbrTextDate = "Jan 2 Mon"
	//microwave date: Mar 23 Wed
	Microwave = "2006/01 2 Mon 15:04"
)

change the other two functions and add one that will read ReadTasks from the file

func SaveTasks(filename string, tasks *Config) error { // !!!here is a pointer, read more about them.
	b, err := toml.Marshal(tasks)
	err = os.WriteFile(filename, b, 0644)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
	return nil
}

func ReadTasks(filename string) (*Config, error) { // !!!here is a pointer, read more about them.

	var config Config
	// var tasks []Task
	data, err := os.ReadFile(filename)
	if err != nil {
		fmt.Println("Error in reading")
		return nil, err
	}
	if err := toml.Unmarshal(data, &config); err != nil { // !!!here is a pointer, read more about them.

		fmt.Println("Error in unmarshal")
		return nil, err
	}

	fmt.Println("the config is:", config.Tasks)
	return &config, nil
}

func (t Task) ToString() string {
	return fmt.Sprintf("Name: %s\nCreated: %s\nDue to: %s",
		t.Name,
		t.Created,
		t.DueBy)
}

and add command becomes like this:

	Run: func(cmd *cobra.Command, args []string) {
		newtasks := todot.Config{  //
			Tasks: []todot.Task{}, // Initialize Tasks as an empty slice
		}
		timenow := time.Now().Local()
		for _, task := range args {
			newtasks.Tasks = append(newtasks.Tasks, // append to it
				todot.Task{
					Name:    task,
					Created: timenow.Format(todot.Microwave),
					DueBy:   timenow.Add(time.Minute * time.Duration(5)).Format(todot.Microwave),
				})
		}
		err := todot.SaveTasks("./x.toml", &newtasks)
		if err != nil {
			panic(err)
		}

		fmt.Println(newtasks.Tasks[0].ToString())
		// fmt.Printf("%#v\n", tasks)
	},
}

and then run them to see if it actually make a proper TOML file:

> ./todoer add "one two" three
[[task]]
Name = 'one two'
Created = '2025/02 22 Fri 16:01'
DueBy = '2025/02 22 Fri 16:06'

[[task]]
Name = 'three'
Created = '2025/02 22 Fri 16:01'
DueBy = '2025/02 22 Fri 16:06'

Name: one two
Created: 2025/02 22 Fri 16:01
Due to: 2025/02 22 Fri 16:06

> cat x.toml 
[[task]]
Name = 'one two'
Created = '2025/02 28 Fri 16:01'
DueBy = '2025/02 28 Fri 16:06'

[[task]]
Name = 'three'
Created = '2025/02 28 Fri 16:01'
DueBy = '2025/02 28 Fri 16:06'

Ok I think for this post is fine. Let's say it is the Part1 and move on to the next!

Footnotes

2

I once spent an entire afternoon organizing my bookmarks, from two different firefox profiles, my work and my personal, only to realize I had forgotten to eat lunch (╯°□°)╯︵ ┻━┻. Who knew that “bookmarks management” could be a full-time distracting job?

3

Deadlines are like zombies: they just keep coming back, no matter how many times you think you've "killed" them.