diff --git a/cmd/terminal-client/main.go b/cmd/terminal-client/main.go index 60b15e9..d1752b8 100644 --- a/cmd/terminal-client/main.go +++ b/cmd/terminal-client/main.go @@ -8,15 +8,18 @@ import ( ) func main() { + logger := tui.NewLogger() + p, err := tui.New(tui.Config{ TMDBAPIKey: os.Getenv("TMDB_API_KEY"), EMDBAPIKey: os.Getenv("EMDB_API_KEY"), EMDBBaseURL: "https://emdb.ewintr.nl", - }) + }, logger) if err != nil { fmt.Println(err) os.Exit(1) } + logger.SetProgram(p) if _, err := p.Run(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/terminal-client/tui/basemodel.go b/cmd/terminal-client/tui/basemodel.go new file mode 100644 index 0000000..c873468 --- /dev/null +++ b/cmd/terminal-client/tui/basemodel.go @@ -0,0 +1,136 @@ +package tui + +import ( + "fmt" + "strings" + + "ewintr.nl/emdb/client" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type baseModel struct { + config Config + emdb *client.EMDB + tmdb *client.TMDB + Tabs []string + TabContent tea.Model + activeTab int + ready bool + logger *Logger + logViewport viewport.Model + windowSize tea.WindowSizeMsg + contentSize tea.WindowSizeMsg + tabSize tea.WindowSizeMsg +} + +func (m baseModel) Init() tea.Cmd { + return nil +} + +func (m baseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q", "esc": + return m, tea.Quit + case "right", "tab": + m.activeTab = min(m.activeTab+1, len(m.Tabs)-1) + return m, nil + case "left", "shift+tab": + m.activeTab = max(m.activeTab-1, 0) + return m, nil + } + case tea.WindowSizeMsg: + m.windowSize = msg + if !m.ready { + m.initialModel() + } + m.Log(fmt.Sprintf("new window size: %dx%d", msg.Width, msg.Height)) + m.setSize() + tabSize := TabSizeMsgType{ + Width: m.contentSize.Width, + Height: m.contentSize.Height, + } + m.TabContent, cmd = m.TabContent.Update(tabSize) + cmds = append(cmds, cmd) + m.Log("done with resize") + } + + m.TabContent, cmd = m.TabContent.Update(msg) + cmds = append(cmds, cmd) + + m.logViewport.SetContent(strings.Join(m.logger.Lines, "\n")) + m.logViewport.GotoBottom() + m.logViewport, cmd = m.logViewport.Update(msg) + + return m, tea.Batch(cmds...) +} + +func (m *baseModel) Log(msg string) { + m.logger.Log(msg) +} + +func (m baseModel) View() string { + if !m.ready { + return "\n Initializing..." + } + + doc := strings.Builder{} + doc.WriteString(m.renderMenu()) + doc.WriteString("\n") + doc.WriteString(m.renderTabContent()) + doc.WriteString("\n") + doc.WriteString(m.renderLog()) + return docStyle.Render(doc.String()) +} + +func (m *baseModel) renderMenu() string { + var items []string + for i, t := range m.Tabs { + if i == m.activeTab { + items = append(items, lipgloss.NewStyle(). + Foreground(colorHighLightForeGround). + Render(fmt.Sprintf(" * %s ", t))) + continue + } + + items = append(items, lipgloss.NewStyle(). + Foreground(colorNormalForeground). + Render(fmt.Sprintf(" %s ", t))) + } + + return lipgloss.PlaceHorizontal(m.contentSize.Width, lipgloss.Left, lipgloss.JoinHorizontal(lipgloss.Top, items...)) +} + +func (m *baseModel) renderTabContent() string { + content := m.TabContent.View() + return windowStyle.Width(m.contentSize.Width).Height(m.contentSize.Height).Render(content) +} + +func (m *baseModel) renderLog() string { + return windowStyle.Width(m.contentSize.Width).Height(logLineCount).Render(m.logViewport.View()) +} + +func (m *baseModel) initialModel() { + m.logViewport = viewport.New(0, 0) + m.logViewport.KeyMap = viewport.KeyMap{} + m.setSize() + + m.ready = true +} + +func (m *baseModel) setSize() { + logHeight := logLineCount + docStyle.GetVerticalFrameSize() + menuHeight := 1 + + m.contentSize.Width = m.windowSize.Width - windowStyle.GetHorizontalFrameSize() - docStyle.GetHorizontalFrameSize() + m.contentSize.Height = m.windowSize.Height - windowStyle.GetVerticalFrameSize() - docStyle.GetVerticalFrameSize() - logHeight - menuHeight + + m.logViewport.Width = m.contentSize.Width + m.logViewport.Height = logLineCount +} diff --git a/cmd/terminal-client/tui/tabemdb.go b/cmd/terminal-client/tui/tabemdb.go new file mode 100644 index 0000000..3083b7c --- /dev/null +++ b/cmd/terminal-client/tui/tabemdb.go @@ -0,0 +1,76 @@ +package tui + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +type emdbTab struct { + ready bool + list list.Model + parent *baseModel + logger *Logger +} + +func NewEMDBTab(parent *baseModel, logger *Logger) tea.Model { + m := emdbTab{} + m.parent = parent + m.logger = logger + + return m +} + +func (m emdbTab) Init() tea.Cmd { + return nil +} + +func (m emdbTab) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + + switch msg := msg.(type) { + case TabSizeMsgType: + if !m.ready { + m.initialModel() + } + m.list.SetSize(msg.Width, msg.Height) + } + + m.list, cmd = m.list.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m emdbTab) View() string { + return m.list.View() +} + +func (m *emdbTab) initialModel() { + m.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) + m.list.Title = "Movies" + m.list.SetShowHelp(false) + m.refreshMovieList() + m.ready = true +} + +func (m *emdbTab) Log(s string) { + m.logger.Log(s) +} + +func (m *emdbTab) refreshMovieList() { + m.Log("fetch emdb movies...") + ems, err := m.parent.emdb.GetMovies() + if err != nil { + m.Log(err.Error()) + } + items := make([]list.Item, len(ems)) + for i, em := range ems { + items[i] = list.Item(Movie{m: em}) + } + m.list.SetItems(items) + m.Log(fmt.Sprintf("found %d movies in in emdb", len(items))) + +} diff --git a/cmd/terminal-client/tui/tabtmdb.go b/cmd/terminal-client/tui/tabtmdb.go new file mode 100644 index 0000000..37a525e --- /dev/null +++ b/cmd/terminal-client/tui/tabtmdb.go @@ -0,0 +1,24 @@ +package tui + +//focused string +//searchInput textinput.Model +//searchResults list.Model +//movieList list.Model + +//func (m *model) Search() { +// m.Log("start search") +// movies, err := m.tmdb.Search(m.searchInput.Value()) +// if err != nil { +// m.Log(fmt.Sprintf("error: %v", err)) +// return +// } +// +// m.Log(fmt.Sprintf("found %d results", len(movies))) +// items := []list.Item{} +// for _, res := range movies { +// items = append(items, Movie{m: res}) +// } +// +// m.searchResults.SetItems(items) +// m.focused = "result" +//} diff --git a/cmd/terminal-client/tui/tui.go b/cmd/terminal-client/tui/tui.go index 9482b84..0bcb9d1 100644 --- a/cmd/terminal-client/tui/tui.go +++ b/cmd/terminal-client/tui/tui.go @@ -1,12 +1,7 @@ package tui import ( - "fmt" - "strings" - "ewintr.nl/emdb/client" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" @@ -24,193 +19,45 @@ var ( logLineCount = 5 ) -func New(conf Config) (*tea.Program, error) { - tabs := []string{"Erik's movie database", "The movie database"} - tabContent := []string{"Emdb", "TMDB"} +type Logger struct { + p *tea.Program + Lines []string +} +func NewLogger() *Logger { + return &Logger{ + Lines: make([]string, 0), + } +} + +type LogMsg tea.Msg + +func (l *Logger) SetProgram(p *tea.Program) { + l.p = p +} + +func (l *Logger) Log(s string) { + l.Lines = append(l.Lines, s) +} + +type TabSizeMsgType tea.WindowSizeMsg + +func New(conf Config, logger *Logger) (*tea.Program, error) { + tabs := []string{"Erik's movie database", "The movie database"} tmdb, err := client.NewTMDB(conf.TMDBAPIKey) if err != nil { return nil, err } m := baseModel{ - config: conf, - emdb: client.NewEMDB(conf.EMDBBaseURL, conf.EMDBAPIKey), - tmdb: tmdb, - Tabs: tabs, - TabContent: tabContent, + config: conf, + emdb: client.NewEMDB(conf.EMDBBaseURL, conf.EMDBAPIKey), + tmdb: tmdb, + Tabs: tabs, + logger: logger, } - return tea.NewProgram(m, tea.WithAltScreen()), nil -} - -type baseModel struct { - config Config - emdb *client.EMDB - tmdb *client.TMDB - Tabs []string - TabContent []string - activeTab int - //focused string - //searchInput textinput.Model - //searchResults list.Model - movieList list.Model - logContent string - ready bool - logViewport viewport.Model - windowSize tea.WindowSizeMsg - contentSize tea.WindowSizeMsg - tabSize tea.WindowSizeMsg -} - -func (m baseModel) Init() tea.Cmd { - return nil -} - -func (m baseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - var cmds []tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "q", "esc": - return m, tea.Quit - case "right", "tab": - m.activeTab = min(m.activeTab+1, len(m.Tabs)-1) - return m, nil - case "left", "shift+tab": - m.activeTab = max(m.activeTab-1, 0) - return m, nil - } - case tea.WindowSizeMsg: - m.windowSize = msg - if !m.ready { - m.initialModel() - } - //m.Log(fmt.Sprintf("new window size: %dx%d", msg.Width, msg.Height)) - m.setSizes() - - } - - //switch m.focused { - //case "search": - // m.searchInput, cmd = m.searchInput.Update(msg) - //case "result": - // m.searchResults, cmd = m.searchResults.Update(msg) - //} - m.logViewport, cmd = m.logViewport.Update(msg) - cmds = append(cmds, cmd) - - return m, tea.Batch(cmds...) -} - -func (m *baseModel) Log(msg string) { - m.logContent = fmt.Sprintf("%s\n%s", m.logContent, msg) - m.logViewport.SetContent(m.logContent) - m.logViewport.GotoBottom() -} - -//func (m *model) Search() { -// m.Log("start search") -// movies, err := m.tmdb.Search(m.searchInput.Value()) -// if err != nil { -// m.Log(fmt.Sprintf("error: %v", err)) -// return -// } -// -// m.Log(fmt.Sprintf("found %d results", len(movies))) -// items := []list.Item{} -// for _, res := range movies { -// items = append(items, Movie{m: res}) -// } -// -// m.searchResults.SetItems(items) -// m.focused = "result" -//} - -func (m baseModel) View() string { - if !m.ready { - return "\n Initializing..." - } - - doc := strings.Builder{} - doc.WriteString(m.renderMenu()) - doc.WriteString("\n") - doc.WriteString(m.renderTabContent()) - doc.WriteString("\n") - doc.WriteString(m.renderLog()) - return docStyle.Render(doc.String()) -} - -func (m *baseModel) renderMenu() string { - var items []string - for i, t := range m.Tabs { - if i == m.activeTab { - items = append(items, lipgloss.NewStyle(). - Foreground(colorHighLightForeGround). - Render(fmt.Sprintf(" * %s ", t))) - continue - } - - items = append(items, lipgloss.NewStyle(). - Foreground(colorNormalForeground). - Render(fmt.Sprintf(" %s ", t))) - } - - return lipgloss.PlaceHorizontal(m.contentSize.Width, lipgloss.Left, lipgloss.JoinHorizontal(lipgloss.Top, items...)) -} - -func (m *baseModel) renderTabContent() string { - content := m.TabContent[m.activeTab] - switch m.activeTab { - case 0: - content = m.movieList.View() - case 1: - content = "tmdb" - } - - return windowStyle.Width(m.contentSize.Width).Height(m.contentSize.Height).Render(content) -} - -func (m *baseModel) renderLog() string { - return windowStyle.Width(m.contentSize.Width).Height(logLineCount).Render(m.logViewport.View()) -} - -func (m *baseModel) initialModel() { - m.movieList = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) - m.movieList.Title = "Movies" - m.movieList.SetShowHelp(false) - - m.logViewport = viewport.New(0, 0) - m.logViewport.KeyMap = viewport.KeyMap{} - - m.setSizes() - m.refreshMovieList() - - m.ready = true -} - -func (m *baseModel) setSizes() { - logHeight := logLineCount + docStyle.GetVerticalFrameSize() - menuHeight := 1 - - m.contentSize.Width = m.windowSize.Width - windowStyle.GetHorizontalFrameSize() - docStyle.GetHorizontalFrameSize() - m.contentSize.Height = m.windowSize.Height - windowStyle.GetVerticalFrameSize() - docStyle.GetVerticalFrameSize() - logHeight - menuHeight - - m.movieList.SetSize(m.contentSize.Width, m.contentSize.Height) - m.logViewport.Width = m.contentSize.Width - m.logViewport.Height = logLineCount -} - -func (m *baseModel) refreshMovieList() { - m.Log("fetch emdb movies...") - ems, err := m.emdb.GetMovies() - if err != nil { - m.Log(err.Error()) - } - items := make([]list.Item, len(ems)) - for i, em := range ems { - items[i] = list.Item(Movie{m: em}) - } - m.movieList.SetItems(items) - m.Log(fmt.Sprintf("found %d movies in in emdb", len(items))) + m.TabContent = NewEMDBTab(&m, m.logger) + + p := tea.NewProgram(m, tea.WithAltScreen()) + + return p, nil }