profile

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package profile stores the local player identities twixtui plays under.

There are no passwords: a profile is a name and two timestamps, kept in one JSON file under the user's configuration directory. The store's job is to let a returning player pick the identity they used last time even when they misremember how they spelled it, which is what Search is for.

Index

Examples

Constants

View Source
const EnvConfigDir = "TWIXTUI_CONFIG_DIR"

EnvConfigDir names an environment variable that replaces the default configuration directory outright (it is used as-is, with no "twixtui" component appended). Tests and the end-to-end harness set it so a run never touches the state of the person running it.

View Source
const MaxNameRunes = 32

MaxNameRunes bounds a profile name. Thirty-two characters is long enough for a full name with spaces and short enough that a list of profiles still fits in a narrow terminal pane alongside its rating.

Variables

View Source
var (
	ErrNameEmpty     = errors.New("name is empty")
	ErrNameTooLong   = errors.New("name is too long")
	ErrNamePadded    = errors.New("name has leading or trailing whitespace")
	ErrNameControl   = errors.New("name contains a control character")
	ErrNameInvisible = errors.New("name contains an invisible or bidirectional control character")
	ErrNameNotUTF8   = errors.New("name is not valid UTF-8")
)

Name rejection reasons. Callers match these with errors.Is to report the specific problem rather than a generic "invalid name".

View Source
var (
	ErrNotFound = errors.New("profile not found")
	ErrExists   = errors.New("profile already exists")
)

Store errors. Callers match these with errors.Is.

Functions

func DefaultDir

func DefaultDir() (string, error)

DefaultDir returns the directory twixtui keeps player state in.

func ValidateName

func ValidateName(name string) error

ValidateName reports whether a string is usable as a profile name.

The rule: valid UTF-8, one to MaxNameRunes characters, no leading or trailing whitespace, no control characters, and no invisible or bidirectional formatting characters. Interior spaces and non-Latin scripts are fine — the name is a display identity, not a filename or a shell word.

Invisible formatting characters are refused rather than stripped because a name that does not render as its own characters cannot be typed back by the person who chose it, which defeats the point of being able to find your profile again.

Types

type Match

type Match struct {
	Profile Profile
	// Score ranks the match; higher is better. Scores are only comparable
	// within one result set.
	Score int
	// Positions are the indexes, in runes, of the characters of Profile.Name
	// that the query matched, so the caller can highlight them.
	Positions []int
}

Match is a profile that a search query found.

type Profile

type Profile struct {
	Name     string    `json:"name"`
	Created  time.Time `json:"created"`
	LastUsed time.Time `json:"last_used"`
	// Introduced records that this player has been through the short
	// introduction the interface offers on a first run. See introduction.go for
	// why it is a property of the profile rather than of the machine, and for
	// why it is added without moving storeVersion.
	Introduced bool `json:"introduced,omitempty"`
}

Profile is one local player identity.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the set of profiles on this machine.

A Store is safe for concurrent use. Mutations reload the file inside an advisory lock before applying, so a second twixtui process cannot lose the first one's writes; reads reload only when the file has changed underneath.

func Open

func Open(dir string) (*Store, error)

Open loads the profile store in dir, creating the directory on first use. An empty dir means the default configuration directory.

func (*Store) ClearCurrent

func (s *Store) ClearCurrent() error

ClearCurrent forgets which profile is playing.

func (*Store) Create

func (s *Store) Create(name string) (Profile, error)

Create adds a profile. The name is validated and must not collide with an existing one, case and interior spacing ignored.

func (*Store) Current

func (s *Store) Current() (Profile, bool)

Current returns the profile last chosen on this machine.

A name that no longer matches a profile reports as no choice rather than as an error: a deleted profile is an ordinary thing to find here, not a fault.

func (*Store) Delete

func (s *Store) Delete(name string) error

Delete removes a profile. Recorded results are not touched: the leaderboard keeps its own history, and deleting an identity is not meant to rewrite the record of games that were played.

func (*Store) Get

func (s *Store) Get(name string) (Profile, bool)

Get looks a profile up by name, ignoring case and whitespace differences the same way duplicate detection does.

func (*Store) List

func (s *Store) List() []Profile

List returns every profile, most recently used first.

Example
dir, cleanup := exampleDir()
defer cleanup()

store, err := Open(dir)
if err != nil {
	log.Fatal(err)
}
for _, name := range []string{"Balint", "Bernadett"} {
	if _, err := store.Create(name); err != nil {
		log.Fatal(err)
	}
}
if err := store.Touch("balint"); err != nil {
	log.Fatal(err)
}

// Most recently used first, which is the order the launch prompt offers.
for _, p := range store.List() {
	fmt.Println(p.Name)
}
Output:
Balint
Bernadett

func (*Store) MarkIntroduced added in v0.2.0

func (s *Store) MarkIntroduced(name string) error

MarkIntroduced records that a profile has been through the introduction. It is called when the introduction is left, whether the player read it through or skipped it: somebody who skipped does not want it again next launch, so the two departures are the same fact.

func (*Store) Path

func (s *Store) Path() string

Path reports the file the store reads and writes, for diagnostics.

func (*Store) Rename

func (s *Store) Rename(oldName, newName string) error

Rename changes a profile's name, keeping its timestamps. Changing only the capitalisation of an existing name is allowed; colliding with a different profile is not.

func (*Store) Search

func (s *Store) Search(query string) []Match

Search ranks profiles against a query.

An empty query returns every profile in List order, most recently used first, which is the browsable list a player scrolls when they cannot recall the name at all. A non-empty query is matched two ways:

  • as a subsequence, scored by github.com/sahilm/fuzzy, which handles partial names ("lin"), dropped letters ("balnt") and any capitalisation;
  • failing that, by bounded edit distance against any part of the name, which handles the typo classes a subsequence matcher structurally cannot see — a transposition ("balitn"), a doubled letter ("ballint") or a wrong letter ("balont") all put a query rune where no later occurrence exists.

Every subsequence match outranks every rescued one, and rescues are ordered by how many corrections they needed. Ties break towards the most recently used profile.

Example
dir, cleanup := exampleDir()
defer cleanup()

store, err := Open(dir)
if err != nil {
	log.Fatal(err)
}
for _, name := range []string{"Balint", "Bernadett", "Bella Ackland"} {
	if _, err := store.Create(name); err != nil {
		log.Fatal(err)
	}
}

// The player transposed the last two letters of their own name.
for _, m := range store.Search("balitn") {
	fmt.Println(m.Profile.Name, m.Positions)
}
Output:
Balint [0 1 2 3 5]

func (*Store) SetCurrent

func (s *Store) SetCurrent(name string) error

SetCurrent records the profile that is playing. The name must already exist, so that the recorded choice cannot point at nothing.

func (*Store) Touch

func (s *Store) Touch(name string) error

Touch records that a profile was just used, which is what orders the list the player sees at launch.

func (*Store) UseCurrent

func (s *Store) UseCurrent(name string) (Profile, error)

UseCurrent records the choice and marks the profile as used, which is the pair of things every caller wants when a player picks a name.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL