diff --git a/bubbletea-example/main.go b/bubbletea-example/main.go new file mode 100644 index 0000000..4fbab38 --- /dev/null +++ b/bubbletea-example/main.go @@ -0,0 +1,204 @@ +package bubbletea_example + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const ( + initialInputs = 2 + maxInputs = 6 + minInputs = 1 + helpHeight = 5 +) + +var ( + cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) + + cursorLineStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("57")). + Foreground(lipgloss.Color("230")) + + placeholderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("238")) + + endOfBufferStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("235")) + + focusedPlaceholderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("99")) + + focusedBorderStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("238")) + + blurredBorderStyle = lipgloss.NewStyle(). + Border(lipgloss.HiddenBorder()) +) + +type keymap = struct { + next, prev, add, remove, quit key.Binding +} + +func newTextarea() textarea.Model { + t := textarea.New() + t.Prompt = "" + t.Placeholder = "Type something" + t.ShowLineNumbers = true + t.Cursor.Style = cursorStyle + t.FocusedStyle.Placeholder = focusedPlaceholderStyle + t.BlurredStyle.Placeholder = placeholderStyle + t.FocusedStyle.CursorLine = cursorLineStyle + t.FocusedStyle.Base = focusedBorderStyle + t.BlurredStyle.Base = blurredBorderStyle + t.FocusedStyle.EndOfBuffer = endOfBufferStyle + t.BlurredStyle.EndOfBuffer = endOfBufferStyle + t.KeyMap.DeleteWordBackward.SetEnabled(false) + t.KeyMap.LineNext = key.NewBinding(key.WithKeys("down")) + t.KeyMap.LinePrevious = key.NewBinding(key.WithKeys("up")) + t.Blur() + return t +} + +type model struct { + width int + height int + keymap keymap + help help.Model + inputs []textarea.Model + focus int +} + +func newModel() model { + m := model{ + inputs: make([]textarea.Model, initialInputs), + help: help.New(), + keymap: keymap{ + next: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "next"), + ), + prev: key.NewBinding( + key.WithKeys("shift+tab"), + key.WithHelp("shift+tab", "prev"), + ), + add: key.NewBinding( + key.WithKeys("ctrl+n"), + key.WithHelp("ctrl+n", "add an editor"), + ), + remove: key.NewBinding( + key.WithKeys("ctrl+w"), + key.WithHelp("ctrl+w", "remove an editor"), + ), + quit: key.NewBinding( + key.WithKeys("esc", "ctrl+c"), + key.WithHelp("esc", "quit"), + ), + }, + } + for i := 0; i < initialInputs; i++ { + m.inputs[i] = newTextarea() + } + m.inputs[m.focus].Focus() + m.updateKeybindings() + return m +} + +func (m model) Init() tea.Cmd { + return textarea.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keymap.quit): + for i := range m.inputs { + m.inputs[i].Blur() + } + return m, tea.Quit + case key.Matches(msg, m.keymap.next): + m.inputs[m.focus].Blur() + m.focus++ + if m.focus > len(m.inputs)-1 { + m.focus = 0 + } + cmd := m.inputs[m.focus].Focus() + cmds = append(cmds, cmd) + case key.Matches(msg, m.keymap.prev): + m.inputs[m.focus].Blur() + m.focus-- + if m.focus < 0 { + m.focus = len(m.inputs) - 1 + } + cmd := m.inputs[m.focus].Focus() + cmds = append(cmds, cmd) + case key.Matches(msg, m.keymap.add): + m.inputs = append(m.inputs, newTextarea()) + case key.Matches(msg, m.keymap.remove): + m.inputs = m.inputs[:len(m.inputs)-1] + if m.focus > len(m.inputs)-1 { + m.focus = len(m.inputs) - 1 + } + } + case tea.WindowSizeMsg: + m.height = msg.Height + m.width = msg.Width + } + + m.updateKeybindings() + m.sizeInputs() + + // Update all textareas + for i := range m.inputs { + newModel, cmd := m.inputs[i].Update(msg) + m.inputs[i] = newModel + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m *model) sizeInputs() { + for i := range m.inputs { + m.inputs[i].SetWidth(m.width / len(m.inputs)) + m.inputs[i].SetHeight(m.height - helpHeight) + } +} + +func (m *model) updateKeybindings() { + m.keymap.add.SetEnabled(len(m.inputs) < maxInputs) + m.keymap.remove.SetEnabled(len(m.inputs) > minInputs) +} + +func (m model) View() string { + help := m.help.ShortHelpView([]key.Binding{ + m.keymap.next, + m.keymap.prev, + m.keymap.add, + m.keymap.remove, + m.keymap.quit, + }) + + var views []string + for i := range m.inputs { + views = append(views, m.inputs[i].View()) + } + + return lipgloss.JoinHorizontal(lipgloss.Top, views...) + "\n\n" + help +} + +func main() { + if _, err := tea.NewProgram(newModel(), tea.WithAltScreen()).Run(); err != nil { + fmt.Println("Error while running program:", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index 1dc8a1c..7dac50b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,28 @@ module ewintr.nl/narratio go 1.21 + +require ( + github.com/charmbracelet/bubbles v0.17.1 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..64c64d1 --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4= +github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/main.go b/main.go index 4815a74..9384e7e 100644 --- a/main.go +++ b/main.go @@ -2,43 +2,28 @@ package main import ( "fmt" - "log" "os" - "os/exec" "path/filepath" - "time" ) -type Scene struct { +type File struct { Path string } func main() { - - // Start Neovim with a server name - cmd := exec.Command("nvim", "--servername", "NVIM_SERVER", tempFile.Name()) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Start() + scenes, err := getScenes() if err != nil { - log.Fatal(err) + fmt.Println(err) + os.Exit(1) } - // Wait for Neovim to start - time.Sleep(time.Second) - - // Send a command to the running Neovim instance - cmd = exec.Command("nvim", "--servername", "NVIM_SERVER", "--remote-send", ":edit /path/to/new/file") - err = cmd.Run() - if err != nil { - log.Fatal(err) + for _, scene := range scenes { + fmt.Println(scene.Path) } - } -func getScenes() ([]Scene, error) { - scenes := make([]Scene, 0) +func getScenes() ([]File, error) { + scenes := make([]File, 0) root := "content" err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -46,7 +31,7 @@ func getScenes() ([]Scene, error) { } if !info.IsDir() { if filepath.Ext(path) == ".md" { - scenes = append(scenes, Scene{Path: path}) + scenes = append(scenes, File{Path: path}) } } return nil diff --git a/tui/tui.go b/tui/tui.go new file mode 100644 index 0000000..78e95c9 --- /dev/null +++ b/tui/tui.go @@ -0,0 +1,4 @@ +package tui + +type model struct { +}