diff --git a/main.go b/main.go index 24267cc..3c77f67 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "flag" "net" "os" "os/signal" @@ -11,6 +12,8 @@ import ( "syscall" "time" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -51,10 +54,63 @@ func getPostWithoutMetaData(post string) (string, error) { return strings.Join(splitPost[index+2:], "\n"), nil } +type keyMap struct { + Up key.Binding + Down key.Binding + Left key.Binding + Right key.Binding + Blog key.Binding + Help key.Binding + Quit key.Binding +} + +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Help, k.Quit} +} + +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Up, k.Down, k.Left, k.Right}, + {k.Blog, k.Help, k.Quit}, + } +} + +var keys = keyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "move up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "move down"), + ), + Left: key.NewBinding( + key.WithKeys("left", "h"), + key.WithHelp("←/h", "move left"), + ), + Right: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("→/l", "move right"), + ), + Blog: key.NewBinding( + key.WithKeys("b"), + key.WithHelp("b", "go to blog"), + ), + Help: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "toggle help"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "esc", "ctrl+c"), + key.WithHelp("q", "quit"), + ), +} + type Page int const ( FRONT Page = iota + POST_LIST POST ) @@ -62,10 +118,15 @@ type model struct { list list.Model allPosts []string + keys keyMap + help help.Model + width int height int - postModel postModel + frontPageContent string + frontPageModel postModel + postModel postModel page Page @@ -75,6 +136,16 @@ type model struct { err error } +func getViewPort(width int, height int) viewport.Model { + vp := viewport.New(width, height) + vp.Style = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")). + PaddingRight(2) + + return vp +} + func initialModel() model { posts, err := getAllPosts("/home/johnc/Code/JohnTech/content/blog") if err != nil { @@ -99,11 +170,8 @@ func initialModel() model { panic(err) } - vp := viewport.New(w, h) - vp.Style = lipgloss.NewStyle(). - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("62")). - PaddingRight(2) + // -1 to allow for the initial height of the help model + vp := getViewPort(w, h-1) // We need to adjust the width of the glamour render from our main width // to account for a few things: @@ -124,7 +192,22 @@ func initialModel() model { panic(err) } - return model{list: list, allPosts: posts, page: FRONT, termRenderer: renderer, viewport: vp} + frontPageContent, err := os.ReadFile("/home/johnc/Code/JohnTech/content/_index.md") + if err != nil { + panic(err) + } + + processedFrontPage, err := processMarkdown(string(frontPageContent)) + if err != nil { + panic(err) + } + + frontPageModel, err := createMarkdownPostModel(processedFrontPage, renderer, vp) + if err != nil { + panic(err) + } + + return model{list: list, allPosts: posts, page: FRONT, termRenderer: renderer, viewport: vp, frontPageModel: frontPageModel, frontPageContent: processedFrontPage, help: help.New(), keys: keys, width: w, height: h} } func (m model) Init() tea.Cmd { @@ -134,6 +217,44 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.page { case FRONT: + updatedFrontPageModel, cmd := m.frontPageModel.Update(msg) + m.frontPageModel = updatedFrontPageModel.(postModel) + + if cmd != nil { + if _, ok := cmd().(childMessage); ok { + return m, tea.Quit + } + } + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keys.Help): + m.help.ShowAll = !m.help.ShowAll + + adjustedViewportHeight := 0 + if m.help.ShowAll { + adjustedViewportHeight = 4 + } else { + adjustedViewportHeight = 1 + } + + viewportYOffset := m.frontPageModel.viewport.YOffset + m.viewport = getViewPort(m.width, m.height-adjustedViewportHeight) + m.viewport.SetYOffset(viewportYOffset) + + frontPageModel, err := createMarkdownPostModel(m.frontPageContent, m.termRenderer, m.viewport) + if err != nil { + panic(err) + } + + m.frontPageModel = frontPageModel + case key.Matches(msg, m.keys.Blog): + m.page = POST_LIST + } + } + + return m, cmd + case POST_LIST: switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -141,8 +262,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.page = POST selectedItem := m.list.SelectedItem().(item) - postModel, err := createPostModel(m.allPosts[selectedItem.index], m.termRenderer, m.viewport) + processedPost, err := processMarkdown(m.allPosts[selectedItem.index]) + if err != nil { + panic(err) + } + postModel, err := createMarkdownPostModel(processedPost, m.termRenderer, m.viewport) if err != nil { panic(err) } @@ -166,7 +291,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { updatedPostModel, cmd := m.postModel.Update(msg) if cmd != nil { if _, ok := cmd().(childMessage); ok { - m.page = FRONT + m.page = POST_LIST } } @@ -182,6 +307,10 @@ func (m model) View() string { // TODO: better type safety would be to do this over the type. switch m.page { case FRONT: + helpView := m.help.View(m.keys) + + return m.frontPageModel.View() + "\n" + helpView + case POST_LIST: return docStyle.Render(m.list.View()) case POST: return m.postModel.View() @@ -195,7 +324,7 @@ const ( port = "23234" ) -func main() { +func initServer() { s, err := wish.NewServer( wish.WithAddress(net.JoinHostPort(host, port)), wish.WithHostKeyPath(".ssh/id_rsa"), @@ -228,6 +357,22 @@ func main() { } } +func main() { + var isSsh bool + flag.BoolVar(&isSsh, "ssh", false, "Start an SSH server instead of the TUI application.") + + flag.Parse() + + if isSsh { + initServer() + } else { + p := tea.NewProgram(initialModel()) + if _, err := p.Run(); err != nil { + log.Fatal(err) + } + } +} + // You can wire any Bubble Tea model up to the middleware with a function that // handles the incoming ssh.Session. Here we just grab the terminal info and // pass it to the new model. You can also return tea.ProgramOptions (such as diff --git a/post.go b/post.go index 2e7d5c8..7f18aed 100644 --- a/post.go +++ b/post.go @@ -42,31 +42,32 @@ func (m postModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func createPostModel(post string, termRender *glamour.TermRenderer, viewPort viewport.Model) (postModel, error) { - postContent, err := getPostWithoutMetaData(post) +func processMarkdown(markdown string) (string, error) { + postContent, err := getPostWithoutMetaData(markdown) if err != nil { - return postModel{}, err + return "", err } - postInfo, err := getPostInfo(post) + postInfo, err := getPostInfo(markdown) if err != nil { - return postModel{}, err + return "", err } // This is stupid lines := strings.Split(postContent, "\n") lines = append([]string{"# " + postInfo.Title[1:len(postInfo.Title)-1]}, lines...) - post = strings.Join(lines, "\n") + return strings.Join(lines, "\n"), nil +} +func createMarkdownPostModel(markdown string, termRender *glamour.TermRenderer, viewPort viewport.Model) (postModel, error) { m := postModel{ viewport: viewPort, } - out, err := termRender.Render(post) + out, err := termRender.Render(markdown) if err != nil { - // TODO: fix this shit - panic(err) + return postModel{}, err } m.viewport.SetContent(out)