Go one library a day

Time:2022-1-6

brief introduction

bubbleteaIt is a simple, compact framework that can be easily used to write Tui (terminal user interface) programs. Built in simple event processing mechanism, which can respond to external events, such as keyboard keys. Let’s have a look. Have a look firstbubbleteaWhat can be done:

Go one library a day

Thanks for kiyonlin’s recommendation.

Quick use

The code in this article uses go modules.

Create directory and initialize:

$ mkdir bubbletea && cd bubbletea
$ go mod init github.com/darjun/go-daily-lib/bubbletea

installbubbleteaLibrary:

$ go get -u github.com/charmbracelet/bubbletea

bubbleteaAll programs need an implementationbubbletea.ModelType of interface:

type Model interface {
  Init() Cmd
  Update(Msg) (Model, Cmd)
  View() string
}
  • Init()Method will be called immediately when the program starts. It will do some initialization and return aCmdtellbubbleteaWhat command to execute;
  • Update()Method is used to respond to external events, return a modified model, andbubbleteaCommands executed;
  • View()Method is used to return a text string displayed on the console.

Let’s implement a ToDo list. First define the model:

type model struct {
  todos    []string
  cursor   int
  selected map[int]struct{}
}
  • todos: all outstanding items;
  • cursor: cursor position on the interface;
  • selected: identification completed.

No initialization is required to implement an emptyInit()Method and returnsnil

import (
  tea "github.com/charmbracelet/bubbletea"
)
func (m model) Init() tea.Cmd {
  return nil
}

We need to respond to key events to achieveUpdate()method. When a key event occurs, the correspondingtea.MsgCall for parameterUpdate()method. By adjusting parameterstea.MsgFor type assertion, we can process different events accordingly:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  switch msg := msg.(type) {
  case tea.KeyMsg:
    switch msg.String() {
    case "ctrl+c", "q":
      return m, tea.Quit

    case "up", "k":
      if m.cursor > 0 {
        m.cursor--
      }

    case "down", "j":
      if m.cursor < len(m.todos)-1 {
        m.cursor++
      }

    case "enter", " ":
      _, ok := m.selected[m.cursor]
      if ok {
        delete(m.selected, m.cursor)
      } else {
        m.selected[m.cursor] = struct{}{}
      }
    }
  }

  return m, nil
}

appointment:

  • ctrl+corq: exit the program;
  • upork: move the cursor up;
  • downorj: move the cursor down;
  • enteror : toggles the completion status of the event at the cursor.

handlectrl+corqWhen you press the key, you return to a specialtea.Quit, notificationbubbleteaYou need to exit the program.

Finally realizeView()Method, the string returned by this method is the text finally displayed on the console. We can assemble according to the model data in the form we want:

func (m model) View() string {
  s := "todo list:\n\n"

  for i, choice := range m.todos {
    cursor := " "
    if m.cursor == i {
      cursor = ">"
    }

    checked := " "
    if _, ok := m.selected[i]; ok {
      checked = "x"
    }

    s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
  }

  s += "\nPress q to quit.\n"
  return s
}

Cursor position>Identification, increase of completed itemsxidentification.

After the model type is defined, you need to create an object of the model;

var initModel = model{
  todos:    []string{"cleanning", "wash clothes", "write a blog"},
  selected: make(map[int]struct{}),
}

In order for the program to work, we also create abubbleteaApplication object, throughbubbletea.NewProgram()Complete, and then call the object.Start()Method start execution:

func main() {
  cmd := tea.NewProgram(initModel)
  if err := cmd.Start(); err != nil {
    fmt.Println("start failed:", err)
    os.Exit(1)
  }
}

function:

Go one library a day

GitHub Trending

A simple todo application doesn’t seem interesting. Next, let’s write a program to pull the GitHub trending warehouse and display it on the console.

The interface of GitHub trending is as follows:

Go one library a day

You can select a language (spoke language), a language (programming language), and a time range (today, this week, this month). Since GitHub does not provide the official API for trending, we can only crawl the web page to analyze it ourselves. Fortunately, go has a powerful analysis tool goquery, which provides powerful functions comparable to jQuery. I also wrote an article about it before — go query, a library every day.

Open the chrome console and click the Elements tab to view the structure of each item:

Go one library a day

Base version

Define model:

type model struct {
  repos []*Repo
  err   error
}

amongreposThe field represents the pulled trend warehouse list and the structureRepoAs follows, the meanings of the fields are clearly Annotated:

type Repo struct {
  Name string // warehouse name
  Author string // author name
  Link string // link
  Desc string // description
  Lang string // language
  Stars int // number of stars
  Forks int // number of forks
  Add int // new in the cycle
  Builtby [] string // contribution value avatar img link
}

errField indicates the error value set for pull failure. In order to execute the network request to pull the trending list when the program starts, we let the modelInit()Method returns atea.CmdValue of type:

