Shellish, a Shell-ish program written in Go using Pterm and Survey packages.

Shellish, a Shell-ish program written in Go using Pterm and Survey packages.

Shellish is a side project I started when I was looking in the build-your-own-x repo for a project to build in Go. And started to follow the tutorial about how to build a shell in Go. And I have the idea to build something similar but using Pterm and Survey packages.

I didn't use Pterm and Survey extensively in this program. These packages have more features, with examples in their docs. Also, this program only navigates through the file system, shows you the files in a directory, the current path, and lets you go to a child directory or a father directory.

This is the result:

image.png

image.png

You use the arrows of your keyboard and select the options. I use the files system to execute the commands.

Let's start explaining the commands:

"dir": It will show all the files in the directory and their seizes in bytes.

"cd": It will take to a child directory.

"cd..": it will take you to the father directory.

"current path": it will show the current path.

"exit": To end the program.

Let's begin writing the options:

main.go

package main

import (
    "fmt"

    "github.com/AlecAivazis/survey/v2"
)

func ListOptions() (cmd string) {
    cmd = ""
    prompt := &survey.Select{
        Message: "Hi, choose a cmd:",
        Options: []string{"dir", "cd", "cd..", "current path", "exit"},
        Default: "dir",
    }
    err := survey.AskOne(prompt, &cmd)

    if err != nil {
        fmt.Println(err.Error())

    }
    return cmd

}

The ListOptions function displays the list of commands in the terminal. In this function, we will use the Survey package to display a list. First, we declare a variable, in this case, cmd. Then we declare prompt and instantiate the select struct and write the message we want to display, the options, and a Default option. Then we use the AskOne function and pass prompt and &cmd as arguments. The option selected is stored in cmd.

func Cases(cmd string) {

    switch cmd {
    case "dir":
        FilesTable()

    case "cd":
        directory := ""
        prompt := &survey.Input{
            Message: "Write the name of a child directory:",
        }
        survey.AskOne(prompt, &directory)

        err := os.Chdir(directory)
        if err != nil {
            log.Println(err)
        }

        path, err := os.Getwd()
        if err != nil {
            log.Println(err)
        }
        fmt.Println(path)

    case "cd..":
        err := os.Chdir("../")
        if err != nil {
            log.Println(err)
        }

        path, err := os.Getwd()
        if err != nil {
            log.Println(err)
        }
        fmt.Println(path)

    case "current path":
        path, err := os.Getwd()
        if err != nil {
            log.Println(err)
        }
        fmt.Println(path)

    case "exit":
        os.Exit(0)
    }

}

The Cases function takes the option selected and stored in cmd, and uses a switch case, If "dir" was selected, executes FilesTable().

If "cd" was selected, instantiate Input from survey package, write a message, and then the directory's name written in the terminal when the program is executed is stored in directory (the variable).

We pass the directory to os.Chdir, this method takes you to a child folder.

Then CurrentPath executes and the current path is printed.

If "cd.." was selected it executes os.Chdir(".../"), to go back to the parent folder.

If "current path" was selected it prints the current path.

if "exit" was selected it executes os.Exit(0) to exit the program.

func ListFiles() ([]string, error) {

    var files []string

    path, err := os.Getwd()

    if err != nil {
        log.Println(err)
    }

    fileInfo, err := ioutil.ReadDir(path)
    if err != nil {
        log.Println(err)
    }

    for _, file := range fileInfo {
        files = append(files, file.Name())
    }
    return files, err

}

In ListFiles() we called os.Getwd(), it returns a rooted path name corresponding to the current directory. Then, we use the path and pass it as an argument to ioutil.ReadDir(), returns a list of fs.FileInfo for the directory's contents. We iterate through fileInfo to get only the file names and store them in files.

func SizeFile(name string) (string, error) {

    files, err := os.Stat(name)
    if err != nil {
        log.Println(err)
    }

    fileSize := fmt.Sprint(files.Size())
    return fileSize, err

}

In SizeFile() we called os.Stat() that returns the FileInfo structure describing file and get its size.

func FilesTable() error {

    d := pterm.TableData{{"File Name", "Size(bytes)"}}

    fileName, err := ListFiles()
    if err != nil {
        log.Println(err)
    }

    for _, file := range fileName {

        sizeFile, err := SizeFile(file)
        if err != nil {
            log.Println(err)
        }

        d = append(d, []string{file, sizeFile})
    }
    pterm.DefaultTable.WithHasHeader().WithData(d).Render()
    return err

}

In FilesTable() we use pterm.TableData to display a table in our terminal. then we called ListFiles() to get a list of files in the directory. Then we iterate through the list of files and pass every name as an argument in SizeFile to get its size. Then we append them to the table and render it.

And I want to make clear that this only shows the size of the files, not subdirectories.

func main() {
    pterm.DisableColor()

    bigText, _ := pterm.DefaultBigText.WithLetters(pterm.NewLettersFromString("Shellish")).Srender()

    pterm.DefaultCenter.Println(bigText)

    for {

        cmd := ListOptions()
        Cases(cmd)

    }
}

In my case, I have to use pterm.DisableColor() because I'm using Windows 7, Windows CMD does not support colors using ANSI codes before Windows 10. We store in bigText the word we want to display in the terminal, in this case, the name of the program "Shellish".

We use a loop to always display a list of options and execute them.

If you have any recommendations about other packages, architectures, how to improve my code, my English, anything; please leave a comment or contact me through Twitter, LinkedIn.

The source code is here

Reference: