diff --git a/examples/code_blocks.md b/examples/code_blocks.md index cb286b2f..e782bf16 100644 --- a/examples/code_blocks.md +++ b/examples/code_blocks.md @@ -65,6 +65,10 @@ fmt.Println("Hello, world!") console.log("Hello, world!") ``` +```javascript +console.log("Hello, universe!") +``` + --- ### Lua diff --git a/internal/code/languages.go b/internal/code/languages.go index 31a0c30e..2cf60997 100644 --- a/internal/code/languages.go +++ b/internal/code/languages.go @@ -25,7 +25,7 @@ const ( Python = "python" Ruby = "ruby" Rust = "rust" - Java = "java" + Java = "java" ) // Languages is a map of supported languages with their extensions and commands diff --git a/internal/code/selection.go b/internal/code/selection.go new file mode 100644 index 00000000..e85e2534 --- /dev/null +++ b/internal/code/selection.go @@ -0,0 +1 @@ +package code diff --git a/internal/model/model.go b/internal/model/model.go index 33e25127..31be3214 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -7,6 +7,8 @@ import ( "fmt" "io" "os" + "regexp" + "strconv" "strings" "time" @@ -48,6 +50,8 @@ type Model struct { // original slides, it will be displayed on a slide and reset on page change VirtualText string Search navigation.Search + + blocks *navigation.Blocks } type fileWatchMsg struct{} @@ -102,6 +106,7 @@ func (m *Model) Load() error { if m.Theme == nil { m.Theme = styles.SelectTheme(metaData.Theme) } + m.blocks = nil return nil } @@ -140,6 +145,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } + if m.blocks != nil { + return m.runCodeBlocks(msg) + } + switch keyPress { case "/": // Begin search @@ -154,15 +163,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { blocks, 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() + m.SetVirtualText("\n" + err.Error()) return m, nil } - var outs []string - for _, block := range blocks { - res := code.Execute(block) - outs = append(outs, res.Out) - } - m.VirtualText = strings.Join(outs, "\n") + m.blocks = navigation.NewBlocks(blocks) + return m, nil case "y": blocks, err := code.Parse(m.Slides[m.Page]) if err != nil { @@ -236,6 +241,52 @@ func (m *Model) paging() string { } } +func (m *Model) runCodeBlocks(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.Type { + case tea.KeyEnter: + m.blocks.ExecuteNext(m) + if m.blocks.Done() { + m.blocks = nil + } + return m, nil + case tea.KeyCtrlC, tea.KeyEscape: + m.blocks = nil + return m, nil + } + + keyPress := msg.String() + switch keyPress { + case "a": + m.blocks.ExecuteAll(m) + m.blocks = nil + return m, nil + case "n": + m.blocks.ExecuteNext(m) + if m.blocks.Done() { + m.blocks = nil + } + return m, nil + } + + digits := regexp.MustCompile(`\d+`) + match := digits.FindString(keyPress) + if match == "" { + return m, nil + } + + idx, err := strconv.Atoi(match) + if err != nil { + m.SetVirtualText("\n" + err.Error()) + return m, nil + } + + m.blocks.ExecuteIdx(uint(idx), m) + if m.blocks.Done() { + m.blocks = nil + } + return m, nil +} + func readFile(path string) (string, error) { s, err := os.Stat(path) if err != nil { @@ -310,3 +361,8 @@ func (m *Model) SetPage(page int) { func (m *Model) Pages() []string { return m.Slides } + +// SetVirtualText sets the virtual text for the presentation. +func (m *Model) SetVirtualText(text string) { + m.VirtualText = text +} diff --git a/internal/navigation/blocks.go b/internal/navigation/blocks.go new file mode 100644 index 00000000..3af06ff4 --- /dev/null +++ b/internal/navigation/blocks.go @@ -0,0 +1,60 @@ +package navigation + +import ( + "github.com/maaslalani/slides/internal/code" + "strconv" +) + +type Blocks struct { + blocks []code.Block + nextIdx uint + done bool +} + +func NewBlocks(blocks []code.Block) *Blocks { + b := new(Blocks) + b.blocks = blocks + b.nextIdx = 0 + b.checkDone() + + return b +} + +func (b *Blocks) ExecuteAll(m Model) { + for _, block := range b.blocks { + res := code.Execute(block) + m.SetVirtualText(res.Out) + } + b.nextIdx = uint(len(b.blocks)) + b.checkDone() +} + +func (b *Blocks) ExecuteNext(m Model) { + if b.done { + return + } + + res := code.Execute(b.blocks[b.nextIdx]) + m.SetVirtualText(res.Out) + + b.nextIdx++ + b.checkDone() +} + +func (b *Blocks) ExecuteIdx(idx uint, m Model) { + if idx >= uint(len(b.blocks)) { + m.SetVirtualText("no code block with index [" + strconv.Itoa(int(idx)) + "]") + return + } + + b.nextIdx = idx + b.ExecuteNext(m) +} + +func (b *Blocks) Done() bool { + return b.done +} + +func (b *Blocks) checkDone() { + b.done = b.nextIdx >= uint(len(b.blocks)) +} diff --git a/internal/navigation/blocks_test.go b/internal/navigation/blocks_test.go new file mode 100644 index 00000000..aa0fe364 --- /dev/null +++ b/internal/navigation/blocks_test.go @@ -0,0 +1,88 @@ +package navigation + +import ( + "github.com/maaslalani/slides/internal/code" + "github.com/stretchr/testify/require" + "testing" +) + +func TestBlocks_ExecuteAll(t *testing.T) { + b := []code.Block{ + {}, + {}, + {}, + } + m := mockModel{} + + blocks := NewBlocks(b) + require.False(t, blocks.Done()) + + blocks.ExecuteAll(&m) + require.True(t, blocks.Done()) +} + +func TestBlocks_ExecuteNext(t *testing.T) { + b := []code.Block{ + { + Language: "javascript", + Code: `console.log("first block")`, + }, + { + Language: "javascript", + Code: `console.log("second block")`, + }, + { + Language: "javascript", + Code: `console.log("third block")`, + }, + } + m := mockModel{} + + blocks := NewBlocks(b) + require.False(t, blocks.Done()) + + blocks.ExecuteNext(&m) + require.Equal(t, "first block\n", m.virtualText) + require.False(t, blocks.Done()) + + blocks.ExecuteNext(&m) + require.Equal(t, "second block\n", m.virtualText) + require.False(t, blocks.Done()) + + blocks.ExecuteNext(&m) + require.Equal(t, "third block\n", m.virtualText) + require.True(t, blocks.Done()) +} + +func TestBlocks_ExecuteIdx(t *testing.T) { + b := []code.Block{ + { + Language: "javascript", + Code: `console.log("first block")`, + }, + { + Language: "javascript", + Code: `console.log("second block")`, + }, + { + Language: "javascript", + Code: `console.log("third block")`, + }, + } + m := mockModel{} + + blocks := NewBlocks(b) + require.False(t, blocks.Done()) + + blocks.ExecuteIdx(1, &m) + require.Equal(t, "second block\n", m.virtualText) + require.False(t, blocks.Done()) + + blocks.ExecuteIdx(0, &m) + require.Equal(t, "first block\n", m.virtualText) + require.False(t, blocks.Done()) + + blocks.ExecuteIdx(2, &m) + require.Equal(t, "third block\n", m.virtualText) + require.True(t, blocks.Done()) +} diff --git a/internal/navigation/model.go b/internal/navigation/model.go new file mode 100644 index 00000000..b57587f6 --- /dev/null +++ b/internal/navigation/model.go @@ -0,0 +1,31 @@ +package navigation + +// Model is an interface for models.model, so that cycle imports are avoided +type Model interface { + CurrentPage() int + SetPage(page int) + Pages() []string + SetVirtualText(string) +} + +type mockModel struct { + slides []string + page int + virtualText string +} + +func (m *mockModel) CurrentPage() int { + return m.page +} + +func (m *mockModel) SetPage(page int) { + m.page = page +} + +func (m *mockModel) Pages() []string { + return m.slides +} + +func (m *mockModel) SetVirtualText(text string) { + m.virtualText = text +} diff --git a/internal/navigation/search.go b/internal/navigation/search.go index b18e831d..a3ea0d8a 100644 --- a/internal/navigation/search.go +++ b/internal/navigation/search.go @@ -8,13 +8,6 @@ import ( "github.com/maaslalani/slides/styles" ) -// Model is an interface for models.model, so that cycle imports are avoided -type Model interface { - CurrentPage() int - SetPage(page int) - Pages() []string -} - // Search represents the current search type Search struct { // Active - Show search bar instead of author and date? diff --git a/internal/navigation/search_test.go b/internal/navigation/search_test.go index d3f090e7..381aa34f 100644 --- a/internal/navigation/search_test.go +++ b/internal/navigation/search_test.go @@ -4,23 +4,6 @@ import ( "testing" ) -type mockModel struct { - slides []string - page int -} - -func (m *mockModel) CurrentPage() int { - return m.page -} - -func (m *mockModel) SetPage(page int) { - m.page = page -} - -func (m *mockModel) Pages() []string { - return m.slides -} - func TestSearch(t *testing.T) { data := []string{ "hi",