<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 每日一庫之 bubbletea

          共 15596字,需瀏覽 32分鐘

           ·

          2021-07-27 06:51

          簡介

          bubbletea是一個簡單、小巧、可以非常方便地用來編寫 TUI(terminal User Interface,控制臺界面程序)程序的框架。內置簡單的事件處理機制,可以對外部事件做出響應,如鍵盤按鍵。一起來看下吧。先看看bubbletea能做出什么效果:

          感謝kiyonlin推薦。

          快速使用

          本文代碼使用 Go Modules。

          創(chuàng)建目錄并初始化:

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

          安裝bubbletea庫:

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

          bubbletea程序都需要有一個實現bubbletea.Model接口的類型:

          type Model interface {
            Init() Cmd
            Update(Msg) (Model, Cmd)
            View() string
          }
          • Init()方法在程序啟動時會立刻調用,它會做一些初始化工作,并返回一個Cmd告訴bubbletea要執(zhí)行什么命令;
          • Update()方法用來響應外部事件,返回一個修改后的模型,和想要bubbletea執(zhí)行的命令;
          • View()方法用于返回在控制臺上顯示的文本字符串。

          下面我們來實現一個 Todo List。首先定義模型:

          type model struct {
            todos    []string
            cursor   int
            selected map[int]struct{}
          }
          • todos:所有待完成事項;
          • cursor:界面上光標位置;
          • selected:已完成標識。

          不需要任何初始化工作,實現一個空的Init()方法,并返回nil

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

          我們需要響應按鍵事件,實現Update()方法。按鍵事件發(fā)生時會以相應的tea.Msg為參數調用Update()方法。通過對參數tea.Msg進行類型斷言,我們可以對不同的事件進行對應的處理:

          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
          }

          約定:

          • ctrl+cq:退出程序;
          • upk:向上移動光標;
          • downj:向下移動光標;
          • enter:切換光標處事項的完成狀態(tài)。

          處理ctrl+cq按鍵時,返回一個特殊的tea.Quit,通知bubbletea需要退出程序。

          最后實現View()方法,這個方法返回的字符串就是最終顯示在控制臺上的文本。我們可以按照自己想要的形式,根據模型數據拼裝:

          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
          }

          光標所在位置用>標識,已完成的事項增加x標識。

          模型類型定義好了之后,需要創(chuàng)建一個該模型的對象;

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

          為了讓程序工作,我們還要創(chuàng)建一個bubbletea的應用對象,通過bubbletea.NewProgram()完成,然后調用這個對象的Start()方法開始執(zhí)行:

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

          運行:

          GitHub Trending

          一個簡單的 Todo 應用看起來好像沒什么意思。接下來,我們一起編寫一個拉取 GitHub Trending 倉庫并顯示在控制臺的程序。

          Github Trending 的界面如下:

          可以選擇語言(Spoken Language,本地語言)、語言(Language,編程語言)和時間范圍(Today,This week,This month)。由于 GitHub 沒有提供 trending 的官方 API,我們只能爬取網頁自己來分析。好在 Go 有一個強大的分析工具goquery,提供了堪比 jQuery 的強大功能。我之前也寫過一篇文章介紹它——Go 每日一庫之 goquery。

          打開 Chrome 控制臺,點擊 Elements 頁簽,查看每個條目的結構:

          基礎版本

          定義模型:

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

          其中repos字段表示拉取到的 Trending 倉庫列表,結構體Repo如下,字段含義都有注釋,很清晰了:

          type Repo struct {
            Name    string   // 倉庫名
            Author  string   // 作者名
            Link    string   // 鏈接
            Desc    string   // 描述
            Lang    string   // 語言
            Stars   int      // 星數
            Forks   int      // fork 數
            Add     int      // 周期內新增
            BuiltBy []string // 貢獻值 avatar img 鏈接
          }

          err字段表示拉取失敗設置的錯誤值。為了讓程序啟動時,就去執(zhí)行網絡請求拉取 Trending 的列表,我們讓模型的Init()方法返回一個tea.Cmd類型的值:

          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.Cmd類型為:

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

          tea.Cmd底層是一個函數類型,函數無參數,并且返回一個tea.Msg對象。

          fetchTrending()函數拉取 GitHub 的今日 Trending 列表,如果遇到錯誤,則返回error值。這里我們暫時忽略getTrending()函數的實現,這個與我們要說的重點關系不大,感興趣的童鞋可以去我的 GitHub 倉庫查看詳細代碼。

          程序啟動時如果需要做一些操作,通常就會在Init()方法中返回一個tea.Cmdtea后臺會執(zhí)行這個函數,最終將返回的tea.Msg傳給模型的Update()方法。

          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()方法也比較簡單,首先還是需要監(jiān)聽按鍵事件,我們約定按下 q 或 ctrl+c 或 esc 退出程序。具體按鍵對應的字符串表示可以查看文檔或源碼bubbletea/key.go文件。接收到errMsg類型的消息,表示網絡請求失敗了,記錄錯誤值。接收到[]*Repo類型的消息,表示正確返回的 Trending 倉庫列表,記錄下來。在View()函數中,我們顯示正在拉取,拉取失敗和正確拉取等信息:

          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"
          }

          邏輯很清晰,如果err字段不為nil表示失敗,否則有倉庫數據,顯示倉庫信息。否則正在拉取中。最后顯示一條提示信息,告訴客戶怎么退出程序。

          每個倉庫項的顯示邏輯如下,分為 3 列,基礎信息、描述和鏈接:

          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
          }

          運行(多文件運行不能用go run main.go):

          獲取失敗(國內 GitHub 不穩(wěn)定,多試幾次總會遇到??):

          獲取成功:

          讓界面更美觀

          黑白色我們已經看了太多太多了,能不能讓字體呈現不同的顏色呢?當然可以。bubbletea可以利用lipgloss庫給文本添加各種顏色,我們定義了 4 種顏色,顏色的 RBG 值是我在http://tool.chinaz.com/tools/pagecolor.aspx挑的:

          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"))
          )

          想要將文本變?yōu)槭裁搭伾恍枰{用對應顏色對象的Render()方法將文本傳入即可。例如我們想讓提示變?yōu)榘祷疑虚g文字使用暗黃色,修改View()方法:

          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"
          }

          然后倉庫的基本信息我們用青色(cyan),描述用綠色,鏈接用暗灰色:

          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
          }

          再次運行:

          成功:

          嗯,現在好看多了。

          我沒有偷懶

          有時候網絡很慢,加上一個請求正在處理的提示能讓我們更放心(程序還在跑,沒偷懶)。bubbletea的兄弟倉庫bubbles提供了一個叫做spinner的組件,它只是顯示一些字符,一直在變化,給我們造成一種任務正在處理中的感覺。spinnergithub.com/charmbracelet/bubbles/spinner包中,需要先引入。然后在模型中增加spinner.Model字段:

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

          創(chuàng)建模型時,同時需要初始化spinner.Model對象,我們指定spinner的文本顏色為紫色:

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

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

            return model{
              spinner: sp,
            }
          }

          spinner通過Tick來觸發(fā)其改變狀態(tài),所以需要在Init()方法中返回觸發(fā)TickCmd。但是又需要返回fetchTrendingbubbletea提供了Batch可以將兩個Cmd合并在一起返回:

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

          然后Update()方法中我們需要更新spinnerInit()方法返回的spinner.Tick會產生spinner.TickMsg,我們對其做處理:

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

          spinner.Update(msg)返回一個tea.Cmd對象驅動下一次Tick

          最后在View()方法中,我們將spinner顯示出來。調用其View()方法返回當前狀態(tài)的字符串,拼在我們想要顯示的位置:

          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 = 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"
          }

          運行:

          分頁

          由于一次返回了很多 GitHub 倉庫,我們想對其進行分頁顯示,每頁顯示 5 條,可以按pageuppagedown翻頁。首先在模型中增加兩個字段,當前頁和總頁數:

          const (
            CountPerPage = 5
          )

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

          拉取到倉庫時,計算總頁數:

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

          另外需要監(jiān)聽翻頁按鍵:

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

          View()方法中,我們根據當前頁計算需要顯示哪些倉庫:

          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("--------------------------------------")

          最后,如果總頁數大于 1,給出翻頁按鍵的提示:

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

          運行:

          很棒,我們只顯示了 5 頁。試試翻頁吧:

          總結

          bubbletea提供了一個 TUI 程序運行的基本框架。我們要顯示什么,顯示的樣式,要對哪些事件進行處理都由我們自己指定。bubbletea倉庫的examples文件夾中有多個示例程序,對編寫 TUI 程序感興趣的童鞋千萬不能錯過。另外它的兄弟倉庫bubbles中也提供了不少組件。

          大家如果發(fā)現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??

          參考

          1. bubbletea GitHub:https://github.com/charmbracelet/bubbletea
          2. bubble GitHub:https://github.com/charmbracelet/bubbles
          3. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
          4. issue:https://github.com/darjun/go-daily-lib/issues/22


          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復 ebook 獲取;還可以回復「進群」,和數萬 Gopher 交流學習。

          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操操操电影| 久久五月天电影 | aⅴ中文字幕不卡在线无码 | 亚洲一级Av无码毛片久久精品 | 日插夜插天天插 |