ronantakizawa/codereview-bench huggingface dataset:
Repository: charmbracelet/bubbletea
File: mouse.go (Go)

Code before:
//
//     ESC [M Cb Cx Cy
//
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
func parseX10MouseEvent(buf []byte) (m MouseEvent, err error) {
	if len(buf) != 6 || string(buf[:3]) != "\x1b[M" {
		return m, errors.New("not an X10 mouse event")
	}

	e := buf[3] - 32

	const (
		bitShift  = 0b0000_0100
		bitAlt    = 0b0000_1000
		bitCtrl   = 0b0001_0000
		bitButton = 0b0010_0000
		bitWheel  = 0b0100_0000

		bitsRelease = 0b0000_0011

		bitsMask = 0b0000_0011

		bitsLeft   = 0b0000_0000
		bitsMiddle = 0b0000_0001
		bitsRight  = 0b0000_0010
		bitsMotion = 0b0000_0011

		bitsWheelUp   = 0b0000_0000
		bitsWheelDown = 0b0000_0001
	)

	if e&bitButton != 0 {
		// Check the low two bits.
		switch e & bitsMask {
		case bitsLeft:
			m.Type = MouseLeft
		case bitsMiddle:
			m.Type = MouseMiddle
		case bitsRight:
			m.Type = MouseRight
		case bitsMotion:
			m.Type = MouseMotion
		}
	} else if e&bitWheel != 0 {
		// Check the low two bits.
		switch e & bitsMask {
		case bitsWheelUp:
			m.Type = MouseWheelUp
		case bitsWheelDown:
			m.Type = MouseWheelDown
		}

Review comment (suggestion):
I _think_ the motion bits here are actually `0b0010_0011` (was `35` as an integer in the previous version of the code).

Code after:
//
//     ESC [M Cb Cx Cy
//
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
func parseX10MouseEvent(buf []byte) (m MouseEvent, err error) {
	if len(buf) != 6 || string(buf[:3]) != "\x1b[M" {
		return m, errors.New("not an X10 mouse event")
	}

	e := buf[3] - 32

	const (
		bitShift  = 0b0000_0100
		bitAlt    = 0b0000_1000
		bitCtrl   = 0b0001_0000
		bitMotion = 0b0010_0000
		bitWheel  = 0b0100_0000

		bitsMask = 0b0000_0011

		bitsLeft    = 0b0000_0000
		bitsMiddle  = 0b0000_0001
		bitsRight   = 0b0000_0010
		bitsRelease = 0b0000_0011

		bitsWheelUp   = 0b0000_0000
		bitsWheelDown = 0b0000_0001
	)

	if e&bitWheel != 0 {
		// Check the low two bits.
		switch e & bitsMask {
		case bitsWheelUp:
			m.Type = MouseWheelUp
		case bitsWheelDown:
			m.Type = MouseWheelDown
		}
	} else {
		// Check the low two bits.
		// We do not separate clicking and dragging.
		switch e & bitsMask {
		case bitsLeft:
			m.Type = MouseLeft
		case bitsMiddle:
			m.Type = MouseMiddle
		case bitsRight:
			m.Type = MouseRight
		case bitsRelease:
			if e&bitMotion != 0 {
				m.Type = MouseMotion
			} else {
Repository: charmbracelet/bubbletea
File: logging.go (Go)

Code before:
	"os"
	"unicode"
)

// LogToFile sets up default logging to log to a file. This is helpful as we
// can't print to the terminal since our TUI is occupying it. If the file
// doesn't exist it will be created.
//
// Don't forget to close the file when you're done with it.
//
//   f, err := LogToFile("debug.log", "debug")
//   if err != nil {
//		fmt.Println("fatal:", err)
//		os.Exit(1)
//   }
//   defer f.Close()
func LogToFile(path string, prefix string) (*os.File, error) {
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		return nil, err
	}
	log.SetOutput(f)

	// Add a space after the prefix if one doesn't exist
	finalPrefixChar := prefix[len(prefix)-1]
	if len(prefix) > 0 && !unicode.IsSpace(rune(finalPrefixChar)) {
		prefix += " "
	}
	log.SetPrefix(prefix)

	return f, nil
}


Review comment (suggestion):
The `len` check should probably be dealt with before accesing `prefx[len(prefix)-1]`.

Code after:
	"os"
	"unicode"
)

// LogToFile sets up default logging to log to a file. This is helpful as we
// can't print to the terminal since our TUI is occupying it. If the file
// doesn't exist it will be created.
//
// Don't forget to close the file when you're done with it.
//
//   f, err := LogToFile("debug.log", "debug")
//   if err != nil {
//		fmt.Println("fatal:", err)
//		os.Exit(1)
//   }
//   defer f.Close()
func LogToFile(path string, prefix string) (*os.File, error) {
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		return nil, err
	}
	log.SetOutput(f)

	// Add a space after the prefix if a prefix is being specified and it
	// doesn't already have a trailing space.
	if len(prefix) > 0 {
		finalChar := prefix[len(prefix)-1]
		if unicode.IsSpace(rune(finalChar)) {
			prefix += " "
		}
	}
	log.SetPrefix(prefix)

	return f, nil
}
Repository: charmbracelet/bubbletea
File: examples/debounce/main.go (Go)

Code before:
package main

/*
With each keypress you bump the tag on the `model` and send that tag along in a `Msg` after delaying with a `tea.Tick`.
If the tag in the `Msg` matches the one on the model you know the debouncing is done and you can then return your command

We do this in the [spinner](https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go) Bubble to keep it from spinning out of control in the event that it accidentally gets too many spin Cmds.
*/

import (
	"fmt"
	"os"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

type exitMsg int

type model struct {
	tag int
}

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.KeyMsg:
		m.tag++
		return m, tea.Tick(time.Second, func(_ time.Time) tea.Msg {
			return exitMsg(m.tag)

Review comment (suggestion):
```suggestion
// The tag is incremented with each keypress which sends that tag along in a Msg after a `tea.Tick` delay
// If the tag in the Msg matches the model's state, we can be sure that the debouncing is complete and that we can return our command.
// This is useful in cases such as the Spinner bubble to prevent accidentally spinning it too fast.
// https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go
```

Code after:
package main

// This example illustrates how to debounce commands.
//
// When the user presses a key we increment the "tag" value on the model and,
// after a short delay, we include that tag value in the message produced
// by the Tick command.
//
// In a subsequent Update, if the tag in the Msg matches current tag on the
// model's state we know that the debouncing is complete and we can proceeed as
// normal. If not, we simply ignore the inbound message.

import (
	"fmt"
	"os"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

const debounceDuration = time.Second

type exitMsg int

type model struct {
	tag int
}

func (m model) Init() tea.Cmd {
	return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Repository: charmbracelet/bubbletea
File: examples/debounce/main.go (Go)

Code before:
package main

/*
With each keypress you bump the tag on the `model` and send that tag along in a `Msg` after delaying with a `tea.Tick`.
If the tag in the `Msg` matches the one on the model you know the debouncing is done and you can then return your command

We do this in the [spinner](https://github.com/charmbracelet/bubbles/blob/master/spinner/spinner.go) Bubble to keep it from spinning out of control in the event that it accidentally gets too many spin Cmds.
*/

import (
	"fmt"
	"os"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

type exitMsg int

type model struct {
	tag int
}

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.KeyMsg:
		m.tag++
		return m, tea.Tick(time.Second, func(_ time.Time) tea.Msg {
			return exitMsg(m.tag)
		})
	case exitMsg:
		if int(msg) == m.tag {
			return m, tea.Quit
		}
	}

	return m, nil
}

func (m model) View() string {
	return "To exit press any key, then wait for one second without pressing anything."
}

func main() {
	if err := tea.NewProgram(model{}).Start(); err != nil {
		fmt.Println("uh oh:", err)
		os.Exit(1)
	}
}


Review comment (suggestion):
```suggestion
    m := model{}
    p := tea.NewProgram(m)
	if err := p.Start(); err != nil {
		fmt.Println("uh oh:", err)
		os.Exit(1)
	}
```

Code after:
			return exitMsg(m.tag)
		})
	case exitMsg:
		// If the tag in the message doesn't match the tag on the model then we
		// know that this message was not the last one sent and another is on
		// the way. If that's the case we know, we can ignore this message.
		// Otherwise, the debounce timeout has passed and this message is a
		// valid debounced one.
		if int(msg) == m.tag {
			return m, tea.Quit
		}
	}

	return m, nil
}

func (m model) View() string {
	return fmt.Sprintf("Key presses: %d", m.tag) +
		"\nTo exit press any key, then wait for one second without pressing anything."
}

func main() {
	if err := tea.NewProgram(model{}).Start(); err != nil {
		fmt.Println("uh oh:", err)
		os.Exit(1)
	}
}
Repository: charmbracelet/bubbletea
File: examples/simple/main_test.go (Go)

Code before:
	"strconv"
	"testing"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/bubbletea/teatest"
)

func TestApp(t *testing.T) {
	t.Parallel()
	for i := 0; i < 3; i++ {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()
			m := model(10)
			teatest.TestModel(
				t, m,
				func(p teatest.Program, in io.Writer) {
					time.Sleep(time.Second + time.Millisecond*200)
					p.Send("ignored msg")
					p.Send(tea.KeyMsg{
						Type: tea.KeyEnter,
					})
				},
				func(out []byte) {
					teatest.RequireEqualOutput(t, out)
				},
			)
		})
	}
}


Review comment (suggestion):
Firstly, thank you for this PR! I'm currently building my first bubbles and want to test them. Here's some feedback based on this PR based on my own testing efforts in https://github.com/twpayne/chezmoi/pull/2359.

At this point, I'm testing that my models have certain properties after receiving a bunch of messages, so I'd like to include that in the testing framework. A specific suggestion would be to make `teatest.Model` accept [functional options](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) (just like most of bubbletea already does), so I can write code like:

```go
    teatest.Model(
        t, m,
        teatest.WithSender(p teatest.Program, in io.Writer) {
            // as above
        },
        teatest.WithRequireOutput(/* ... */),
        teatest.WithValidateFinalModel(func(m tea.Model) err {
            // check some properties of m
        },
    )
```

Code after:
	"io"
	"regexp"
	"testing"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/x/exp/teatest"
)

func TestApp(t *testing.T) {
	m := model(10)
	tm := teatest.NewTestModel(
		t, m,
		teatest.WithInitialTermSize(70, 30),
	)
	t.Cleanup(func() {
		if err := tm.Quit(); err != nil {
			t.Fatal(err)
		}
	})

	time.Sleep(time.Second + time.Millisecond*200)
	tm.Type("I'm typing things, but it'll be ignored by my program")
	tm.Send("ignored msg")
	tm.Send(tea.KeyMsg{
		Type: tea.KeyEnter,
	})

	if err := tm.Quit(); err != nil {
		t.Fatal(err)
	}

	out := readBts(t, tm.FinalOutput())
	if !regexp.MustCompile(`This program will exit in \d+ seconds`).Match(out) {
		t.Fatalf("output does not match the given regular expression: %s", string(out))
	}
	teatest.RequireEqualOutput(t, out)

	if tm.FinalModel().(model) != 9 {
		t.Errorf("expected model to be 10, was %d", m)
	}
}

