è¿å¹´ãæ®æ®µã®ä½æ¥ããã¦ã¹ã§ãããããªãæ°æã¡ãé«ã¾ã£ã¦ããï¼ãã¹ã¯å¨ããæ£ããã£ã¦ããããã ã¨ãã説ãæåã§ãï¼ãã¡ã¼ã«ã¯çµå±ã¿ã¼ããã«ã§ã¡ã¼ã«ãèªããã¨ã«ããããåé¡ãªãéããã¦ãããããã®ä»ã®ã¿ã¹ã¯ããã¼ãã¼ãã ãã§ããã«ã¯ãã¿ã¼ããã«åãã¢ããªã±ã¼ã·ã§ã³ãä½ããå¿ è¦ããããããããªãããªãã¤ã ãè¦ãç®ã¯æ´¾æãªæ¹ãããã
ãã®è¨äºã¯ Kyoto.go remote #32 LTä¼ ã§çºè¡¨ãã å ¥é Bubble Tea ã®å¢è£çã§ãã
Bubble Tea ã¨ã¯
GitHub - charmbracelet/bubbletea: A powerful little TUI framework 🏗
Bubble Tea ã¨ã¯ãGo ã§ãªãããªã¿ã¼ããã«ã¢ããªã±ã¼ã·ã§ã³ï¼TUIï¼ãä½ãããã®ãã¬ã¼ã ã¯ã¼ã¯ãCharm ã¨ããããã¸ã§ã¯ãã®ä¸é¨ã®ããã§ããã¼ã ãã¼ã¸ãè¦ã¦ããã£ããåããã¨ããç°æ§ã«ãæ´è½ã§ãããããã¯ããæ°ãåºã¾ãããBubble Tea ã¨ã¯è±èªã§ã¿ããªã«ãã£ã¼ã®ãã¨ãããããªãã»ã©ã
README ã«ããã°ãBubble Tea 㯠Elm Architecture ã«åºã¥ãããã¬ã¼ã ã¯ã¼ã¯ã§ãããã¨ã®ãã¨ã以ä¸ãElm Architecture ã解説ãã¤ã¤ bubbletea ã使ã£ã¦ããã
The Elm Architecture
ã¾ã Elm ã¨ããè¨èªããããHaskell ã©ã¤ã¯ãªé¢æ°åã§ãJavaScript ã«ãã©ã³ã¹ãã¤ã«ããããããªè¨èªã ãElm ãã¦ã§ãã§ã¤ã³ã¿ã©ã¯ãã£ã㪠UI ãå®ç¾ããããã«æ¡ç¨ãã¦ããã®ã Elm Architectureã
ãã® Elm Architecture 㯠Redux ã® prior art ã¨ãã¦ååãããããã¦ããã®ã§ããã®ãããã触ã£ããã¨ãããã°ç°¡åã«ç解ã§ããã¨æãã
Elm Architecture ã§ã¯ Model ãã¢ããªã±ã¼ã·ã§ã³ã®å¯ä¸ã®ã¹ãã¼ãã¨ãªããé¢æ° View ã«ãã£ã¦ Model ãã UI ãçæãããï¼Elm ãªã HTML ã ããBubble Tea ãªãæååï¼ãã¦ã¼ã¶ãªã©ã¢ãã«å¤é¨ããã®å ¥å㯠Msg ã¨ãã¦è¡¨ãããé¢æ° Update ã§å¦çããã¦æ°ãã Model ãçæããããæµããä¸æ¹åã«ãªã£ã¦ããã®ã§å¾¡ããããã¨ããããã
tea.Model
ãã¦ããã®ã¢ã¼ããã¯ãã£ã«åã£ã¦ãbubbletea 㧠Go ã«ããã TUI å®è£
ãè¦ã¦ãããã¾ã㯠tea.Model
ãè¦ã¦ã¿ãï¼bubbletea 㯠tea
ã¨ããååã§ããã±ã¼ã¸ãå®ç¾©ãã¦ããï¼ã
type Model interface { Init() Cmd Update(Msg) (Model, Cmd) View() string }
Cmd
ã¯ãã¨ã§è¦ããã¨ã«ãã¦ãå
ã®èª¬æã©ãããªãã¨ãåããã ããã
- Update() ã¯æ°ãã Model ãè¿ãã
- View() ã¯æååãè¿ããããããã®ã¾ã¾ã¿ã¼ããã«ã«è¡¨ç¤ºãããï¼
ã§ã¯ React ã§ãè¦ããããªã«ã¦ã³ã¿ãä½ã£ã¦ã¿ãããã¹ãã¼ã¹ãã¼ãæ¼ãããæ°ã1ãã¤å¢ããã¦ãããããªã¢ããªã±ã¼ã·ã§ã³ã«ãã¦ã¿ãã
ã½ã¼ã¹ã³ã¼ã㯠https://github.com/motemen/example-go-bubbletea/tree/main/01-counterã
type model struct { count int } func (model) Init() tea.Cmd { return nil } func (m model) View() string { return fmt.Sprintf("count: %v", m.count) }
ã©ãããã®ã¾ã¾æ¸ãä¸ãããããªå½¢ã§ãã¨ãã«è§£èª¬ããããªãã ãããã¢ããªã±ã¼ã·ã§ã³ã®ãã¸ãã¯ãéä¸ããã®ã¯ Update ã«ãªãã
ã¾ãæ¸ããªããä»åã¯ããããæãã ã
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case " ": m.count = m.count + 1 return m, nil // æ´æ°ããã¢ãã«ãè¿ã â View() ãæç»ããã } } return m, nil }
tea.Msg
ã®å®ä½ã¯ interface{}
ã§ãã¤ã¾ãä½ã§ããããã¢ããªã±ã¼ã·ã§ã³ããããã«ç¹æã® Msg ãå®è£
ãããã¨ã«ãªãããäºåã«å®ç¾©ããããã®ããããtea.KeyMsg
ã¯ãã®ä¸ã¤ãã¦ã¼ã¶ããã®ãã¼å
¥åã¯ããã§è¡¨ãããã
ã¹ãã¼ã¹ãã¼ãæ¼ãããã¨ãã«ãcount ã 1 å¢ãããæ°ãã model ãè¿ãã¦ããããã® model ã«å¯¾ã㦠View() ãå¼ã°ããçµæã®æååãã¦ã¼ã¶ã®ã¿ã¼ããã«ã«æç»ããããã¨ããç°¡åãªæµãã ãå®æï¼
ããã§ãåãã®ã ãã©ããã®ã¾ã¾ã ã¨ãã®ããã°ã©ã ã¯çµäºã§ããªããCtrl+C ã§ããã bubbletea ã奪ã£ã¦ãã¾ãããã ãããã§ããããã³ã¼ãã追å ããã
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ... switch msg.String() { case "q", "ctrl+c": return m, tea.Quit ... } ... }
Ctrl+C ãããã¨ãã«ãtea.Quit ã¨ããå¤ãè¿ãã¦ããããã® Cmd ãè¿ã㨠bubbletea ãããã°ã©ã ãçµäºãã¦ãããã¨ããããã ãããã§æ¬å½ã«å®æã
tea.Cmd
tea.Cmd ãç»å ´ãããCmd ã¨ã¯ Elm ã«ãç»å ´ããæ¦å¿µã§ãå¤çã¸ã®ï¼å¯ä½ç¨ãä¼´ãï¼å½ä»¤ã表ç¾ãã¦ãããããã¦ãã®å½ä»¤ãçµãã£ãããåãã Msg ãæ»ãã¦ãããã¨ã«ãªã£ã¦ããã
bubbletea ã«ããã¦ã¯åã« tea.Msg ãè¿ãé¢æ°ã§ãããããã«è¨ã㨠goroutine ã®ä¸ã§å®è¡ãããã
type Cmd func() Msg
ã§ã¯å ã»ã©ã®ã«ã¦ã³ã¿ãã¡ã¢ãªä¸ã§ã¯ãªãå¤ã«ç½®ããã¨ã«ãã¦ã¿ãããããã¤ããããã« CountAPI ã¨ãããµã¼ãã¹ããã£ãã®ã§ããããå©ç¨ãããã¦ã§ãã®ã¢ã¯ã»ã¹ã«ã¦ã³ã¿ã¼ã¿ããããGET ãããã«ã¦ã³ããè¿ãã¦ããã API ããããããã¤ãããããããâ¦â¦ã
ã½ã¼ã¹ã³ã¼ã㯠https://github.com/motemen/example-go-bubbletea/tree/main/02-counter-apiã
éè¦ãªã³ã¼ãã¯ä»¥ä¸ã
type countLoadedMsg struct { count int } func (m model) hitCounter() tea.Msg { count, err := m.api.Hit() // ãã㧠HTTP API ãå©ãâ¦â¦ ... return countLoadedMsg{count: count} // ã¢ãã«ã«æ¸¡ãããã® Msg }
countLoadedMsg
ã¨ããç¬èªã® Msg ãå®ç¾©ãããAPI ããçµæãè¿ã£ã¦ãããããã«ãããã§ã¢ãã«ã«æ¸¡ãã¦ããããã®ãã®ã ãåãã¿ãã°åããã¨ãããm.hitCounter
ã Cmd ã«ãªãã
ãããå©ç¨ãã Update ã¯ä»¥ä¸ã®ããã«ãªãã
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case " ": m.loading = true return m, m.hitCounter // ãã㧠Cmd ãè¿ã⦠} case countLoadedMsg: // Cmd ã®çµæãåãã¨ã m.count = msg.count m.loading = false return m, nil } return m, nil }
counter += 1
ãã代ããã« Cmd ãè¿ããçµæã帰ã£ã¦ãããã¢ãã«ãæ´æ°ããã¨ããã ããUpdate ã®ä»äºã¯ã·ã³ãã«ãªã¾ã¾ã ã
ãã®ä»ã®ã³ã¼ããå°ããã¤å¤æ´ããã
func (m model) Init() tea.Cmd { return m.hitCounter // ããã°ã©ã éå§æã«å®è¡ããã Cmd } func (m model) View() string { if m.loading { return "..." } else { return fmt.Sprintf("count: %v", m.count) } }
èµ·åæã«ã HTTP API ãå©ãã¦ã«ã¦ã³ããåå¾ãããã®ã§ãå ã»ã©ã¯ã¹ã«ã¼ãã Init() 㧠Cmd ãè¿ãã¦ããããã¨ãã¼ãä¸ã¯ããã£ã½ã表示ã«ãã¦ãããããã ãã ããã¤ãããã¯ãªæãã«ãªã£ã¦ã¾ããã¾ããã
bubbles ã§ã³ã³ãã¼ãã³ãã使ã
ãããããããªãããªæããããªããã ãâ¦â¦ãããã§ããããbubbletea ã®ããã¨ãã㯠bubbles ã¨ããã³ã³ãã¼ãã³ãéãããã¨ããã ã
GitHub - charmbracelet/bubbles: TUI components for Bubble Tea 🍡
README è¦ã¦ããã¢ã¬ãããï¼ ã§ã¯ãã¼ãä¸ã«ã¹ããã¼ãåãã¦ã¿ããã¨ã«ããã
bubbles ã®ã³ã³ãã¼ãã³ã㯠bubbletea ã®ã¢ãã«ã¨ãã¦æä¾ãããã®ã§ã以ä¸ã®ããã«çµã¿è¾¼ãã§ããã
type model struct { ... spinner spinner.Model } func (m model) View() string { if m.loading { return m.spinner.View() // '-' ã¨ã '/' ã¨ã '|' ã¨ãã«ãªã } ... }
åã¢ãã«ã¨ã㦠spinner.Model ãæããããã¼ãä¸ã¯ ...
ã®ä»£ããã«å転ããæ£ã表示ãããããã« View ãæ¸ãæããã
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) // ãã㧠Tick ãåãåã£ã¦ãããã¹ããã¼ã®è¡¨ç¤ºãå¤ãã ... } func (m model) Init() tea.Cmd { return tea.Batch( m.hitCounter, m.spinner.Tick, // ã¹ããã¼ã®å転ãã¯ããã ) }
Update ã§ã¯ãåã¢ãã«ã«ã Msg ã渡ããå ·ä½çã«ã¯ spinner.Tick ã¨ãã Msg ãéè¦ã§ããããæ°ç¾msãã¨ã«ã¹ããã¼ãå転ãããããã® Msg ã«ãªã£ã¦ããã
以ä¸ã®ã½ã¼ã¹ã³ã¼ã㯠https://github.com/motemen/example-go-bubbletea/tree/main/03-counter-api-spinnerã
ãã£ã¨ãªããã«ï¼
bubbles ã«ã¯ä»ã«ããããããªã³ã³ãã¼ãã³ããããããæå¾ã«ãªã¹ãã使ã£ãä¾ãããã¦çµããã«ããã試ãã« Scrapbox ã®ãã¼ã¸ãä¸è¦§ãããããªã³ã¼ããæ¸ãã¦ã¿ãã
ã³ã¼ãã®å ¨ä½ã¯ https://github.com/motemen/example-go-bubbletea/tree/main/04-list-apiã
ã¢ãã«ã¯ãããªæãã ã
type model struct { projectName string list list.Model } func initModel(projectName string) model { m := model{ projectName: projectName, list: list.New(nil, list.NewDefaultDelegate(), 1, 1), } ... return m }
list.NewDefaultDelegate()
ã¯ãªã¹ãã®è¦ç´ ãæç»ãã¦ããããªãã¸ã§ã¯ããè¿ããã¨ããããããã使ã£ã¦ããã°ããæãã«ãªãï¼éã«ããã使ããªãã¨ããã£ããè¦å´ãã¦æç»ãããã¨ã«ãªãï¼ã
ããã§ãªã¹ãã®è¦ç´ ãããæãã«æç»ãã¦ãããããã«ããªã¹ãã«è¡¨ç¤ºããã struct 㯠list.DefaultItem ãå®è£ ããã
type scrapboxPage struct { Title_ string `json:"title"` ID string `json:"id"` } // Description implements list.DefaultItem func (p scrapboxPage) Description() string { return p.ID } // Title implements list.DefaultItem func (p scrapboxPage) Title() string { return p.Title_ } // FilterValue implements list.Item func (p scrapboxPage) FilterValue() string { return p.Title_ }
API ãå©ãããçµæã Msg ã«è©°ããå¦çã¯çç¥ããªã¹ããç»é¢ãã£ã±ãã«è¡¨ç¤ºãããããWindowSizeMsg ãåãåã£ã¦ãlist.SetSize() ãããã¡ã½ããã«ã¯ Cmd ãè¿ããã®ã¨ããã§ãªããã®ã¨ããã®ã§ãæ°ã«ãã¦ä½¿ããªãã¨æã£ãããã«åããªããã¨ãããã®ã§æ³¨æã
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.list, cmd = m.list.Update(msg) switch msg := msg.(type) { case tea.WindowSizeMsg: m.list.SetSize(msg.Width, msg.Height) ... } return m, cmd }
ãã¨ã¯ãã¤ã©ã¼ãã¬ã¼ãã¿ãããªããã§ãããã§å®æãç°¡åã§ãããï¼
ã»ãã«ããããã
ã»ãã«ã bubbletea ã®ã³ã³ãããªã³ã¨ãã¦ãã¿ã¼ããã«ã«æååãæç»ãããè²ãããæãã«åãæ±ã£ããããã©ã¤ãã©ãªç¾¤ãããã®ã ãã©ç´¹ä»ããããªãã®ã§ããã¾ã§ãèªåã使ãããªãã¦ãªãé¨åããããâ¦â¦ãããã§ã¯ãã TUI ã©ã¤ããã
The Go gopher is designed by Renee French, licensed under CC BY 3.0