func (m model) Init() tea.Cmd {
  return fetchTrending
}

func fetchTrending() tea.Msg {
  repos, err := getTrending("", "daily")
  if err != nil {
    return errMsg{err}
  }

  return repos
}

tea.CmdType:

// src/github.com/charmbracelet/bubbletea/tea.go
type Cmd func() Msg

tea.CmdThe bottom layer is a function type. The function has no parameters and returns atea.MsgObject.

fetchTrending()The function pulls the current trend list of GitHub. If an error is encountered, it returnserrorValue. Here we ignore it for the time beinggetTrending()The implementation of the function has little to do with the focus we want to talk about. Children’s shoes interested can go to my GitHub warehouse to view the detailed code.

If you need to do something when the program starts, it will usually be in theInit()Methodtea.CmdteaThis function will be executed in the background and will eventually returntea.MsgPassed to modelUpdate()method.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  switch msg := msg.(type) {
  case tea.KeyMsg:
    switch msg.String() {
    case "q", "ctrl+c", "esc":
      return m, tea.Quit
    default:
      return m, nil
    }

  case errMsg:
    m.err = msg
    return m, nil

  case []*Repo:
    m.repos = msg
    return m, nil

  default:
    return m, nil
  }
}

Update()The method is also relatively simple. First, you need to listen for key events. We agree to press Q or Ctrl + C or ESC to exit the program.The string corresponding to the specific key indicates that you can view the document or source codebubbletea/key.gofile。 ReceivederrMsgType of message, indicating that the network request failed, and the error value is recorded. Received[]*RepoType of message, indicating the correct returned trend warehouse list, and record it. stayView()In the function, we display information such as pulling, pulling failure and correct pulling:

func (m model) View() string {
  var s string
  if m.err != nil {
    s = fmt.Sprintf("Fetch trending failed: %v", m.err)
  } else if len(m.repos) > 0 {
    for _, repo := range m.repos {
      s += repoText(repo)
    }
    s += "--------------------------------------"
  } else {
    s = " Fetching GitHub trending ..."
  }
  s += "\n\n"
  s += "Press q or ctrl + c or esc to exit..."
  return s + "\n"
}

The logic is clear, iferrField is notnilIndicates failure, otherwise there is warehouse data and the warehouse information is displayed. Otherwise, it is being pulled. Finally, a prompt message is displayed to tell the customer how to exit the program.

The display logic of each warehouse item is as follows, which is divided into three columns: basic information, description and link:

func repoText(repo *Repo) string {
  s := "--------------------------------------\n"
  s += fmt.Sprintf(`Repo:  %s | Language:  %s | Stars:  %d | Forks:  %d | Stars today:  %d
`, repo.Name, repo.Lang, repo.Stars, repo.Forks, repo.Add)
  s += fmt.Sprintf("Desc:  %s\n", repo.Desc)
  s += fmt.Sprintf("Link:  %s\n", repo.Link)
  return s
}

Run (multi file run cannot be used)go run main.go):

Go one library a day

Acquisition failure (domestic GitHub is unstable, and you will always encounter it if you try several times):

Go one library a day

Successfully obtained:

Go one library a day

Make the interface more beautiful

We’ve seen too much black and white. Can we make the font show different colors? Certainly.bubbleteaCan uselipglossThe library adds various colors to the text. We define four colors. The RBG value of the color is http://tool.chinaz.com/tools/pagecolor.aspx Pick:

var (
  cyan  = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FFFF"))
  green = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32"))
  gray  = lipgloss.NewStyle().Foreground(lipgloss.Color("#696969"))
  gold  = lipgloss.NewStyle().Foreground(lipgloss.Color("#B8860B"))
)

To change the color of the text, you only need to call the corresponding color objectRender()Method to pass in the text. For example, we want to change the prompt to dark gray, use dark yellow for the middle text, and modify itView()method:

func (m model) View() string {
  var s string
  if m.err != nil {
    s = gold.Render(fmt.Sprintf("fetch trending failed: %v", m.err))
  } else if len(m.repos) > 0 {
    for _, repo := range m.repos {
      s += repoText(repo)
    }
    s += cyan.Render("--------------------------------------")
  } else {
    s = gold.Render(" Fetching GitHub trending ...")
  }
  s += "\n\n"
  s += gray.Render("Press q or ctrl + c or esc to exit...")
  return s + "\n"
}

Then we use cyan for the basic information of the warehouse, green for the description and dark gray for the link:

func repoText(repo *Repo) string {
  s := cyan.Render("--------------------------------------") + "\n"
  s += fmt.Sprintf(`Repo:  %s | Language:  %s | Stars:  %s | Forks:  %s | Stars today:  %s
`, cyan.Render(repo.Name), cyan.Render(repo.Lang), cyan.Render(strconv.Itoa(repo.Stars)),
    cyan.Render(strconv.Itoa(repo.Forks)), cyan.Render(strconv.Itoa(repo.Add)))
  s += fmt.Sprintf("Desc:  %s\n", green.Render(repo.Desc))
  s += fmt.Sprintf("Link:  %s\n", gray.Render(repo.Link))
  return s
}

Run again:

Go one library a day

success:

Go one library a day

Well, it looks much better now.

I’m not lazy

Sometimes the network is very slow, and a prompt that a request is being processed can make us more assured (the program is still running, not lazy).bubbleteaBrother warehousebubblesProvides a program calledspinnerThe component, which only displays some characters, has been changing, giving us a feeling that the task is being processed.spinnerstaygithub.com/charmbracelet/bubbles/spinnerIn the package, you need to import it first. Then add in the modelspinner.ModelField:

type model struct {
  repos   []*Repo
  err     error
  spinner spinner.Model
}

When creating a model, initialization is also requiredspinner.ModelObject, we specifyspinnerThe text color is purple:

var purple = lipgloss.NewStyle().Foreground(lipgloss.Color("#800080"))

func newModel() model {
  sp := spinner.NewModel()
  sp.Style = purple

  return model{
    spinner: sp,
  }
}

spinneradoptTickTo trigger it to change state, so you need toInit()MethodTickofCmd。 But you need to returnfetchTrendingbubbleteaProvidedBatchYou can combine twoCmdMerge together to return:

func (m model) Init() tea.Cmd {
  return tea.Batch(
    spinner.Tick,
    fetchTrending,
  )
}

thenUpdate()In the method, we need to updatespinnerInit()Methodspinner.TickWill producespinner.TickMsg, we deal with it:

case spinner.TickMsg:
  var cmd tea.Cmd
  m.spinner, cmd = m.spinner.Update(msg)
  return m, cmd

spinner.Update(msg)Return atea.CmdObject driven next timeTick

Finally inView()In the method, we willspinnerShow it. Call itsView()Method returns the string of the current state and spell it at the position we want to display:

func (m model) View() string {
  var s string
  if m.err != nil {
    s = gold.Render(fmt.Sprintf("fetch trending failed: %v", m.err))
  } else if len(m.repos) > 0 {
    for _, repo := range m.repos {
      s += repoText(repo)
    }
    s += cyan.Render("--------------------------------------")
  } else {
    //Here
    s = m.spinner.View() + gold.Render(" Fetching GitHub trending ...")
  }
  s += "\n\n"
  s += gray.Render("Press q or ctrl + c or esc to exit...")
  return s + "\n"
}

function:

Go one library a day

paging

Since many GitHub warehouses are returned at one time, we want to display them in pages. Five items are displayed on each page. You can presspageupandpagedownTurn the page. First, add two fields in the model, current page and total pages:

const (
  CountPerPage = 5
)

type model struct {
  // ...
  curPage   int
  totalPage int
}

When pulling to the warehouse, calculate the total pages:

case []*Repo:
  m.repos = msg
  m.totalPage = (len(msg) + CountPerPage - 1) / CountPerPage
  return m, nil

In addition, you need to monitor the page turning button:

case "pgdown":
  if m.curPage < m.totalPage-1 {
    m.curPage++
  }
  return m, nil
case "pgup":
  if m.curPage > 0 {
    m.curPage--
  }
  return m, nil

stayView()In the method, we calculate which warehouses need to be displayed according to the current page:

start, end := m.curPage*CountPerPage, (m.curPage+1)*CountPerPage
if end > len(m.repos) {
  end = len(m.repos)
}

for _, repo := range m.repos[start:end] {
  s += repoText(repo)
}
s += cyan.Render("--------------------------------------")

Finally, if the total number of pages is greater than 1, the prompt of page turning key is given:

if m.totalPage > 1 {
  s += gray.Render("Pagedown to next page, pageup to prev page.")
  s += "\n"
}

function:

Go one library a day

Great. We only showed five pages. Try turning the page:

Go one library a day

summary

bubbleteaIt provides a basic framework for running Tui program. What we want to display, the style we want to display, and what events we want to handle are all specified by ourselves.bubbleteaWarehouseexamplesThere are several sample programs in the folder. Children’s shoes interested in writing Tui programs must not be missed. In addition, its brother warehousebubblesMany components are also provided in.

If you find a fun and easy-to-use go language library, you are welcome to submit an issue on GitHub, the daily library of go

reference resources

  1. bubbletea GitHub:https://github.com/charmbracelet/bubbletea
  2. bubble GitHub:https://github.com/charmbracelet/bubbles
  3. Go one library a day GitHub: https://github.com/darjun/go-daily-lib
  4. issue:https://github.com/darjun/go-daily-lib/issues/22

I

My blog: https://darjun.github.io

Welcome to my WeChat official account, GoUpUp, learn together and make progress together.