ronantakizawa/codereview-bench huggingface dataset:
Repository: localsend/localsend
File: app/test/unit/util/file_path_helper_test.dart (Dart)

Code before:
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:test/test.dart';

void main() {
  test('fileName with counter', () async {
    final fileName = 'myFile';

    final fileNameWithCounter1 = 'myFile (1)';
    final fileNameWithCounter2 = 'myFile (2)';

    final fileNameAddCounter1 = fileName.withCount(1);
    final fileNameAddCounter2 = fileNameWithCounter1.withCount(2);

    expect(fileNameWithCounter1, fileNameAddCounter1);
    expect(fileNameWithCounter2, fileNameAddCounter2);
  });
}


Review comment (suggestion):
I think these 4 variables can be inlined:
`expect('myFile'.withCount(1), 'myFile (1)');`

Also add a test that parse an existing count:
`expect('myFile (1)'.withCount(2), 'myFile (2)');`

Code after:
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:test/test.dart';

void main() {
  test('fileName with counter', () async {
    expect('myFile'.withCount(1), 'myFile (1)');
    expect('myFile (1)'.withCount(2), 'myFile (2)');
  });
}
Repository: maaslalani/slides
File: internal/model/model.go (Go)

Code before:
		m.viewport.Height = msg.Height
		return m, nil

	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		case " ", "down", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--
			}
		}

	case fileWatchMsg:
		newFileInfo, err := os.Stat(m.FileName)
		if err == nil {
			if 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, err := r.Render(m.Slides[m.Page])
	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")

Review comment (suggestion):
Can you fix the code to have a more flat structure, something like this?

```suggestion
		if err != nil {
			return m, fileWatchCmd()
		}
		// File unchanged, skip reload
	        if newFileInfo.ModTime() == fileInfo.ModTime() {
			return m, fileWatchCmd()
	        }
		fileInfo = newFileInfo
		_ = m.Load()
		if m.Page >= len(m.Slides) {
			m.Page = len(m.Slides) - 1
		}
```

Code after:
		m.viewport.Height = msg.Height
		return m, nil

	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		case " ", "down", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--
			}
		}

	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, err := r.Render(m.Slides[m.Page])
	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() {
Repository: maaslalani/slides
File: internal/model/model.go (Go)

Code before:

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", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--
			}
		}

	case fileWatchMsg:
		newFileInfo, err := os.Stat(m.FileName)
		if err == nil {
			if 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, err := r.Render(m.Slides[m.Page])
	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)
}

Review comment (suggestion):
We are pretty much implementing our own polling method every one second, is there any way to use something like  https://github.com/fsnotify/fsnotify so we don't have to poll and we get instant updates.

Code after:

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", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--
			}
		}

	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, err := r.Render(m.Slides[m.Page])
	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) {
Repository: maaslalani/slides
File: cmd/root.go (Go)

Code before:
		}

		if err != nil {
			return err
		}

		content = strings.ReplaceAll(content, altDelimiter, delimiter)
		slides := strings.Split(content, delimiter)

		user, err := user.Current()
		if err != nil {
			return errors.New("could not get current user")
		}

		m, metaExists := meta.New().ParseHeader(slides[0])
		if metaExists {
			slides = slides[1:]
		}

		p := tea.NewProgram(model.Model{
			Slides:      slides,
			Page:        0,
			Author:      user.Name,
			Date:        time.Now().Format("2006-01-02"),
			Theme:       styles.SelectTheme(m.Theme),
			CustomTheme: styles.CustomTheme(m.Theme),
		}, tea.WithAltScreen())

		err = p.Start()
		return err
	},
}

func Execute() {
	err := root.Execute()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

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 {

Review comment (suggestion):
Is there a clever way to combine these two. My thinking was that we would put a `Renderer:     glamour.Renderer` on the model and handle the theme stuff before that.

Code after:
		}

		if err != nil {
			return err
		}

		content = strings.ReplaceAll(content, altDelimiter, delimiter)
		slides := strings.Split(content, delimiter)

		user, err := user.Current()
		if err != nil {
			return errors.New("could not get current user")
		}

		m, 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:]
		}

		p := tea.NewProgram(model.Model{
			Slides: slides,
			Page:   0,
			Author: user.Name,
			Date:   time.Now().Format("2006-01-02"),
			Theme:  styles.SelectTheme(m.Theme),
		}, tea.WithAltScreen())

		err = p.Start()
		return err
	},
}