func TestAppInteractive(t *testing.T) {
	m := model(10)
	tm := teatest.NewTestModel(
		t, m,
		teatest.WithInitialTermSize(70, 30),
	)

	time.Sleep(time.Second + time.Millisecond*200)
Repository: charmbracelet/bubbletea
File: key.go (Go)

Code before:

		runes = append(runes, r)
		w = width
	}
	// add the final set of runes we decoded
	runeSets = append(runeSets, runes)

	if len(runeSets) == 0 {
		return nil, errors.New("received 0 runes from input")
	}

	var msgs []Msg
	for _, runes := range runeSets {
		// Is it a sequence, like an arrow key?
		if k, ok := sequences[string(runes)]; ok {
			msgs = append(msgs, KeyMsg(k))
			continue
		}

		// Is this an unrecognized CSI sequence? If so, ignore it.
		if len(runes) > 2 && runes[0] == 0x1b && (runes[1] == '[' ||
			(len(runes) > 3 && runes[1] == 0x1b && runes[2] == '[')) {
			continue
		}

		// Is the alt key pressed? If so, the buffer will be prefixed with an
		// escape.
		alt := false
		if len(runes) > 1 && runes[0] == 0x1b {
			alt = true
			runes = runes[1:]
		}

		for _, v := range runes {
			// Is the first rune a control character?
			r := KeyType(v)
			if r <= keyUS || r == keyDEL {
				msgs = append(msgs, KeyMsg(Key{Type: r, Alt: alt}))
				continue
			}

			// If it's a space, override the type with KeySpace (but still include
			// the rune).
			if r == ' ' {
				msgs = append(msgs, KeyMsg(Key{Type: KeySpace, Runes: []rune{v}, Alt: alt}))
				continue
			}

			// Welp, just regular, ol' runes.
			msgs = append(msgs, KeyMsg(Key{Type: KeyRunes, Runes: []rune{v}, Alt: alt}))
		}

Review comment (suggestion):
Just checking in if this was removed on purpose? Since you added a test for the two sequences above, I assume it's working fine tho.

Code after:
	"\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm

	"\x1b\x1b[28~": {Type: KeyF15, Alt: true}, // urxvt
	"\x1b\x1b[29~": {Type: KeyF16, Alt: true}, // urxvt

	"\x1b[15;2~": {Type: KeyF17},
	"\x1b[17;2~": {Type: KeyF18},
	"\x1b[18;2~": {Type: KeyF19},
	"\x1b[19;2~": {Type: KeyF20},

	"\x1b[31~": {Type: KeyF17},
	"\x1b[32~": {Type: KeyF18},
	"\x1b[33~": {Type: KeyF19},
	"\x1b[34~": {Type: KeyF20},

	"\x1b\x1b[31~": {Type: KeyF17, Alt: true}, // urxvt
	"\x1b\x1b[32~": {Type: KeyF18, Alt: true}, // urxvt
	"\x1b\x1b[33~": {Type: KeyF19, Alt: true}, // urxvt
	"\x1b\x1b[34~": {Type: KeyF20, Alt: true}, // urxvt

	// Powershell sequences.
	"\x1bOA": {Type: KeyUp, Alt: false},
	"\x1bOB": {Type: KeyDown, Alt: false},
	"\x1bOC": {Type: KeyRight, Alt: false},
	"\x1bOD": {Type: KeyLeft, Alt: false},
}

