├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── LICENSE
├── README.md
├── game
├── default_texts.go
├── events.go
├── game.go
├── server.go
└── state.go
├── go.mod
├── go.sum
├── main.go
├── settings
├── enums.go
└── settings.go
├── stats
└── stats.go
├── ui
├── multiplayer.go
├── settings.go
├── singleplayer.go
├── ui.go
└── welcome.go
└── utils
├── errors.go
└── strings.go
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gomod"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | name: Build
8 | runs-on: ubuntu-latest
9 | steps:
10 |
11 | - name: Set up Go 1.13
12 | uses: actions/setup-go@v1
13 | with:
14 | go-version: 1.13
15 | id: go
16 |
17 | - name: Check out code into the Go module directory
18 | uses: actions/checkout@v1
19 |
20 | - name: Get dependencies
21 | run: |
22 | go get -v -t -d ./...
23 |
24 | - name: Build Linux
25 | run: go build -o typer-go -v .
26 |
27 | - name: Build Windows
28 | if: startsWith(github.ref, 'refs/tags/') && github.repository == 'shilangyu/typer-go'
29 | continue-on-error: true
30 | run: GOOS=windows GOARCH=386 go build -o typer-go.exe -v .
31 |
32 | - name: Build OSX
33 | if: startsWith(github.ref, 'refs/tags/') && github.repository == 'shilangyu/typer-go'
34 | continue-on-error: true
35 | run: GOOS=darwin GOARCH=amd64 go build -o typer-go.dmg -v .
36 |
37 | - name: Release
38 | uses: softprops/action-gh-release@v1
39 | if: startsWith(github.ref, 'refs/tags/') && github.repository == 'shilangyu/typer-go'
40 | with:
41 | files: |
42 | typer-go
43 | typer-go.exe
44 | typer-go.dmg
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 |
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Marcin Wojnarowski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # typer-go
2 |
3 | [](https://goreportcard.com/report/github.com/shilangyu/typer-go)
4 | [](https://github.com/shilangyu/typer-go/actions)
5 |
6 | Test your typing speed in a Typer [TUI](https://en.wikipedia.org/wiki/Text-based_user_interface) game!
7 |
8 | - [typer-go](#typer-go)
9 | - [features](#features)
10 | - [install](#install)
11 | - [usage](#usage)
12 | - [settings](#settings)
13 | - [navigation](#navigation)
14 |
15 | ## features
16 |
17 | - collect statistics to track your progress
18 | - Words per minute
19 | - Timestamps
20 | - Amount of mistakes for each word
21 | - Amount of time for each word
22 | - play with your friends in local multiplayer (global coming soon)
23 | - customizable
24 | - comfort of your sweet sweet terminal
25 |
26 | ## install
27 |
28 | Grab an executable from the [release tab](https://github.com/shilangyu/typer-go/releases)
29 |
30 | ... or if you're an Gopher build from source (requires Go v1.13+):
31 |
32 | ```sh
33 | go get github.com/shilangyu/typer-go
34 | ```
35 |
36 | ## usage
37 |
38 | Just run `typer-go` in your terminal and the TUI will start. Full screen terminal is recommended. There are no CLI commands and flags.
39 |
40 | ## settings
41 |
42 | | name | values | description |
43 | | ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
44 | | highlight | `'background'` or `'text'` | The way the already typed text should be highlighted. `'background'` to fill the background, `'text'` to just fill the text. |
45 | | error display | `'typed'` or `'text'` | what should be shown when incorrect character is inputted. `'typed'` will show the typed char, `'text'` will show what should've been typed. |
46 | | texts path | any string | path to your custom typer texts where each text is separated by a new line. If path is empty, preloaded texts will be loaded. |
47 |
48 | ## navigation
49 |
50 | [~~The whole TUI has mouse support!~~](https://github.com/shilangyu/typer-go/issues/9)
51 |
52 | | key | description |
53 | | ----------------- | -------------------- |
54 | | ↑ | menu navigation up |
55 | | ↓ | menu navigation down |
56 | | enter | confirm |
57 | | esc | back |
58 | | tab | switch focus |
59 | | ctrl+c | exit |
60 |
--------------------------------------------------------------------------------
/game/default_texts.go:
--------------------------------------------------------------------------------
1 | package game
2 |
3 | // TyperTexts are a set of default typing texts
4 | const TyperTexts string = `
5 | im typing something
6 | `
7 |
--------------------------------------------------------------------------------
/game/events.go:
--------------------------------------------------------------------------------
1 | package game
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | const (
9 | // ChangeName is for changing usernames
10 | // payload = ID:new username
11 | ChangeName = "change-name"
12 | // ExitPlayer is for players leaving the game
13 | // payload = ID
14 | ExitPlayer = "exit-player"
15 | // StartGame is for starting the game
16 | // payload = unix timestamp of when it starts
17 | StartGame = "start-game"
18 | // Progress is for indicating typing progress
19 | // payload = ID:%
20 | Progress = "progress"
21 | )
22 |
23 | // Events is an array of all available events
24 | var Events = [...]string{ChangeName, ExitPlayer, StartGame, Progress}
25 |
26 | func split(s string) (string, string) {
27 | ss := strings.SplitN(s, ":", 2)
28 | return ss[0], ss[1]
29 | }
30 |
31 | // ExtractChangeName takes a payload and gives extracted data
32 | func ExtractChangeName(payload string) (ID, nickname string) {
33 | return split(payload)
34 | }
35 |
36 | // ExtractExitPlayer takes a payload and gives extracted data
37 | func ExtractExitPlayer(payload string) (ID string) {
38 | return payload
39 | }
40 |
41 | // ExtractStartGame takes a payload and gives extracted data
42 | func ExtractStartGame(payload string) (unixTimestamp int64) {
43 | unixTimestamp, _ = strconv.ParseInt(payload, 10, 64)
44 | return
45 | }
46 |
47 | // ExtractProgress takes a payload and gives extracted data
48 | func ExtractProgress(payload string) (ID string, progress int) {
49 | ID, p := split(payload)
50 | progress, _ = strconv.Atoi(p)
51 | return
52 | }
53 |
--------------------------------------------------------------------------------
/game/game.go:
--------------------------------------------------------------------------------
1 | package game
2 |
3 | import (
4 | "errors"
5 | "io/ioutil"
6 | "math/rand"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/shilangyu/typer-go/settings"
12 | )
13 |
14 | // ChooseText randomly chooses a text from the dataset
15 | func ChooseText() (string, error) {
16 | var texts []string
17 | rand.Seed(time.Now().UTC().UnixNano())
18 |
19 | if settings.I.TextsPath == "" {
20 | texts = strings.Split(strings.TrimSpace(TyperTexts), "\n")
21 | } else if _, err := os.Stat(settings.I.TextsPath); os.IsNotExist(err) {
22 | return "", errors.New("Didn't find typer texts, make sure your path is correct or leave it empty to load some preloaded texts")
23 | } else {
24 | bytes, err := ioutil.ReadFile(settings.I.TextsPath)
25 | if err != nil {
26 | return "", errors.New("Couldnt load the typer texts, make sure the permissions are correct")
27 | }
28 | texts = strings.Split(strings.TrimSpace(string(bytes)), "\n")
29 | }
30 |
31 | return texts[rand.Intn(len(texts))], nil
32 | }
33 |
34 | // Player holds information about an outer player
35 | type Player struct {
36 | // Nickname
37 | Nickname string
38 | // Progress
39 | Progress int
40 | }
41 |
42 | // Players is a helper for other players
43 | type Players map[string]*Player
44 |
45 | // Add adds or edits a player to the map
46 | func (p *Players) Add(ID, nickname string) {
47 | if _, ok := (*p)[ID]; !ok {
48 | (*p)[ID] = &Player{}
49 | }
50 | (*p)[ID].Nickname = nickname
51 | }
52 |
--------------------------------------------------------------------------------
/game/server.go:
--------------------------------------------------------------------------------
1 | package game
2 |
3 | import (
4 | "github.com/kanopeld/go-socket"
5 | )
6 |
7 | // NewServer initializes a server that just broadcasts all events
8 | func NewServer(port string) (*socket.Server, error) {
9 | s, err := socket.NewServer(":" + port)
10 | if err != nil {
11 | return nil, err
12 | }
13 | players := make(Players)
14 |
15 | s.On(socket.CONNECTION_NAME, func(c socket.Client) {
16 | c.On(ChangeName, func(data []byte) {
17 | ID, nickname := ExtractChangeName(string(data))
18 | players.Add(ID, nickname)
19 | c.Broadcast(ChangeName, data)
20 |
21 | for ID, p := range players {
22 | c.Emit(ChangeName, ID+":"+p.Nickname)
23 | }
24 | })
25 |
26 | onExit := func() {
27 | delete(players, c.ID())
28 | c.Broadcast(ExitPlayer, []byte(c.ID()))
29 | }
30 | c.On(socket.DISCONNECTION_NAME, onExit)
31 | c.On(ExitPlayer, onExit)
32 | })
33 |
34 | go s.Start()
35 |
36 | return s, nil
37 | }
38 |
--------------------------------------------------------------------------------
/game/state.go:
--------------------------------------------------------------------------------
1 | package game
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/shilangyu/typer-go/stats"
8 | )
9 |
10 | // State describes the state of a game
11 | type State struct {
12 | // CurrWord is an index to State.Words
13 | CurrWord int
14 | // Words contains the text split by spaces
15 | Words []string
16 | // StartTime is a timestamp of the first keystroke
17 | StartTime time.Time
18 | // properties concerning current word
19 | wordStart time.Time
20 | wordErrors int
21 | }
22 |
23 | // NewState initializes State
24 | func NewState(text string) *State {
25 | words := strings.Split(text, " ")
26 | for i := range words[:len(words)-1] {
27 | words[i] += " "
28 | }
29 |
30 | return &State{
31 | Words: words,
32 | }
33 | }
34 |
35 | // Start starts the mechanism
36 | func (s *State) Start() {
37 | s.StartTime = time.Now()
38 | s.wordStart = s.StartTime
39 | }
40 |
41 | // End ends the mechanism
42 | func (s *State) End() {
43 | stats.AddHistory(s.Wpm())
44 | stats.Save()
45 | }
46 |
47 | // Wpm is the words per minute
48 | func (s State) Wpm() float64 {
49 | return float64(s.CurrWord) / time.Since(s.StartTime).Minutes()
50 | }
51 |
52 | // Progress returns a float in the (0;1) range represending the progress made
53 | func (s State) Progress() float64 {
54 | return float64(s.CurrWord) / float64(len(s.Words))
55 | }
56 |
57 | // IncError increments the error count
58 | func (s *State) IncError() {
59 | s.wordErrors++
60 | }
61 |
62 | // NextWord saves stats of the current word and increments the counter
63 | func (s *State) NextWord() {
64 | stats.AddWord(s.Words[s.CurrWord], time.Since(s.wordStart), s.wordErrors)
65 | s.CurrWord++
66 |
67 | s.wordStart = time.Now()
68 | s.wordErrors = 0
69 | }
70 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/shilangyu/typer-go
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/gdamore/tcell v1.4.0
7 | github.com/kanopeld/go-socket v0.0.0-20191129220547-03aa20f2125c
8 | github.com/rivo/tview v0.0.0-20191121195645-2d957c4be01d
9 | gopkg.in/yaml.v2 v2.4.0
10 | )
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
2 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
3 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
4 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
5 | github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
6 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
7 | github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
8 | github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
9 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
10 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
11 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
12 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
13 | github.com/kanopeld/go-socket v0.0.0-20191129220547-03aa20f2125c h1:EOKxY4jGiQkldDKxh9bluB0WcxxLtd8/0+6OvA8pAWY=
14 | github.com/kanopeld/go-socket v0.0.0-20191129220547-03aa20f2125c/go.mod h1:TX1p6Ep6DDbMwMO4QDksv++0hlmL7mhfdyVUOvj5RO4=
15 | github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
16 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
17 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
18 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
19 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
20 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
21 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
22 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
23 | github.com/rivo/tview v0.0.0-20191121195645-2d957c4be01d h1:dPWYyMzc2VB5XX7eA/Pe5TXBGzhlVZZr54GhRJLTbts=
24 | github.com/rivo/tview v0.0.0-20191121195645-2d957c4be01d/go.mod h1:/rBeY22VG2QprWnEqG57IBC8biVu3i0DOIjRLc9I8H0=
25 | github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
26 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
27 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
28 | github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
29 | github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
30 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
31 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
32 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
34 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
35 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
36 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
37 | golang.org/x/sys v0.0.0-20191018095205-727590c5006e h1:ZtoklVMHQy6BFRHkbG6JzK+S6rX82//Yeok1vMlizfQ=
38 | golang.org/x/sys v0.0.0-20191018095205-727590c5006e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
40 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
41 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
42 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
43 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
44 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
47 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
48 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
49 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/gdamore/tcell"
7 | "github.com/rivo/tview"
8 | "github.com/shilangyu/typer-go/ui"
9 | "github.com/shilangyu/typer-go/utils"
10 | )
11 |
12 | func main() {
13 | tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
14 | // tview.Styles.PrimaryTextColor = tcell.ColorBlack
15 |
16 | app := tview.NewApplication()
17 | defer app.Stop()
18 |
19 | app.SetBeforeDrawFunc(func(s tcell.Screen) bool {
20 | s.Clear()
21 | return false
22 | })
23 |
24 | err := ui.CreateWelcome(app)
25 | utils.Check(err)
26 |
27 | if err := app.Run(); err != nil {
28 | log.Panicln(err)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/settings/enums.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | // Highlight describes how the text should be highlighted
4 | type Highlight int
5 |
6 | const (
7 | // HighlightBackground says the text should have a background highlight
8 | HighlightBackground Highlight = iota
9 | // HighlightText says the text should have a text highlight
10 | HighlightText
11 | )
12 |
13 | func (e Highlight) String() string {
14 | return []string{"background", "text"}[e]
15 | }
16 |
17 | // ErrorDisplay describes how incorrectly typed letters should be displayed
18 | type ErrorDisplay int
19 |
20 | const (
21 | // ErrorDisplayTyped says the typed letter should be displayed
22 | ErrorDisplayTyped ErrorDisplay = iota
23 | // ErrorDisplayText says the text letter should be displayed
24 | ErrorDisplayText
25 | )
26 |
27 | func (e ErrorDisplay) String() string {
28 | return []string{"typed", "text"}[e]
29 | }
30 |
--------------------------------------------------------------------------------
/settings/settings.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path"
7 |
8 | "github.com/shilangyu/typer-go/utils"
9 | "gopkg.in/yaml.v2"
10 | )
11 |
12 | type settings struct {
13 | Highlight Highlight
14 | ErrorDisplay ErrorDisplay
15 | TextsPath string
16 | }
17 |
18 | // I contains current settings properties from settings.yaml
19 | var I settings
20 | var settingsPath string
21 |
22 | func init() {
23 | userConfigDir, err := os.UserConfigDir()
24 | utils.Check(err)
25 |
26 | settingsPath = path.Join(userConfigDir, "typer-go", "settings.yaml")
27 | if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
28 | err := os.MkdirAll(path.Dir(settingsPath), os.ModePerm)
29 | utils.Check(err)
30 |
31 | file, err := os.Create(settingsPath)
32 | file.Close()
33 | utils.Check(err)
34 | Save()
35 | }
36 |
37 | content, err := ioutil.ReadFile(settingsPath)
38 | utils.Check(err)
39 |
40 | err = yaml.Unmarshal(content, &I)
41 | utils.Check(err)
42 | }
43 |
44 | // Save saves the current settings
45 | func Save() error {
46 | bytes, err := yaml.Marshal(I)
47 | if err != nil {
48 | return err
49 | }
50 | return ioutil.WriteFile(settingsPath, bytes, os.ModePerm)
51 | }
52 |
--------------------------------------------------------------------------------
/stats/stats.go:
--------------------------------------------------------------------------------
1 | package stats
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "os"
7 | "path"
8 | "strings"
9 | "time"
10 |
11 | "github.com/shilangyu/typer-go/utils"
12 | )
13 |
14 | // wordStat describes the errors concerning a word
15 | type wordStat struct {
16 | Duration time.Duration `json:"duration"`
17 | ErrorCount int `json:"errorCount"`
18 | }
19 |
20 | type history struct {
21 | Timestamp time.Time `json:"timestamp"`
22 | Wpm float64 `json:"wpm"`
23 | }
24 |
25 | type stats struct {
26 | History []history `json:"history"`
27 | Words map[string][]wordStat `json:"words"`
28 | }
29 |
30 | // I contains current statistics
31 | // its initialized because json.Marshal sees the properties
32 | // as null pointers not empty objects as it should
33 | var I = stats{
34 | History: []history{},
35 | Words: map[string][]wordStat{},
36 | }
37 | var statsPath string
38 |
39 | func init() {
40 | userConfigDir, err := os.UserConfigDir()
41 | utils.Check(err)
42 |
43 | statsPath = path.Join(userConfigDir, "typer-go", "stats.json")
44 | if _, err := os.Stat(statsPath); os.IsNotExist(err) {
45 | err := os.MkdirAll(path.Dir(statsPath), os.ModePerm)
46 | utils.Check(err)
47 |
48 | file, err := os.Create(statsPath)
49 | file.Close()
50 | utils.Check(err)
51 | Save()
52 | }
53 |
54 | content, err := ioutil.ReadFile(statsPath)
55 | utils.Check(err)
56 |
57 | err = json.Unmarshal(content, &I)
58 | utils.Check(err)
59 | }
60 |
61 | // Save saves the current statistics
62 | func Save() error {
63 | bytes, err := json.Marshal(I)
64 | if err != nil {
65 | return err
66 | }
67 | return ioutil.WriteFile(statsPath, bytes, os.ModePerm)
68 | }
69 |
70 | // AddHistory appends wpm with a timestamp to the history property
71 | func AddHistory(wpm float64) {
72 | I.History = append(I.History, history{time.Now(), wpm})
73 | }
74 |
75 | // AddWord appends stats about a word
76 | func AddWord(rawWord string, time time.Duration, errorCount int) {
77 | word := strings.TrimFunc(strings.ToLower(rawWord), func(ch rune) bool {
78 | return ch == ' ' || ch == '.' || ch == ':' || ch == '?' || ch == '!'
79 | })
80 | I.Words[word] = append(I.Words[word], wordStat{time, errorCount})
81 | }
82 |
--------------------------------------------------------------------------------
/ui/multiplayer.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/kanopeld/go-socket"
5 | "github.com/rivo/tview"
6 | "github.com/shilangyu/typer-go/game"
7 | "github.com/shilangyu/typer-go/utils"
8 | )
9 |
10 | type setup struct {
11 | RoomIP, Nickname, Port string
12 | IsServer bool
13 | // Server will be nil if IsServer is false
14 | Server *socket.Server
15 | Client socket.Client
16 | }
17 |
18 | // CreateMultiplayerSetup creates multiplayer room
19 | func CreateMultiplayerSetup(app *tview.Application) error {
20 | IP, _ := utils.IPv4()
21 | setup := setup{IP, "", "9001", true, nil, nil}
22 |
23 | formWi := tview.NewForm().
24 | AddInputField("Room IP", setup.RoomIP, 20, nil, func(text string) { setup.RoomIP = text }).
25 | AddInputField("Port", setup.Port, 20, nil, func(text string) { setup.Port = text }).
26 | AddCheckbox("Server", setup.IsServer, func(checked bool) { setup.IsServer = checked }).
27 | AddButton("CONNECT", func() {
28 | if setup.IsServer {
29 | var err error
30 | setup.Server, err = game.NewServer(setup.Port)
31 | utils.Check(err)
32 | }
33 |
34 | c, err := socket.NewDial(setup.RoomIP + ":" + setup.Port)
35 | utils.Check(err)
36 | setup.Client = c
37 |
38 | utils.Check(CreateMultiplayerRoom(app, setup))
39 | }).
40 | AddButton("CANCEL", func() {
41 | utils.Check(CreateWelcome(app))
42 | })
43 |
44 | app.SetRoot(Center(28, 11, formWi), true)
45 | keybindings(app, CreateWelcome)
46 | return nil
47 | }
48 |
49 | // CreateMultiplayerRoom creates multiplayer room
50 | func CreateMultiplayerRoom(app *tview.Application, setup setup) error {
51 | const maxNicknameLength int = 10
52 |
53 | players := make(game.Players)
54 |
55 | roomWi := tview.NewTextView()
56 | roomWi.SetBorder(true).SetTitle("ROOM")
57 | renderRoom := func() {
58 | ps := ""
59 | for _, p := range players {
60 | ps += p.Nickname + "\n"
61 | }
62 | app.QueueUpdateDraw(func() {
63 | roomWi.SetText(ps)
64 | })
65 | }
66 |
67 | setup.Client.On(socket.CONNECTION_NAME, func(ccc socket.Client) {
68 | setup.Client.On(game.ChangeName, func(payload string) {
69 | ID, nickname := game.ExtractChangeName(payload)
70 | players.Add(ID, nickname)
71 | renderRoom()
72 | })
73 | setup.Client.On(game.ExitPlayer, func(payload string) {
74 | ID := game.ExtractExitPlayer(payload)
75 | delete(players, ID)
76 | renderRoom()
77 | })
78 | setup.Client.Emit(game.ChangeName, setup.Client.ID()+":"+setup.Nickname)
79 | players[setup.Client.ID()] = &game.Player{Nickname: setup.Nickname}
80 | renderRoom()
81 | })
82 |
83 | formWi := tview.NewForm().
84 | AddInputField("Nickname", setup.Nickname, 20, func(textToCheck string, lastChar rune) bool {
85 | return len(textToCheck) <= maxNicknameLength
86 | }, func(text string) {
87 | setup.Nickname = text
88 | players[setup.Client.ID()].Nickname = setup.Nickname
89 | setup.Client.Emit(game.ChangeName, setup.Client.ID()+":"+setup.Nickname)
90 | }).
91 | AddButton("BACK", func() {
92 | utils.Check(CreateMultiplayerSetup(app))
93 | })
94 |
95 | layout := tview.NewFlex().
96 | SetDirection(tview.FlexRow).
97 | AddItem(tview.NewBox(), 0, 1, false).
98 | AddItem(tview.NewFlex().
99 | AddItem(Center(28, 11, formWi), 0, 1, true).
100 | AddItem(Center(28, 11, roomWi), 0, 1, false),
101 | 0, 1, true).
102 | AddItem(tview.NewBox(), 0, 1, false)
103 |
104 | app.SetRoot(layout, true)
105 | keybindings(app, func(app *tview.Application) error {
106 | setup.Client.Emit(game.ExitPlayer, setup.Client.ID())
107 | return CreateMultiplayerSetup(app)
108 | })
109 |
110 | return nil
111 | }
112 |
113 | // // CreateMultiplayer creates multiplayer screen widgets
114 | // func CreateMultiplayer(g *gocui.Gui) error {
115 | // text, err := game.ChooseText()
116 | // if err != nil {
117 | // return err
118 | // }
119 |
120 | // var state *game.State
121 | // if srv != nil {
122 | // srv.State = game.NewState(text)
123 | // state = srv.State
124 | // } else {
125 | // clt.State = game.NewState(text)
126 | // state = clt.State
127 | // }
128 |
129 | // w, h := g.Size()
130 |
131 | // statsFrameWi := widgets.NewCollection("multiplayer-stats", "STATS", false, 0, 0, w/5, h)
132 |
133 | // statWis := []*widgets.Text{
134 | // widgets.NewText("multiplayer-stats-wpm", "wpm: 0 ", false, false, 2, 1),
135 | // widgets.NewText("multiplayer-stats-time", "time: 0s ", false, false, 2, 2),
136 | // }
137 |
138 | // textFrameWi := widgets.NewCollection("multiplayer-text", "", false, w/5+1, 0, 4*w/5, 5*h/6+1)
139 |
140 | // points := []struct{ x, y int }{}
141 | // var textWis []*widgets.Text
142 | // for i, p := range points {
143 | // textWis = append(textWis, widgets.NewText("multiplayer-text-"+strconv.Itoa(i), state.Words[i], false, false, w/5+1+p.x, p.y))
144 | // }
145 |
146 | // progressWi := widgets.NewText("multiplayer-progress", strings.Repeat(" ", w/5), false, false, 1, h/2)
147 | // updateProgress := func() {
148 | // g.Update(progressWi.ChangeText(strings.Repeat("█", int(math.Floor(state.Progress()*float64(w/5))))))
149 | // }
150 |
151 | // var inputWi *widgets.Input
152 | // inputWi = widgets.NewInput("multiplayer-input", true, false, w/5+1, h-h/6, w-w/5-1, h/6, func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
153 | // if key == gocui.KeyEnter || len(v.Buffer()) == 0 && ch == 0 {
154 | // return false
155 | // }
156 |
157 | // if state.StartTime.IsZero() {
158 | // state.Start()
159 | // go func() {
160 | // ticker := time.NewTicker(100 * time.Millisecond)
161 | // for range ticker.C {
162 | // if state.CurrWord == len(state.Words) {
163 | // ticker.Stop()
164 | // return
165 | // }
166 |
167 | // g.Update(func(g *gocui.Gui) error {
168 | // err := statWis[1].ChangeText(
169 | // fmt.Sprintf("time: %.02fs", time.Since(state.StartTime).Seconds()),
170 | // )(g)
171 | // if err != nil {
172 | // return err
173 | // }
174 |
175 | // err = statWis[0].ChangeText(
176 | // fmt.Sprintf("wpm: %.0f", state.Wpm()),
177 | // )(g)
178 | // if err != nil {
179 | // return err
180 | // }
181 |
182 | // return nil
183 | // })
184 | // }
185 | // }()
186 | // }
187 |
188 | // gocui.DefaultEditor.Edit(v, key, ch, mod)
189 |
190 | // b := v.Buffer()[:len(v.Buffer())-1]
191 |
192 | // if ch != 0 && (len(b) > len(state.Words[state.CurrWord]) || rune(state.Words[state.CurrWord][len(b)-1]) != ch) {
193 | // state.IncError()
194 | // }
195 |
196 | // // ansiWord := state.PaintDiff(b)
197 |
198 | // // g.Update(textWis[state.CurrWord].ChangeText(ansiWord))
199 |
200 | // if b == state.Words[state.CurrWord] {
201 | // state.NextWord()
202 | // updateProgress()
203 | // if state.CurrWord == len(state.Words) {
204 | // state.End()
205 |
206 | // var popupWi *widgets.Modal
207 | // popupWi = widgets.NewModal("multiplayer-popup", "The end of the end\n is the end of times who craes", []string{"play", "quit"}, true, w/2, h/2, func(i int) {
208 | // popupWi.Layout(g)
209 | // }, func(i int) {
210 | // switch i {
211 | // case 0:
212 | // // CreateSingleplayer(g)
213 | // case 1:
214 | // // CreateWelcome(g)
215 | // }
216 | // })
217 | // g.Update(func(g *gocui.Gui) error {
218 | // popupWi.Layout(g)
219 | // popupWi.Layout(g)
220 | // g.SetCurrentView("multiplayer-popup")
221 | // g.SetViewOnTop("multiplayer-popup")
222 | // return nil
223 | // })
224 |
225 | // }
226 | // g.Update(inputWi.ChangeText(""))
227 | // }
228 |
229 | // return false
230 | // })
231 |
232 | // var wis []gocui.Manager
233 | // wis = append(wis, statsFrameWi)
234 | // for _, stat := range statWis {
235 | // wis = append(wis, stat)
236 | // }
237 | // wis = append(wis, textFrameWi)
238 | // for _, text := range textWis {
239 | // wis = append(wis, text)
240 | // }
241 | // wis = append(wis, inputWi)
242 | // wis = append(wis, progressWi)
243 |
244 | // g.SetManager(wis...)
245 |
246 | // g.Update(func(*gocui.Gui) error {
247 | // g.SetCurrentView("multiplayer-input")
248 | // return nil
249 | // })
250 |
251 | // return nil //keybindings(g, CreateMultiplayerSetup)
252 | // }
253 |
--------------------------------------------------------------------------------
/ui/settings.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/rivo/tview"
5 | "github.com/shilangyu/typer-go/settings"
6 | "github.com/shilangyu/typer-go/utils"
7 | )
8 |
9 | // CreateSettings creates a screen with settings
10 | func CreateSettings(app *tview.Application) error {
11 | settingsWi := tview.NewForm().
12 | AddDropDown(
13 | "highlight",
14 | []string{settings.HighlightBackground.String(), settings.HighlightText.String()},
15 | int(settings.I.Highlight),
16 | func(option string, index int) {
17 | settings.I.Highlight = settings.Highlight(index)
18 | settings.Save()
19 | }).
20 | AddDropDown(
21 | "error display",
22 | []string{settings.ErrorDisplayText.String(), settings.ErrorDisplayTyped.String()},
23 | int(settings.I.ErrorDisplay),
24 | func(option string, index int) {
25 | settings.I.ErrorDisplay = settings.ErrorDisplay(index)
26 | settings.Save()
27 | }).
28 | AddInputField(
29 | "texts path",
30 | settings.I.TextsPath,
31 | 20,
32 | nil,
33 | func(text string) {
34 | settings.I.TextsPath = text
35 | settings.Save()
36 | }).
37 | AddButton("DONE", func() { utils.Check(CreateWelcome(app)) })
38 |
39 | layout := tview.NewFlex().AddItem(Center(34, 10, settingsWi), 0, 1, true)
40 |
41 | app.SetRoot(layout, true)
42 | keybindings(app, CreateWelcome)
43 | return nil
44 | }
45 |
--------------------------------------------------------------------------------
/ui/singleplayer.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gdamore/tcell"
8 | "github.com/rivo/tview"
9 | "github.com/shilangyu/typer-go/game"
10 | "github.com/shilangyu/typer-go/settings"
11 | "github.com/shilangyu/typer-go/utils"
12 | )
13 |
14 | // CreateSingleplayer creates singleplayer screen widgets
15 | func CreateSingleplayer(app *tview.Application) error {
16 | text, err := game.ChooseText()
17 | if err != nil {
18 | return err
19 | }
20 | state := game.NewState(text)
21 |
22 | statsWis := [...]*tview.TextView{
23 | tview.NewTextView().SetText("wpm: 0"),
24 | tview.NewTextView().SetText("time: 0s"),
25 | }
26 |
27 | pages := tview.NewPages().
28 | AddPage("modal", tview.NewModal().
29 | SetText("Play again?").
30 | SetBackgroundColor(tcell.ColorDefault).
31 | AddButtons([]string{"yes", "exit"}).
32 | SetDoneFunc(func(index int, label string) {
33 | switch index {
34 | case 0:
35 | utils.Check(CreateSingleplayer(app))
36 | case 1:
37 | utils.Check(CreateWelcome(app))
38 | }
39 | }), false, false)
40 |
41 | var textWis []*tview.TextView
42 | for _, word := range state.Words {
43 | textWis = append(textWis, tview.NewTextView().SetText(word).SetDynamicColors(true))
44 | }
45 |
46 | currInput := ""
47 | inputWi := tview.NewInputField().
48 | SetFieldBackgroundColor(tcell.ColorDefault)
49 | inputWi.
50 | SetChangedFunc(func(text string) {
51 | if state.StartTime.IsZero() {
52 | state.Start()
53 | go func() {
54 | ticker := time.NewTicker(100 * time.Millisecond)
55 | for range ticker.C {
56 | if state.CurrWord == len(state.Words) {
57 | ticker.Stop()
58 | return
59 | }
60 | app.QueueUpdateDraw(func() {
61 | statsWis[0].SetText(fmt.Sprintf("wpm: %.0f", state.Wpm()))
62 | statsWis[1].SetText(fmt.Sprintf("time: %.02fs", time.Since(state.StartTime).Seconds()))
63 | })
64 | }
65 | }()
66 | }
67 |
68 | if len(currInput) < len(text) {
69 | if len(text) > len(state.Words[state.CurrWord]) || state.Words[state.CurrWord][len(text)-1] != text[len(text)-1] {
70 | state.IncError()
71 | }
72 | }
73 |
74 | app.QueueUpdateDraw(func(i int) func() {
75 | return func() {
76 | textWis[i].SetText(paintDiff(state.Words[i], text))
77 | }
78 | }(state.CurrWord))
79 |
80 | if text == state.Words[state.CurrWord] {
81 | state.NextWord()
82 | if state.CurrWord == len(state.Words) {
83 | state.End()
84 | pages.ShowPage("modal")
85 | } else {
86 | inputWi.SetText("")
87 | }
88 | }
89 |
90 | currInput = text
91 | })
92 |
93 | layout := tview.NewFlex()
94 | statsFrame := tview.NewFlex().SetDirection(tview.FlexRow)
95 | statsFrame.SetBorder(true).SetBorderPadding(1, 1, 1, 1).SetTitle("STATS")
96 | for _, statsWi := range statsWis {
97 | statsFrame.AddItem(statsWi, 1, 1, false)
98 | }
99 | layout.AddItem(statsFrame, 0, 1, false)
100 |
101 | secondColumn := tview.NewFlex().SetDirection(tview.FlexRow)
102 | textsLayout := tview.NewFlex()
103 | for _, textWi := range textWis {
104 | textsLayout.AddItem(textWi, len(textWi.GetText(true)), 1, false)
105 | }
106 | textsLayout.SetBorder(true)
107 | secondColumn.AddItem(textsLayout, 0, 3, false)
108 | inputWi.SetBorder(true)
109 | secondColumn.AddItem(inputWi, 0, 1, true)
110 | layout.AddItem(secondColumn, 0, 3, true)
111 |
112 | pages.AddPage("game", layout, true, true).SendToBack("game")
113 | app.SetRoot(pages, true)
114 |
115 | keybindings(app, CreateWelcome)
116 | return nil
117 | }
118 |
119 | // paintDiff returns an tview-painted string displaying the difference
120 | func paintDiff(toColor string, differ string) (colorText string) {
121 | var h string
122 | if settings.I.Highlight == settings.HighlightBackground {
123 | h = ":"
124 | }
125 |
126 | for i := range differ {
127 | if i >= len(toColor) || differ[i] != toColor[i] {
128 | colorText += "[" + h + "red]"
129 | } else {
130 | colorText += "[" + h + "green]"
131 | }
132 |
133 | switch settings.I.ErrorDisplay {
134 | case settings.ErrorDisplayText:
135 | colorText += string(differ[i])
136 | case settings.ErrorDisplayTyped:
137 | if i < len(toColor) {
138 | colorText += string(toColor[i])
139 | }
140 | }
141 | }
142 | colorText += "[-:-:-]"
143 |
144 | if len(differ) < len(toColor) {
145 | colorText += toColor[len(differ):]
146 | }
147 |
148 | return
149 | }
150 |
--------------------------------------------------------------------------------
/ui/ui.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/rivo/tview"
6 | "github.com/shilangyu/typer-go/utils"
7 | )
8 |
9 | func keybindings(app *tview.Application, goBack func(app *tview.Application) error) {
10 | if goBack != nil {
11 | app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
12 | if event.Key() == tcell.KeyEsc {
13 | app.QueueUpdateDraw(func() {
14 | utils.Check(goBack(app))
15 | })
16 | }
17 |
18 | return event
19 | })
20 | }
21 | }
22 |
23 | // Center returns a new primitive which shows the provided primitive in its
24 | // center, given the provided primitive's size.
25 | // credits: https://github.com/rivo/tview/blob/master/demos/presentation/center.go
26 | func Center(width, height int, p tview.Primitive) tview.Primitive {
27 | return tview.NewFlex().
28 | AddItem(tview.NewBox(), 0, 1, false).
29 | AddItem(tview.NewFlex().
30 | SetDirection(tview.FlexRow).
31 | AddItem(tview.NewBox(), 0, 1, false).
32 | AddItem(p, height, 1, true).
33 | AddItem(tview.NewBox(), 0, 1, false), width, 1, true).
34 | AddItem(tview.NewBox(), 0, 1, false)
35 | }
36 |
--------------------------------------------------------------------------------
/ui/welcome.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/rivo/tview"
5 | "github.com/shilangyu/typer-go/utils"
6 | )
7 |
8 | // CreateWelcome creates welcome screen widgets
9 | func CreateWelcome(app *tview.Application) error {
10 | const welcomeSign = `
11 | _
12 | | |_ _ _ _ __ ___ _ __ __ _ ___
13 | | __| | | | '_ \ / _ \ '__|____ / _ |/ _ \
14 | | |_| |_| | |_) | __/ | |_____| (_| | (_) |
15 | \__|\__, | .__/ \___|_| \__, |\___/
16 | |___/|_| |___/
17 | `
18 | signWi := tview.NewTextView().SetText(welcomeSign)
19 | menuWi := tview.NewList().
20 | AddItem("single player", "test your typing skills offline!", 0, func() {
21 | utils.Check(CreateSingleplayer(app))
22 | }).
23 | AddItem("multi player", "battle against other typers", 0, func() {
24 | utils.Check(CreateMultiplayerSetup(app))
25 | }).
26 | AddItem("stats", "TO BE RELEASED", 0, nil).
27 | AddItem("settings", "change app settings", 0, func() {
28 | utils.Check(CreateSettings(app))
29 | }).
30 | AddItem("exit", "exit the app", 0, func() {
31 | app.Stop()
32 | })
33 |
34 | signW, signH := utils.StringDimensions(welcomeSign)
35 | menuW, menuH := 32, menuWi.GetItemCount()*2
36 | layout := tview.NewFlex().
37 | SetDirection(tview.FlexRow).
38 | AddItem(tview.NewBox(), 0, 1, false).
39 | AddItem(Center(signW, signH, signWi), 0, 1, false).
40 | AddItem(Center(menuW, menuH, menuWi), 0, 1, true).
41 | AddItem(tview.NewBox(), 0, 1, false)
42 |
43 | app.SetRoot(layout, true)
44 | return nil
45 | }
46 |
--------------------------------------------------------------------------------
/utils/errors.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | // Check checks for the error and panics if there is one
4 | func Check(err error) {
5 | if err != nil {
6 | panic(err)
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/utils/strings.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "strings"
7 | )
8 |
9 | // StringDimensions returns the width and height of a string
10 | // where w = longest line, h = amount of lines
11 | func StringDimensions(s string) (w, h int) {
12 | text := strings.Split(s, "\n")
13 | h = len(text)
14 |
15 | for _, line := range text {
16 | if len(line) > w {
17 | w = len(line)
18 | }
19 | }
20 |
21 | return
22 | }
23 |
24 | // Center takes an array of strings and adds spaces to center them
25 | func Center(s []string) (res []string) {
26 | maxW, _ := StringDimensions(strings.Join(s, "\n"))
27 |
28 | for _, text := range s {
29 | diff := maxW - len(text)
30 | text = strings.Repeat(" ", diff/2) + text + strings.Repeat(" ", diff-diff/2)
31 | res = append(res, text)
32 | }
33 |
34 | return
35 | }
36 |
37 | // IPv4 returns users ipv4 as a string
38 | func IPv4() (string, error) {
39 | conn, err := net.Dial("udp", "8.8.8.8:80")
40 | if err != nil {
41 | return "", errors.New("Failed to determine your IP")
42 | }
43 | localAddr := conn.LocalAddr().(*net.UDPAddr)
44 | myIP := localAddr.IP.String()
45 | conn.Close()
46 | return myIP, nil
47 | }
48 |
--------------------------------------------------------------------------------