func Execute() {
	err := root.Execute()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

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)
Repository: maaslalani/slides
File: cmd/root.go (Go)

Code before:
	Short: "Slides is a terminal based presentation tool",
	Args:  cobra.MaximumNArgs(1),
	RunE: func(_ *cobra.Command, args []string) error {
		var content string
		var err error

		if len(args) > 0 {
			content, err = readFile(args[0])
		} else {
			content, err = readStdin()
		}

		if err != nil {
			return err
		}

		content = strings.ReplaceAll(content, altDelimiter, delimiter)
		slides := strings.Split(content, delimiter)

		user, err := user.Current()
		if err != nil {
			return errors.New("could not get current user")
		}

		m, metaExists := meta.New().ParseHeader(slides[0])
		if metaExists {
			slides = slides[1:]
		}

		p := tea.NewProgram(model.Model{
			Slides:      slides,
			Page:        0,
			Author:      user.Name,
			Date:        time.Now().Format("2006-01-02"),
			Theme:       styles.SelectTheme(m.Theme),
			CustomTheme: styles.CustomTheme(m.Theme),
		}, tea.WithAltScreen())

		err = p.Start()
		return err
	},
}

func Execute() {
	err := root.Execute()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}


Review comment (suggestion):
```suggestion
		m, 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 {
```

Code after:
	Short: "Slides is a terminal based presentation tool",
	Args:  cobra.MaximumNArgs(1),
	RunE: func(_ *cobra.Command, args []string) error {
		var content string
		var err error

		if len(args) > 0 {
			content, err = readFile(args[0])
		} else {
			content, err = readStdin()
		}

		if err != nil {
			return err
		}

		content = strings.ReplaceAll(content, altDelimiter, delimiter)
		slides := strings.Split(content, delimiter)

		user, err := user.Current()
		if err != nil {
			return errors.New("could not get current user")
		}

		m, 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:]
		}

		p := tea.NewProgram(model.Model{
			Slides: slides,
			Page:   0,
			Author: user.Name,
			Date:   time.Now().Format("2006-01-02"),
			Theme:  styles.SelectTheme(m.Theme),
		}, tea.WithAltScreen())

		err = p.Start()
		return err
	},
}

func Execute() {
	err := root.Execute()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
Repository: maaslalani/slides
File: internal/model/model.go (Go)

Code before:
		return m, nil

	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		case " ", "down", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--
			}
		}
	}
	return m, nil
}

func (m Model) View() string {
	style := glamour.WithStyles(m.Theme)
	if m.CustomTheme != nil {
		style = m.CustomTheme
	}

	r, _ := glamour.NewTermRenderer(style)
	slide, err := r.Render(m.Slides[m.Page])
	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)
}


Review comment (suggestion):
I think we should have the `glamour.TermRenderer` on the model, this way we don't need the `Theme` or `CustomTheme` on the model.

Code after:
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			return m, tea.Quit
		case " ", "down", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--
			}
		}
	}
	return m, nil
}

func (m Model) View() string {
  r, _ := glamour.NewTermRenderer(m.Theme, glamour.WithWordWrap(0))
	slide, err := r.Render(m.Slides[m.Page])
	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)
}
Repository: maaslalani/slides
File: styles/styles.go (Go)

Code before:

// SelectTheme picks a glamour style config based
// on the theme provided in the markdown header
func SelectTheme(theme string) ansi.StyleConfig {
	switch theme {
	case "ascii":
		return glamour.ASCIIStyleConfig
	case "light":
		return glamour.LightStyleConfig
	case "notty":
		return glamour.NoTTYStyleConfig
	default:
		return glamour.DarkStyleConfig
	}
}

// fileExists is a helper to verify
// that the provided filepath exists
// on the system
func fileExists(filepath string) bool {
	info, err := os.Stat(filepath)
	if os.IsNotExist(err) {
		return false
	}
	return !info.IsDir()
}


Review comment (suggestion):
Totally optional but we can create a package called `file` and make this a method `Exists`: `file.Exists(path)`

Code after:
// Package styles implements the theming logic for slides
package styles

import (
	_ "embed"
	"strings"

	"github.com/charmbracelet/glamour"
	"github.com/charmbracelet/lipgloss"
)