// readInputs reads keypress and mouse inputs from a TTY and returns messages
// containing information about the key or mouse events accordingly.
func readInputs(input io.Reader) ([]Msg, error) {
	var buf [256]byte

	// Read and block
	numBytes, err := input.Read(buf[:])
	if err != nil {
		return nil, err
	}
	b := buf[:numBytes]
	b, err = localereader.UTF8(b)
	if err != nil {
		return nil, err
	}

	// Check if it's a mouse event. For now we're parsing X10-type mouse events
	// only.
	mouseEvent, err := parseX10MouseEvents(b)
	if err == nil {
		var m []Msg
		for _, v := range mouseEvent {
			m = append(m, MouseMsg(v))
		}
Repository: charmbracelet/bubbletea
File: tea_test.go (Go)

Code before:
				return
			}
		}
	}()

	if _, err := p.Run(); err != nil {
		t.Fatal(err)
	}
}

func TestTeaBatchMsg(t *testing.T) {
	var buf bytes.Buffer
	var in bytes.Buffer

	inc := func() Msg {
		return incrementMsg{}
	}

	m := &testModel{}
	p := NewProgram(m, WithInput(&in), WithOutput(&buf))
	go func() {
		p.Send(batchMsg{inc, inc})

		for {
			time.Sleep(time.Millisecond)
			if m.counter.Load() == 2 {
				p.Quit()
				return
			}
		}
	}()

	if _, err := p.Run(); err != nil {
		t.Fatal(err)
	}

	if m.counter.Load() != 2 {
		t.Fatalf("counter should be 2, got %d", m.counter)
	}
}


Review comment (suggestion):
Though this is _extremely_ unlikely to miss this condition, I usually prefer using `>=` for these types of situations so that we don't accidentally cause a infinitely for loop (albeit _extremely_ unlikely).

