<commit_before>package interactive

import (
	"io"
	"os"
	"strings"

	"golang.org/x/crypto/ssh/terminal"
)

// A Session is an interactive shell. The New() function should be used to
// obtain a new Session instance.
type Session struct {
	// Action is the actual application logic that is looped until the
	// application gets terminated.
	Action ActionFunc
	// After is run AFTER the action function, BEFORE the session is closed.
	// It is invoked by context.Close().
	After AfterFunc
	// Before is run BEFORE the action function.
	Before BeforeFunc

	context *Context
	fd      int
	state   *terminal.State
	term    *terminal.Terminal
}

// New spawns an interactive session in the current terminal. A prompt character
// needs to be provided which will be printed when user input is awaited.
func New(prompt string) *Session {
	// Save old state and set terminal into raw mode.
	fd := int(os.Stdin.Fd())
	oldState, err := terminal.MakeRaw(fd)
	if err != nil {
		panic(err)
	}

	// Satisfies the ReadWriter interface and serves as I/O for the new terminal.
	shell := &Shell{
		r: os.Stdin,
		w: os.Stdout,
	}

	// Create new terminal with desired prompt sign.
	term := terminal.NewTerminal(shell, strings.Trim(prompt, " ")+" ")

	// Finally create the session.
	s := &Session{
		Action: dummyAction,
		fd:     fd,
		state:  oldState,
		term:   term,
	}
	s.context = &Context{session: s}

	// Set up Ctrl^C listener.
	term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
		if key == '\x03' {
			s.close(0)
		}
		return "", 0, false
	}

	return s
}

// Run is a blocking method that executes the actual logic.
func (s *Session) Run() {
	// Run Before function if present.
	if s.Before != nil {
		s.Before(s.context)
	}

	// Loop root action. Close if an error is present.
	for {
		if err := s.Action(s.context); err != nil {
			s.writeLine(err.Error())
			s.close(1)
		}
	}
}

func (s *Session) close(exitCode int) {
	// Run After function if present.
	if s.After != nil {
		s.After(s.context)
	}

	// Restore terminal.
	terminal.Restore(s.fd, s.state)
	os.Exit(exitCode)
}

func (s *Session) readLine() string {
	text, err := s.term.ReadLine()
	if err != nil {
		// Close session on Ctrl^D.
		if err == io.EOF {
			s.close(0)
		}
		panic(err)
	}
	return text
}

func (s *Session) writeLine(text string) {
	s.term.Write([]byte(text + "\n"))
}
<commit_msg>Returning an error in the Before() function will close the session.<commit_after>package interactive

import (
	"io"
	"os"
	"strings"

	"golang.org/x/crypto/ssh/terminal"
)

// A Session is an interactive shell. The New() function should be used to
// obtain a new Session instance.
type Session struct {
	// Action is the actual application logic that is looped until the
	// application gets terminated.
	Action ActionFunc
	// After is run AFTER the action function, BEFORE the session is closed.
	// It is invoked by context.Close().
	After AfterFunc
	// Before is run BEFORE the action function.
	Before BeforeFunc

	context *Context
	fd      int
	state   *terminal.State
	term    *terminal.Terminal
}

// New spawns an interactive session in the current terminal. A prompt character
// needs to be provided which will be printed when user input is awaited.
func New(prompt string) *Session {
	// Save old state and set terminal into raw mode.
	fd := int(os.Stdin.Fd())
	oldState, err := terminal.MakeRaw(fd)
	if err != nil {
		panic(err)
	}

	// Satisfies the ReadWriter interface and serves as I/O for the new terminal.
	shell := &Shell{
		r: os.Stdin,
		w: os.Stdout,
	}

	// Create new terminal with desired prompt sign.
	term := terminal.NewTerminal(shell, strings.Trim(prompt, " ")+" ")

	// Finally create the session.
	s := &Session{
		Action: dummyAction,
		fd:     fd,
		state:  oldState,
		term:   term,
	}
	s.context = &Context{session: s}

	// Set up Ctrl^C listener.
	term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
		if key == '\x03' {
			s.close(0)
		}
		return "", 0, false
	}

	return s
}

// Run is a blocking method that executes the actual logic.
func (s *Session) Run() {
	// Run Before function if present. Close session if an error occurs.
	if s.Before != nil {
		if err := s.Before(s.context); err != nil {
			s.writeLine(err.Error())
			s.close(1)
		}
	}

	// Loop root action. Close session if an error occurs.
	for {
		if err := s.Action(s.context); err != nil {
			s.writeLine(err.Error())
			s.close(1)
		}
	}
}

func (s *Session) close(exitCode int) {
	// Run After function if present.
	if s.After != nil {
		if err := s.After(s.context); err != nil {
			s.writeLine(err.Error())
		}
	}

	// Restore terminal.
	terminal.Restore(s.fd, s.state)
	os.Exit(exitCode)
}

func (s *Session) readLine() string {
	text, err := s.term.ReadLine()
	if err != nil {
		// Close session on Ctrl^D.
		if err == io.EOF {
			s.close(0)
		}
		panic(err)
	}
	return text
}

func (s *Session) writeLine(text string) {
	s.term.Write([]byte(text + "\n"))
}

{'fraction_non_alphanumeric': 0.12574493444576879, 'fraction_numerical': 0.002582439411998411, 'mean_word_length': 3.0409309791332264, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3539666', 'n_tokens_mistral': 1814, 'n_tokens_neox': 1697, 'n_words': 599}