const (
	salmon = lipgloss.Color("#E8B4BC")
)

var (
	Author = lipgloss.NewStyle().Foreground(salmon).Align(lipgloss.Left).MarginLeft(2)
	Date   = lipgloss.NewStyle().Faint(true).Align(lipgloss.Left).Margin(0, 1)
	Page   = lipgloss.NewStyle().Foreground(salmon).Align(lipgloss.Right).MarginRight(3)
	Slide  = lipgloss.NewStyle().Padding(1)
	Status = lipgloss.NewStyle().Padding(1)
)

var (
	//go:embed theme.json
	DefaultTheme []byte
)

func JoinHorizontal(left, right string, width int) string {
	length := lipgloss.Width(left + right)
	if width < length {
		return left + " " + right
	}
	padding := strings.Repeat(" ", width-length)
	return left + padding + right
}

func JoinVertical(top, bottom string, height int) string {
	h := lipgloss.Height(top) + lipgloss.Height(bottom)
	if height < h {
		return top + "\n" + bottom
	}
	fill := strings.Repeat("\n", height-h)
	return top + fill + bottom
}

// SelectTheme picks a glamour style config based
// on the theme provided in the markdown header
func SelectTheme(theme string) glamour.TermRendererOption {
	switch theme {
	case "ascii":
		return glamour.WithStyles(glamour.ASCIIStyleConfig)
	case "light":
		return glamour.WithStyles(glamour.LightStyleConfig)
	case "dark":
		return glamour.WithStyles(glamour.DarkStyleConfig)
	case "notty":
		return glamour.WithStyles(glamour.NoTTYStyleConfig)
	default:
		return glamour.WithStylesFromJSONBytes(DefaultTheme)
	}
}
Repository: maaslalani/slides
File: internal/model/model.go (Go)

Code before:
package model

import (
	"fmt"

	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/glamour"
	"github.com/maaslalani/slides/styles"
)

type Model struct {
	Slides         []string
	Page           int
	Author         string
	Date           string
	RendererOption glamour.TermRendererOption
	viewport       viewport.Model
}

func (m Model) Init() tea.Cmd {
	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", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--

Review comment (suggestion):
I think we can name this `Theme`, since thats a more descriptive name for the `RendererOption` (in this case)

Code after:
package model

import (
	"fmt"

	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/glamour"
	"github.com/maaslalani/slides/styles"
)

type Model struct {
	Slides   []string
	Page     int
	Author   string
	Date     string
	Theme    glamour.TermRendererOption
	viewport viewport.Model
}

func (m Model) Init() tea.Cmd {
	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", "k", "right", "l", "enter", "n":
			if m.Page < len(m.Slides)-1 {
				m.Page++
			}
		case "up", "j", "left", "h", "p":
			if m.Page > 0 {
				m.Page--
Repository: maaslalani/slides
File: styles/styles.go (Go)

Code before:
	if height < h {
		return top + "\n" + bottom
	}
	fill := strings.Repeat("\n", height-h)
	return top + fill + bottom
}

// SelectTheme picks a glamour style config based
// on the theme provided in the markdown header
func SelectTheme(theme string) glamour.TermRendererOption {
	switch theme {
	case "ascii":
		return glamour.WithStyles(glamour.ASCIIStyleConfig)
	case "light":
		return glamour.WithStyles(glamour.LightStyleConfig)
	case "dark":
		return glamour.WithStyles(glamour.DarkStyleConfig)
	case "notty":
		return glamour.WithStyles(glamour.NoTTYStyleConfig)
	default:
		if termenv.EnvNoColor() {
			return glamour.WithStyles(glamour.NoTTYStyleConfig)
		}

		if !termenv.HasDarkBackground(){
			return glamour.WithStyles(glamour.LightStyleConfig)
		}
		return glamour.WithStylesFromJSONBytes(DefaultTheme)
	}
}


Review comment (bug):
👍  to this, I'm thinking about writing all error messages to a log file because we fail silently in most cases

Code after:
	h := lipgloss.Height(top) + lipgloss.Height(bottom)
	if height < h {
		return top + "\n" + bottom
	}
	fill := strings.Repeat("\n", height-h)
	return top + fill + bottom
}

// SelectTheme picks a glamour style config based
// on the theme provided in the markdown header
func SelectTheme(theme string) glamour.TermRendererOption {
	switch theme {
	case "ascii":
		return glamour.WithStyles(glamour.ASCIIStyleConfig)
	case "light":
		return glamour.WithStyles(glamour.LightStyleConfig)
	case "dark":
		return glamour.WithStyles(glamour.DarkStyleConfig)
	case "notty":
		return glamour.WithStyles(glamour.NoTTYStyleConfig)
	default:
		bytes, err := os.ReadFile(theme)
		if err == nil {
			return glamour.WithStylesFromJSONBytes(bytes)
		}
		// Should log a warning so the user knows we failed to read their theme file

		if termenv.EnvNoColor() {
			return glamour.WithStyles(glamour.NoTTYStyleConfig)
		}

		if !termenv.HasDarkBackground() {
			return glamour.WithStyles(glamour.LightStyleConfig)
		}

		return glamour.WithStylesFromJSONBytes(DefaultTheme)
	}
}
Repository: maaslalani/slides
File: styles/styles.go (Go)

Code before:

func JoinVertical(top, bottom string, height int) string {
	h := lipgloss.Height(top) + lipgloss.Height(bottom)
	if height < h {
		return top + "\n" + bottom
	}
	fill := strings.Repeat("\n", height-h)
	return top + fill + bottom
}

// SelectTheme picks a glamour style config based
// on the theme provided in the markdown header
func SelectTheme(theme string) glamour.TermRendererOption {
	switch theme {
	case "ascii":
		return glamour.WithStyles(glamour.ASCIIStyleConfig)
	case "light":
		return glamour.WithStyles(glamour.LightStyleConfig)
	case "dark":
		return glamour.WithStyles(glamour.DarkStyleConfig)
	case "notty":
		return glamour.WithStyles(glamour.NoTTYStyleConfig)
	default:
		var themeReader io.Reader
		var err error
		if strings.HasPrefix(theme, "http://") || strings.HasPrefix(theme, "https://") {
			var resp *http.Response
			resp, err = http.Get(theme)
			if err != nil {
				return getDefaultTheme()
			}
			defer resp.Body.Close()
			themeReader = resp.Body
		} else {
			file, err := os.Open(theme)
			if err != nil {
				return getDefaultTheme()
			}
			defer file.Close()
			themeReader = file
		}
		bytes, err := io.ReadAll(themeReader)
		if err == nil {
			return glamour.WithStylesFromJSONBytes(bytes)
		}
		// Should log a warning so the user knows we failed to read their theme file
		return getDefaultTheme()
	}
}

func getDefaultTheme() glamour.TermRendererOption {

Review comment (suggestion):
I think this is good enough

```suggestion
		if strings.HasPrefix(theme, "http") {
```

Code after:

func JoinVertical(top, bottom string, height int) string {
	h := lipgloss.Height(top) + lipgloss.Height(bottom)
	if height < h {
		return top + "\n" + bottom
	}
	fill := strings.Repeat("\n", height-h)
	return top + fill + bottom
}

// SelectTheme picks a glamour style config based
// on the theme provided in the markdown header
func SelectTheme(theme string) glamour.TermRendererOption {
	switch theme {
	case "ascii":
		return glamour.WithStyles(glamour.ASCIIStyleConfig)
	case "light":
		return glamour.WithStyles(glamour.LightStyleConfig)
	case "dark":
		return glamour.WithStyles(glamour.DarkStyleConfig)
	case "notty":
		return glamour.WithStyles(glamour.NoTTYStyleConfig)
	default:
		var themeReader io.Reader
		var err error
		if strings.HasPrefix(theme, "http") {
			var resp *http.Response
			resp, err = http.Get(theme)
			if err != nil {
				return getDefaultTheme()
			}
			defer resp.Body.Close()
			themeReader = resp.Body
		} else {
			file, err := os.Open(theme)
			if err != nil {
				return getDefaultTheme()
			}
			defer file.Close()
			themeReader = file
		}
		bytes, err := io.ReadAll(themeReader)
		if err == nil {
			return glamour.WithStylesFromJSONBytes(bytes)
		}
		// Should log a warning so the user knows we failed to read their theme file
		return getDefaultTheme()
	}
}

func getDefaultTheme() glamour.TermRendererOption {