```suggestion
			if m.counter.Load() >= 2 {
```

Code after:
				return
			}
		}
	}()

	if _, err := p.Run(); err != nil {
		t.Fatal(err)
	}
}

func TestTeaBatchMsg(t *testing.T) {
	var buf bytes.Buffer
	var in bytes.Buffer

	inc := func() Msg {
		return incrementMsg{}
	}

	m := &testModel{}
	p := NewProgram(m, WithInput(&in), WithOutput(&buf))
	go func() {
		p.Send(batchMsg{inc, inc})

		for {
			time.Sleep(time.Millisecond)
			i := m.counter.Load()
			if i != nil && i.(int) >= 2 {
				p.Quit()
				return
			}
		}
	}()

	if _, err := p.Run(); err != nil {
		t.Fatal(err)
	}

	if m.counter.Load() != 2 {
		t.Fatalf("counter should be 2, got %d", m.counter)
	}
}
Repository: charmbracelet/bubbletea
File: tea.go (Go)

Code before:

			case showCursorMsg:
				p.renderer.showCursor()

			case hideCursorMsg:
				p.renderer.hideCursor()

			case execMsg:
				// NB: this blocks.
				p.exec(msg.cmd, msg.fn)

			case BatchMsg:
				for _, cmd := range msg {
					cmds <- cmd
				}
				continue

			case sequenceMsg:
				go func() {
					// Execute commands one at a time, in order.
					for _, cmd := range msg {
						if cmd == nil {
							continue
						}
						msg := cmd()
						switch msg := msg.(type) {
						case BatchMsg: // if it's a batch msg
							g, _ := errgroup.WithContext(p.ctx)
							for _, cmd := range msg {
								cmd := cmd
								g.Go(func() error {
									p.Send(cmd())
									return nil
								})
							}
							//nolint:errcheck
							g.Wait() // wait for all commands from batch msg to finish
							continue
						}
						p.Send(msg)
					}
				}()
			}

			// Process internal messages for the renderer.
			if r, ok := p.renderer.(*standardRenderer); ok {
				r.handleMessages(msg)
			}

			var cmd Cmd
			model, cmd = model.Update(msg) // run update

Review comment (performance):
Since there's only one type case here and the `continue` bailing out is a bit hidden, maybe a simple type assertion would be nicer here.

Like:

```go
if m, ok := msg.(BatchMsg); ok {
...
} else {
    p.Send(msg)
}
```

What do you think, @stoffeastrom?

Code after:

			case showCursorMsg:
				p.renderer.showCursor()

			case hideCursorMsg:
				p.renderer.hideCursor()

			case execMsg:
				// NB: this blocks.
				p.exec(msg.cmd, msg.fn)

			case BatchMsg:
				for _, cmd := range msg {
					cmds <- cmd
				}
				continue

			case sequenceMsg:
				go func() {
					// Execute commands one at a time, in order.
					for _, cmd := range msg {
						if cmd == nil {
							continue
						}
						msg := cmd()
						if batchMsg, ok := msg.(BatchMsg); ok {
							g, _ := errgroup.WithContext(p.ctx)
							for _, cmd := range batchMsg {
								cmd := cmd
								g.Go(func() error {
									p.Send(cmd())
									return nil
								})
							}
							//nolint:errcheck
							g.Wait() // wait for all commands from batch msg to finish
							continue
						} else {
							p.Send(msg)
						}
					}
				}()
			}

			// Process internal messages for the renderer.
			if r, ok := p.renderer.(*standardRenderer); ok {
				r.handleMessages(msg)
			}

			var cmd Cmd
			model, cmd = model.Update(msg) // run update
Repository: charmbracelet/bubbletea
File: commands.go (Go)

Code before:
//	   }
//	   return nil
//	}
//
//	cmd := Sequentially(saveStateCmd, Quit)
//
// Deprecated: use Sequence instead.
func Sequentially(cmds ...Cmd) Cmd {
	return func() Msg {
		for _, cmd := range cmds {
			if cmd == nil {
				continue
			}
			if msg := cmd(); msg != nil {
				return msg
			}
		}
		return nil
	}
}

