Go로 CUI 툴을 쉽고 편하게! gocui 사용기
안녕하세요.
요즘 Go로 CUI·CLI 툴을 만드는 것에 푹 빠져 있는데요.
CUI 툴을 만들 때 사용하고 있는 라이브러리로 gocui라는 것이 있습니다.
어떤 건가요?
터미널 상에서 HTML의 폼(form)처럼 입력 인터페이스를 간단하게 만들 수 있습니다.
버튼이나 체크박스 등도 준비해두었는데요.
사용 방법
[_demos Github 예제](https://github.com/skanehira/gocui-component/tree/master/_demos에 있는 select 샘플을 바탕으로 설명해보겠습니다.
func main() {
gui, err := gocui.NewGui(gocui.Output256)
if err != nil {
panic(err)
}
defer gui.Close()
if err := gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
panic(err)
}
component.NewSelect(gui, "Programming Language:", 0, 0, 21, 10).
AddOptions("Go", "Java", "PHP", "Python", "Ruby", "C", "C++", "C#").
Draw()
if err := gui.MainLoop(); err != nil && err != gocui.ErrQuit {
panic(err)
}
}
gocui.NewGui(gocui.Output256)
로 gocui 인스턴스를 생성해둡니다.component.NewSelect
로 select 컴포넌트 인스턴스를 생성합니다.AddOptions
로 select 목록에 선택하고 싶은 옵션들을 추가합니다.Draw
로 gocui 인스턴스에 설정을 추가합니다.gui.MainLoop()
를 호출하여, gocui 인스턴스에 추가된 설정을 바탕으로 화면 그리기나 키 바인딩 처리를 실행합니다.
기본 흐름은 위와 같이 gocui 인스턴스를 생성하고, 그것을 컴포넌트에 전달한 후에 메서드로 각 설정을 해주고 Draw
로 전달된 gocui 인스턴스에 설정을 추가하는 방식인데요.
gocui 자체는 gocui 인스턴스에 뷰(view)의 설정을 추가한 후에 MainLoop
로 한꺼번에 처리하는 동작을 하고 있습니다.
내부 처리
간단한 사용 방법을 설명했으니, 위의 Select 컴포넌트 내부에서 어떤 처리를 하고 있는지 살펴볼까요?
Select의 구조체는 다음과 같습니다.
type Select struct {
*InputField
options []string // 선택할 옵션 리스트를 보유
currentOpt int // 현재 선택된 옵션의 인덱스
isExpanded bool // 옵션 리스트가 열려 있는지 여부를 판단하는 플래그
ctype ComponentType // 컴포넌트 타입, Form에서 컴포넌트 판정에 사용
listColor *Attributes // 옵션 리스트의 색상 정의
listHandlers Handlers // 옵션 리스트를 열었을 때의 동작 정의
}
Select 자체는 InputField
컴포넌트를 내장하고 있으며, 그것을 확장한 컴포넌트입니다.
아래는 select 컴포넌트를 생성할 때의 처리입니다.
// NewSelect new select
func NewSelect(gui *gocui.Gui, label string, x, y, labelWidth, fieldWidth int) *Select {
s := &Select{
InputField: NewInputField(gui, label, x, y, labelWidth, fieldWidth), // InputField 인스턴스 생성
listHandlers: make(Handlers),
ctype: TypeSelect,
}
// Enter로 옵션 리스트를 열 수 있도록 InputField의 AddHandler를 사용하여 정의
s.AddHandler(gocui.KeyEnter, s.expandOpt)
// 옵션 리스트의 색상을 정의
s.AddAttribute(gocui.ColorBlack, gocui.ColorWhite, gocui.ColorBlack, gocui.ColorGreen).
// AddListHandler를 이용하여 옵션 리스트를 열었을 때, j/k 또는 ↑/↓로 이동, Enter로 선택할 수 있도록 정의
AddListHandler('j', s.nextOpt).
AddListHandler('k', s.preOpt).
AddListHandler(gocui.KeyArrowDown, s.nextOpt).
AddListHandler(gocui.KeyArrowUp, s.preOpt).
AddListHandler(gocui.KeyEnter, s.selectOpt).
// `InputField`를 내장하고 있으므로, 입력하지 못하도록 설정해야 함
SetEditable(false)
return s
}
Select에서 어려웠던 것은 옵션 리스트를 어떻게 표시하고 선택할 수 있게 할 것인가 하는 점인데요.
gocui의 사양상, 뷰(view)를 생성하여 뷰의 영역 내에서 문자를 그려야 합니다.
즉, 옵션 리스트를 표시할 때는 옵션 수만큼 뷰를 정의해야 합니다.
또한, 어떤 옵션을 선택하고 있는지 알 수 있도록 포커스 처리나 선택 후 닫는 처리도 필요합니다.
옵션 리스트를 표시하는 처리는 expandOpt
에서 하고 있으니, 그 부분을 살펴보겠습니다.
func (s *Select) expandOpt(g *gocui.Gui, vi *gocui.View) error {
if s.hasOpts() {
s.isExpanded = true
g.Cursor = false
x := s.field.X
w := s.field.W
y := s.field.Y
h := y + 2
for _, opt := range s.options {
// 옵션 리스트는 아래로 펼쳐지므로 y와 h 좌표를 증가시킵니다.
y++
h++
// 옵션마다 뷰를 정의합니다.
if v, err := g.SetView(opt, x, y, w, h); err != nil {
if err != gocui.ErrUnknownView {
panic(err)
}
v.Frame = false
v.SelFgColor = s.listColor.textColor
v.SelBgColor = s.listColor.textBgColor
v.FgColor = s.listColor.hilightColor
v.BgColor = s.listColor.hilightBgColor
// 설정한 키 바인딩을 옵션마다 추가합니다.
for key, handler := range s.listHandlers {
if err := g.SetKeybinding(v.Name(), key, gocui.ModNone, handler); err != nil {
panic(err)
}
}
fmt.Fprint(v, opt)
}
}
// 리스트를 열었을 때 선택된 옵션에 포커스를 맞춥니다.
v, _ := g.SetCurrentView(s.options[s.currentOpt])
v.Highlight = true
}
return nil
}
Select
에서 Enter를 누르면 위의 처리가 실행되어, 옵션마다 뷰 좌표를 증가시키면서 그립니다. 방법 자체는 단순하지만, 코드량과 처리량이 많은 것이 문제네요.
간단히 요약하면,
- 옵션 리스트를 Enter로 동적으로 생성하려면 옵션마다 뷰를 생성하고, 이동과 선택의 키 바인딩을 추가하는 처리가 필요하다.
- 이동은 이전 뷰의 포커스를 해제하고, 다음 뷰에 포커스를 맞추는 처리가 필요하다.
- 옵션 리스트에서 Enter를 누르면 선택한 옵션을 반영하고, 리스트를 닫는 처리가 필요하다.
이런 것들을 생각하고 구현해야 합니다.
꽤 힘든 작업입니다.
차라리 다른 라이브러리를 사용하는 것이 더 낫지 않을까 생각도 듭니다.
gocui의 앞으로와 그 대안
gocui의 앞으로에 대해서인데요, 제작자 본인이 별로 활동하지 않는 것 같아서, 풀 리퀘스트가 머지될 기미도 없고, 새로운 기능이 추가될 것을 크게 기대할 수는 없을 것 같습니다.
gocui의 대안이 될 만한 것을 몇 개 찾아봤는데, 그중 가장 괜찮아 보이는 것이 tview였습니다.
gocui와 비교해서 tview는 아직 나온 지 몇년 안된 거 같고, 개발이 꽤 활발하며 폼이나 테이블 등의 컴포넌트가 기본 탑재되어 있어서 리치한 CUI 라이브러리입니다.
'Go' 카테고리의 다른 글
Go 실행 파일에 ZIP으로 리소스 임베딩하기: 간단하게 알아볼까요? (0) | 2024.09.19 |
---|---|
Go 언어 fmt.Printf 완벽 가이드 (0) | 2024.09.19 |
AriaSQL: Go 언어로 만든 새로운 관계형 데이터베이스의 탄생 (2) | 2024.09.07 |
Argon/Bcrypt가 CPU를 100% 사용하는 이유와 해결책 (2) | 2024.09.07 |
Go 1.23 iter 패키지 완전 정복 (0) | 2024.08.24 |