feat: adding help button

This commit is contained in:
2025-06-01 13:33:47 +01:00
parent ed391bee8e
commit d73d5dcc5b
2 changed files with 165 additions and 19 deletions

165
main.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"errors" "errors"
"flag"
"net" "net"
"os" "os"
"os/signal" "os/signal"
@ -11,6 +12,8 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@ -51,10 +54,63 @@ func getPostWithoutMetaData(post string) (string, error) {
return strings.Join(splitPost[index+2:], "\n"), nil 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 type Page int
const ( const (
FRONT Page = iota FRONT Page = iota
POST_LIST
POST POST
) )
@ -62,10 +118,15 @@ type model struct {
list list.Model list list.Model
allPosts []string allPosts []string
keys keyMap
help help.Model
width int width int
height int height int
postModel postModel frontPageContent string
frontPageModel postModel
postModel postModel
page Page page Page
@ -75,6 +136,16 @@ type model struct {
err error 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 { func initialModel() model {
posts, err := getAllPosts("/home/johnc/Code/JohnTech/content/blog") posts, err := getAllPosts("/home/johnc/Code/JohnTech/content/blog")
if err != nil { if err != nil {
@ -99,11 +170,8 @@ func initialModel() model {
panic(err) panic(err)
} }
vp := viewport.New(w, h) // -1 to allow for the initial height of the help model
vp.Style = lipgloss.NewStyle(). vp := getViewPort(w, h-1)
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("62")).
PaddingRight(2)
// We need to adjust the width of the glamour render from our main width // We need to adjust the width of the glamour render from our main width
// to account for a few things: // to account for a few things:
@ -124,7 +192,22 @@ func initialModel() model {
panic(err) 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 { 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) { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.page { switch m.page {
case FRONT: 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) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
@ -141,8 +262,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.page = POST m.page = POST
selectedItem := m.list.SelectedItem().(item) 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 { if err != nil {
panic(err) panic(err)
} }
@ -166,7 +291,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
updatedPostModel, cmd := m.postModel.Update(msg) updatedPostModel, cmd := m.postModel.Update(msg)
if cmd != nil { if cmd != nil {
if _, ok := cmd().(childMessage); ok { 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. // TODO: better type safety would be to do this over the type.
switch m.page { switch m.page {
case FRONT: case FRONT:
helpView := m.help.View(m.keys)
return m.frontPageModel.View() + "\n" + helpView
case POST_LIST:
return docStyle.Render(m.list.View()) return docStyle.Render(m.list.View())
case POST: case POST:
return m.postModel.View() return m.postModel.View()
@ -195,7 +324,7 @@ const (
port = "23234" port = "23234"
) )
func main() { func initServer() {
s, err := wish.NewServer( s, err := wish.NewServer(
wish.WithAddress(net.JoinHostPort(host, port)), wish.WithAddress(net.JoinHostPort(host, port)),
wish.WithHostKeyPath(".ssh/id_rsa"), 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 // 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 // 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 // pass it to the new model. You can also return tea.ProgramOptions (such as

19
post.go
View File

@ -42,31 +42,32 @@ func (m postModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
func createPostModel(post string, termRender *glamour.TermRenderer, viewPort viewport.Model) (postModel, error) { func processMarkdown(markdown string) (string, error) {
postContent, err := getPostWithoutMetaData(post) postContent, err := getPostWithoutMetaData(markdown)
if err != nil { if err != nil {
return postModel{}, err return "", err
} }
postInfo, err := getPostInfo(post) postInfo, err := getPostInfo(markdown)
if err != nil { if err != nil {
return postModel{}, err return "", err
} }
// This is stupid // This is stupid
lines := strings.Split(postContent, "\n") lines := strings.Split(postContent, "\n")
lines = append([]string{"# " + postInfo.Title[1:len(postInfo.Title)-1]}, lines...) 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{ m := postModel{
viewport: viewPort, viewport: viewPort,
} }
out, err := termRender.Render(post) out, err := termRender.Render(markdown)
if err != nil { if err != nil {
// TODO: fix this shit return postModel{}, err
panic(err)
} }
m.viewport.SetContent(out) m.viewport.SetContent(out)