// setWindowTitleMsg is an internal message used to set the window title.
type setWindowTitleMsg string

// SetWindowTitle produces a command that sets the terminal title.
// The message returned contains the title that was set.
//
// For example:
//
//	func (m model) Init() Cmd {
//	    // Set title.
//	    return tea.SetWindowTitle("My App")
//	}
func SetWindowTitle(title string) Cmd {
	return func() Msg {
		return setWindowTitleMsg(title)
	}
}


Review comment (suggestion):
Actually, I'd drop the part of comment about the message because it doesn't pertain to the end user.

Code after:
//	   }
//	   return nil
//	}
//
//	cmd := Sequentially(saveStateCmd, Quit)
//
// Deprecated: use Sequence instead.
func Sequentially(cmds ...Cmd) Cmd {
	return func() Msg {
		for _, cmd := range cmds {
			if cmd == nil {
				continue
			}
			if msg := cmd(); msg != nil {
				return msg
			}
		}
		return nil
	}
}

// setWindowTitleMsg is an internal message used to set the window title.
type setWindowTitleMsg string

// SetWindowTitle produces a command that sets the terminal title.
//
// For example:
//
//	func (m model) Init() Cmd {
//	    // Set title.
//	    return tea.SetWindowTitle("My App")
//	}
func SetWindowTitle(title string) Cmd {
	return func() Msg {
		return setWindowTitleMsg(title)
	}
}
Repository: charmbracelet/bubbletea
File: logging.go (Go)

Code before:
// LogToFile sets up default logging to log to a file. This is helpful as we
// can't print to the terminal since our TUI is occupying it. If the file
// doesn't exist it will be created.
//
// Don't forget to close the file when you're done with it.
//
//	  f, err := LogToFile("debug.log", "debug")
//	  if err != nil {
//			fmt.Println("fatal:", err)
//			os.Exit(1)
//	  }
//	  defer f.Close()
func LogToFile(path string, prefix string) (*os.File, error) {
	return LogToFileWith(path, prefix, log.Default())
}

// LogOptionsSetter is an interface implemented by stdlib's log and charm's log
// libraries.
type LogOptionsSetter interface {
	SetOutput(io.Writer)
	SetPrefix(string)
}

// LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter.
func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) {
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) //nolint:gomnd
	if err != nil {
		return nil, fmt.Errorf("error opening file for logging: %w", err)
	}
	log.SetOutput(f)

	// Add a space after the prefix if a prefix is being specified and it
	// doesn't already have a trailing space.
	if len(prefix) > 0 {
		finalChar := prefix[len(prefix)-1]
		if !unicode.IsSpace(rune(finalChar)) {
			prefix += " "
		}
	}
	log.SetPrefix(prefix)

	return f, nil
}


Review comment (security):
Actually, this might be a valid warning and `0o600` could be more sensible for (potentially) sensitive data like a logfile?

Code after:
// LogToFile sets up default logging to log to a file. This is helpful as we
// can't print to the terminal since our TUI is occupying it. If the file
// doesn't exist it will be created.
//
// Don't forget to close the file when you're done with it.
//
//	  f, err := LogToFile("debug.log", "debug")
//	  if err != nil {
//			fmt.Println("fatal:", err)
//			os.Exit(1)
//	  }
//	  defer f.Close()
func LogToFile(path string, prefix string) (*os.File, error) {
	return LogToFileWith(path, prefix, log.Default())
}

// LogOptionsSetter is an interface implemented by stdlib's log and charm's log
// libraries.
type LogOptionsSetter interface {
	SetOutput(io.Writer)
	SetPrefix(string)
}

// LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter.
func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) {
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:gomnd
	if err != nil {
		return nil, fmt.Errorf("error opening file for logging: %w", err)
	}
	log.SetOutput(f)

	// Add a space after the prefix if a prefix is being specified and it
	// doesn't already have a trailing space.
	if len(prefix) > 0 {
		finalChar := prefix[len(prefix)-1]
		if !unicode.IsSpace(rune(finalChar)) {
			prefix += " "
		}
	}
	log.SetPrefix(prefix)

	return f, nil
}