Files
slides/internal/model/model.go
2021-06-27 21:02:01 -04:00

194 lines
4.0 KiB
Go

package model
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"time"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/maaslalani/slides/internal/code"
"github.com/maaslalani/slides/internal/meta"
"github.com/maaslalani/slides/internal/process"
"github.com/maaslalani/slides/styles"
)
const (
delimiter = "\n---\n"
)
type Model struct {
Slides []string
Page int
Author string
Date string
Theme glamour.TermRendererOption
FileName string
viewport viewport.Model
// VirtualText is used for additional information that is not part of the
// original slides, it will be displayed on a slide and reset on page change
VirtualText string
}
type fileWatchMsg struct{}
var fileInfo os.FileInfo
func (m Model) Init() tea.Cmd {
if m.FileName == "" {
return nil
}
fileInfo, _ = os.Stat(m.FileName)
return fileWatchCmd()
}
func fileWatchCmd() tea.Cmd {
return tea.Every(time.Second, func(t time.Time) tea.Msg {
return fileWatchMsg{}
})
}
func (m *Model) Load() error {
var content string
var err error
if m.FileName != "" {
content, err = readFile(m.FileName)
} else {
content, err = readStdin()
}
if err != nil {
return err
}
content = process.Pre(content)
slides := strings.Split(content, delimiter)
metaData, exists := meta.New().ParseHeader(slides[0])
// If the user specifies a custom configuration options
// skip the first "slide" since this is all configuration
if exists {
slides = slides[1:]
}
m.Slides = slides
if m.Theme == nil {
m.Theme = styles.SelectTheme(metaData.Theme)
}
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case " ", "down", "j", "right", "l", "enter", "n":
if m.Page < len(m.Slides)-1 {
m.Page++
}
m.VirtualText = ""
case "up", "k", "left", "h", "p":
if m.Page > 0 {
m.Page--
}
m.VirtualText = ""
case "ctrl+e":
// Run code block
block, err := code.Parse(m.Slides[m.Page])
if err != nil {
// We couldn't parse the code block on the screen
m.VirtualText = "\n" + err.Error()
return m, nil
}
res := code.Execute(block)
m.VirtualText = res.Out
}
case fileWatchMsg:
newFileInfo, err := os.Stat(m.FileName)
if err == nil && newFileInfo.ModTime() != fileInfo.ModTime() {
fileInfo = newFileInfo
_ = m.Load()
if m.Page >= len(m.Slides) {
m.Page = len(m.Slides) - 1
}
}
return m, fileWatchCmd()
}
return m, nil
}
func (m Model) View() string {
r, _ := glamour.NewTermRenderer(m.Theme, glamour.WithWordWrap(0))
slide := m.Slides[m.Page]
slide, err := r.Render(slide)
slide += m.VirtualText
if err != nil {
slide = fmt.Sprintf("Error: Could not render markdown! (%v)", err)
}
slide = styles.Slide.Render(slide)
left := styles.Author.Render(m.Author) + styles.Date.Render(m.Date)
right := styles.Page.Render(fmt.Sprintf("Slide %d / %d", m.Page+1, len(m.Slides)))
status := styles.Status.Render(styles.JoinHorizontal(left, right, m.viewport.Width))
return styles.JoinVertical(slide, status, m.viewport.Height)
}
func readFile(path string) (string, error) {
s, err := os.Stat(path)
if err != nil {
return "", errors.New("could not read file")
}
if s.IsDir() {
return "", errors.New("can not read directory")
}
b, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
return string(b), err
}
func readStdin() (string, error) {
stat, err := os.Stdin.Stat()
if err != nil {
return "", err
}
if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 {
return "", errors.New("no slides provided")
}
reader := bufio.NewReader(os.Stdin)
var b strings.Builder
for {
r, _, err := reader.ReadRune()
if err != nil && err == io.EOF {
break
}
_, err = b.WriteRune(r)
if err != nil {
return "", err
}
}
return b.String(), nil
}