Initial commit

master
sloum 2 years ago
commit 6152fd1d24

1
.gitignore vendored

@ -0,0 +1 @@
fanorona

@ -0,0 +1,19 @@
# Fanorona
Fanorona is a game originating in Madagascar. [Wikipedia](https://en.wikipedia.org/wiki/Fanorona) has a good bit of information information about the game's history and rules.
![screen shot of fanorona being played, it is black's turn](fanorona.png)
## Building / Installing
You will need a [Go](https://golang.org) compiler. After that you can run `go build` from the repository directory to build the executable locally or `go install` to install it.
## Usage
By default, executing the program will put you into two player mode. Two player mode takes place on the same system (swapping keybaord or with one player dictating moves to the other for their turn). To play against an automated opponent you must pass the flag `-cpu`, as in: `fanorona -cpu`. The automated opponent is, as of this writing, not great. It plays decently in the early parts of the game but does not know how to finish the game well.
### Playing Moves
Moves are played as `[from]-[to]`, for example `A2-B3`. The letters are not case sensitive. If the move you play results in captured/dead stones you will be offered the opportunity to make a continuation play. The position you are at will be prepopulated, so you just need to add the `-[to]` part. If you are done for the turn and either cannot play a continuation or do not want to you may enter `PASS` or `DONE` (again, not case sensitive).

179
cpu.go

@ -0,0 +1,179 @@
package main
// FIXME
// Working:
// - Moving
// - Approach captures
// - Withdrawl captures
// - Choosing randomly from moves with equal points
// Broken:
// - Continuations moves
import (
"fmt"
"math/rand"
)
var weakDirs = []dir{{0,-1}, {0,1}, {1,0}, {-1,0}}
var strongDirs = []dir{{-1,-1}, {-1,1}, {1,1}, {1,-1}}
type point struct {
row int
col int
}
type score struct {
d dir
attacking bool
continuation bool
deadCount int
approach bool
}
func (s score) points() int {
count := 0
if s.continuation {
count += 3
}
return count + s.deadCount
}
type place struct {
row int
col int
strong bool
s []score
maxPts int
bestScore []int
}
func (p *place) FindBestDir() {
if len(p.s) == 0 {
return
}
for i, s := range p.s {
if s.points() > p.maxPts {
p.maxPts = s.points()
p.bestScore = []int{i}
} else if s.points() == p.maxPts {
p.bestScore = append(p.bestScore, i)
}
}
}
// FIXME This is broken
func ScoreContinuation(m move, g *Game, blacklist []point) (move, bool, error) {
p := place{m.toRow, m.toCol, g.board[m.toRow][m.toCol].strong, make([]score, 0, 8), 0, make([]int, 0, 7)}
p.Score(g)
outer: for _, s := range p.s {
if s.points() > 0 {
newPt := point{m.toRow+s.d.rowOff, m.toCol+s.d.colOff}
for _, bl := range blacklist {
if newPt.row == bl.row && newPt.col == bl.col {
continue outer
}
}
return move{m.toRow, m.toCol, newPt.row, newPt.col}, s.approach, nil
}
}
return move{}, false, fmt.Errorf("No valid continuation")
}
func (p *place) Score(g *Game) {
dirs := make([]dir, 0, 8)
dirs = append(dirs, weakDirs...)
if p.strong {
dirs = append(dirs, strongDirs...)
}
for _, d := range dirs {
m := move{p.row, p.col, p.row + d.rowOff, p.col + d.colOff}
err := g.ValidateMove(m)
if err != nil {
// This is not a valid move
continue
}
// Check continuation
hasContinuation := false
for _, d2 := range dirs {
if d2.rowOff == d.rowOff && d2.colOff == d.colOff {
// Cannot move in the same direction
continue
}
m2 := move{m.toRow, m.toCol, m.toRow + d2.rowOff, m.toCol + d2.colOff}
if g.withdrawAvailable(m2) || g.approachAvailable(m) {
hasContinuation = true
break
}
}
// Check Attack
canWithdraw := g.withdrawAvailable(m)
canApproach := g.approachAvailable(m)
if !canWithdraw && !canApproach {
p.s = append(p.s, score{d, false, false, 0, false})
}
if canWithdraw {
p.s = append(p.s, score{d, true, hasContinuation, g.CountPotentialDead(m, false), false})
}
if canApproach {
p.s = append(p.s, score{d, true, hasContinuation, g.CountPotentialDead(m, true), true})
}
}
p.FindBestDir()
}
type cpu struct {
legal []place
bestScore int
bestMove []int
}
func (c *cpu) PickMove(g *Game) (move, bool, bool, bool) {
c.Clear()
c.GetLegalMoves(g)
for i := range c.legal {
if c.legal[i].maxPts > c.bestScore {
c.bestScore = c.legal[i].maxPts
c.bestMove = []int{i}
} else if c.legal[i].maxPts == c.bestScore {
c.bestMove = append(c.bestMove, i)
}
}
bestM := rand.Intn(len(c.bestMove))
m := c.legal[c.bestMove[bestM]]
bestS := rand.Intn(len(m.bestScore))
return move{
m.row,
m.col,
m.row+m.s[m.bestScore[bestS]].d.rowOff,
m.col+m.s[m.bestScore[bestS]].d.colOff},
m.s[m.bestScore[bestS]].approach,
m.s[m.bestScore[bestS]].attacking,
m.s[m.bestScore[bestS]].continuation
}
func (c *cpu) Clear() {
c.legal = make([]place, 0, 15)
c.bestMove = make([]int, 0, 5)
c.bestScore = 0
}
func (c *cpu) GetLegalMoves(g *Game) {
for r, row := range g.board {
for p, pt := range row {
if pt.state == Empty || pt.state == Black {
continue
}
pl := place{r, p, g.board[r][p].strong, make([]score, 0, 8), -1, make([]int, 0, 5)}
pl.Score(g)
if len(pl.s) > 0 {
c.legal = append(c.legal, pl)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@ -0,0 +1,478 @@
package main
import (
"bufio"
"flag"
"fmt"
"math/rand"
"os"
"strings"
"time"
"git.rawtext.club/sloum/fanorona/qline"
"git.rawtext.club/sloum/fanorona/termios"
)
const (
Black int = iota
White
Empty
For rune = ''
Bak rune = '╲'
Ver rune = '│'
Hor string = "\033[38;5;234m─"
Wh string = "\033[38;5;255m█"
Bl string = "\033[38;5;234m█"
BoardBG string = "\033[48;5;215m"
ShadowBG string = "\033[48;5;95m"
)
var againstComputer bool = false
type pos struct {
strong bool // Strong if true, weak if not
state int // Valid: Black. Black, Empty
}
type move struct {
fromRow int
fromCol int
toRow int
toCol int
}
type dir struct {
rowOff int
colOff int
}
func (d dir) Equal(other dir) bool {
if d.rowOff == other.rowOff && d.colOff == other.colOff {
return true
}
return false
}
type Game struct {
board [5][9]pos
turn int
}
func MakeMove(s string) (move, error) {
s = strings.ToUpper(s)
o := move{}
n, err := fmt.Sscanf(s, "%1c%d-%1c%d", &o.fromRow, &o.fromCol, &o.toRow, &o.toCol)
if n < 2 || err != nil {
return o, fmt.Errorf("Invalid coordinate (%q)", s)
}
o.fromRow -= 65
o.toRow -= 65
o.fromCol -= 1
o.toCol -= 1
if o.fromRow > 4 || o.fromCol > 8 || o.fromRow < 0 || o.fromCol < 0 || o.toRow > 4 || o.toCol > 8 || o.toRow < 0 || o.toCol < 0 {
return o, fmt.Errorf("Invalid coordinate (%q)", s)
}
return o, nil
}
func (m move) Direction() dir {
return dir{m.toRow-m.fromRow, m.toCol-m.fromCol}
}
func (m move) String() (string, string) {
return fmt.Sprintf("%c%d", m.fromRow+65, m.fromCol+1), fmt.Sprintf("%c%d", m.toRow+65, m.toCol+1)
}
func (g *Game) ValidateMove(c move) error {
// Validate coordinates
if c.fromRow > 4 || c.fromCol > 8 || c.fromRow < 0 || c.fromCol < 0 || c.toRow > 4 || c.toCol > 8 || c.toRow < 0 || c.toCol < 0 {
return fmt.Errorf("Invalid coordinates")
}
// Validate the from position has a piece in it
if g.board[c.fromRow][c.fromCol].state == Empty {
from, _ := c.String()
return fmt.Errorf("There is no stone at %s", from)
}
// Validate that the correct piece is being moved
if g.board[c.fromRow][c.fromCol].state != g.turn {
return fmt.Errorf("That is not your stone to move")
}
// Validate that the move only moves one space away
if abs(c.fromRow-c.toRow) > 1 || abs(c.fromCol-c.toCol) > 1 {
return fmt.Errorf("You cannot move more than one space away")
}
// Validate that the new space is not occupied
if g.board[c.toRow][c.toCol].state != Empty {
return fmt.Errorf("You cannot move to an occupied space")
}
// Validate that a weak point does not try to perform a strong move
if (c.fromRow != c.toRow && c.fromCol != c.toCol) && !g.board[c.fromRow][c.fromCol].strong {
return fmt.Errorf("You cannot move diagonally from that space")
}
// TODO check if the move is non-capturing. If so, see if there
// are available capturing moves that could be played. This will
// be semi-involved as it will require checking all other available
// pieces by the same player
return nil
}
func (g *Game) MoveAndDestroyStones(m move) (int, error) {
dead := g.RemoveDeadStones(m, false, false)
g.board[m.toRow][m.toCol].state = g.turn
g.board[m.fromRow][m.fromCol].state = Empty
return dead, nil
}
func (g Game) withdrawAvailable(m move) bool {
d := m.Direction()
d.rowOff *= -1
d.colOff *= -1
nextRow, nextCol := m.fromRow+d.rowOff, m.fromCol+d.colOff
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
return false
}
if g.board[nextRow][nextCol].state == g.Enemy() {
return true
}
return false
}
func (g Game) approachAvailable(m move) bool {
d := m.Direction()
nextRow, nextCol := m.toRow+d.rowOff, m.toCol+d.colOff
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
return false
}
if g.board[nextRow][nextCol].state == g.Enemy() {
return true
}
return false
}
func (g Game) Enemy() int {
if g.turn == Black {
return White
}
return Black
}
func (g *Game) CountPotentialDead(m move, approach bool) int {
enemy := g.Enemy()
var count int
d := m.Direction()
var nextRow, nextCol int
if !approach {
nextRow, nextCol = m.fromRow, m.fromCol
d.rowOff *= -1
d.colOff *= -1
} else {
nextRow, nextCol = m.toRow, m.toCol
}
for {
nextRow += d.rowOff
nextCol += d.colOff
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
break
}
if g.board[nextRow][nextCol].state == enemy {
count++
} else {
break
}
}
return count
}
func (g *Game) RemoveDeadStones(m move, cpu bool, approach bool) int {
enemy := g.Enemy()
var count int
d := m.Direction()
var canWithdraw, canApproach bool
if !cpu {
canWithdraw = g.withdrawAvailable(m)
canApproach = g.approachAvailable(m)
if canWithdraw && canApproach {
request: for {
fmt.Println("(W)ithdraw or (A)pproach attack?")
k, _ := Getch()
switch k {
case 'w', 'W':
canApproach = false
case 'a', 'A':
canWithdraw = false
default:
continue
}
break request
}
} else if !canWithdraw && !canApproach {
// This is a non-capturing move, validating this should have happened in
// `validateAndMovePiece`, so we accept it here without checking
return 0
}
} else {
if approach {
canWithdraw = false
canApproach = true
} else {
canWithdraw = true
canApproach = false
}
}
var nextRow, nextCol int
if canWithdraw {
nextRow, nextCol = m.fromRow, m.fromCol
d.rowOff *= -1
d.colOff *= -1
} else {
nextRow, nextCol = m.toRow, m.toCol
}
for {
nextRow += d.rowOff
nextCol += d.colOff
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
break
}
if g.board[nextRow][nextCol].state == enemy {
count++
g.board[nextRow][nextCol].state = Empty
} else {
break
}
}
return count
}
func (g Game) GameOver() {
enemy := g.Enemy()
for _, row := range g.board {
for _, space := range row {
if space.state == enemy {
return
}
}
}
fmt.Printf("GAME OVER! %s wins!\n", g.Player())
termios.Restore()
os.Exit(0)
}
func (g *Game) String() string {
var out strings.Builder
out.WriteString("\033[2J\033[0;0H")
var interline strings.Builder
bak := false
for i, _ := range g.board {
interline.Reset()
interline.WriteString("\033[38;5;234m")
if i == 0 {
out.WriteString(fmt.Sprintf(" \033[38;5;95m▟\033[0m%s%*.*s\033[0m\n", ShadowBG, 19, 19, ""))
out.WriteString(fmt.Sprintf(" %s%*.*s%s %s\033[0m\n", BoardBG, 19, 19, "", ShadowBG, BoardBG))
}
if i % 2 == 0 {
bak = true
} else {
bak = false
}
for y, p := range g.board[i] {
if y == 0 {
out.WriteString(fmt.Sprintf("\033[1m%c\033[0m", rune(i+65)))
out.WriteString(fmt.Sprintf("%s ", BoardBG))
interline.WriteString(fmt.Sprintf(" %s ", BoardBG))
}
switch p.state {
case Black:
out.WriteString(Bl)
case White:
out.WriteString(Wh)
default:
out.WriteRune(' ')
}
interline.WriteRune(Ver)
if y < 8 {
out.WriteString(Hor)
if bak {
interline.WriteRune(Bak)
} else {
interline.WriteRune(For)
}
bak = !bak
} else {
out.WriteString(fmt.Sprintf(" %s %s\033[0m\n", ShadowBG, BoardBG))
interline.WriteString(fmt.Sprintf(" %s %s\033[0m\n", ShadowBG, BoardBG))
}
}
if i < 4 {
out.WriteString(interline.String())
} else {
out.WriteString(fmt.Sprintf(" %s%*.*s\033[0m\033[38;5;95m▛\033[0m\n", BoardBG, 19, 19, ""))
}
}
out.WriteString("\033[0m\033[1m 1 2 3 4 5 6 7 8 9\033[0m\n\n")
return out.String()
}
func Getch() (rune, error) {
reader := bufio.NewReader(os.Stdin)
return qline.ReadKey(reader)
}
func (g Game) Player() string {
if g.turn == Black {
return "Black"
}
return "White"
}
func (g *Game) TakeTurn() {
var input string
var m move
var err error
firstMove := true
cont := ""
piece := ""
spaceList := make(map[string]bool)
var lastDir dir
for {
fmt.Print(g.String())
prompt := fmt.Sprintf("%s %s> ", g.Player(), cont)
input = qline.GetInput(prompt, piece, len(prompt)+10)
fmt.Print("\n")
if lowIn := strings.ToLower(input); strings.HasPrefix("pass", lowIn) || strings.HasPrefix("done", lowIn) {
if firstMove {
ShowError("Cannot pass before making any moves on your turn")
continue
}
break
}
m, err = MakeMove(input)
if err != nil {
ShowError(err.Error())
continue
}
fromString, toString := m.String()
if !firstMove && fromString != piece {
ShowError("Cannot switch pieces mid-turn")
continue
}
if _, ok := spaceList[toString]; ok {
ShowError("Cannot revisit a space on the same turn")
continue
}
if !firstMove && lastDir.Equal(m.Direction()) {
ShowError("Cannot move twice in the same direction")
continue
}
err := g.ValidateMove(m)
if err != nil {
ShowError(err.Error())
continue
}
dead, err := g.MoveAndDestroyStones(m)
if err != nil {
ShowError(err.Error())
continue
}
if firstMove {
firstMove = false
spaceList[fromString] = true
cont = "(continuing) "
}
spaceList[toString] = true
piece = toString
lastDir = m.Direction()
if dead == 0 {
break
}
}
}
func ShowError(e string) {
fmt.Printf("\033[1m\033[38;5;124m%s\033[0m\n...Press any key to continue...\n", e)
Getch()
}
func (g *Game) Play() {
var c cpu
for {
if g.Player() == "Black" || !againstComputer {
g.TakeTurn()
} else {
fmt.Print(g.String())
m, approach, attacking, continuation := c.PickMove(g)
if attacking {
g.RemoveDeadStones(m, true, approach)
}
g.board[m.toRow][m.toCol].state = g.turn
g.board[m.fromRow][m.fromCol].state = Empty
time.Sleep(time.Second * 1)
fmt.Print(g.String())
if continuation {
var cont move
var err error
cont = m
bl := make([]point, 2, 5)
bl[0] = point{m.fromRow, m.fromCol}
bl[1] = point{m.toRow, m.toCol}
for {
cont, approach, err = ScoreContinuation(cont, g, bl)
if err != nil {
break
}
bl = append(bl, point{cont.toRow, cont.toCol})
g.RemoveDeadStones(cont, true, approach)
g.board[cont.toRow][cont.toCol].state = g.turn
g.board[cont.fromRow][cont.fromCol].state = Empty
time.Sleep(time.Second * 1)
fmt.Print(g.String())
}
}
}
g.GameOver()
g.turn = abs(g.turn-1)
}
}
func abs(n int) int {
if n < 0 {
n *= -1
}
return n
}
var b = [5][9]pos {
{pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black},pos{true, Black}},
{pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}},
{pos{true, Black}, pos{false, White}, pos{true, Black}, pos{false, White}, pos{true, Empty}, pos{false, Black}, pos{true, White}, pos{false, Black},pos{true, White}},
{pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}},
{pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White},pos{true, White}},
}
func main() {
termios.SetCharMode()
defer termios.Restore()
flag.BoolVar(&againstComputer, "cpu", false, "Play against a computer oponent")
flag.Parse()
rand.Seed(time.Now().UnixNano())
g := Game{b, Black}
g.Play()
}

@ -0,0 +1,286 @@
package qline
// qline is a line input library for terminals that utilize
// vt100 compatible escape sequences
//
// Copyright © 2021 Brian Evans
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the “Software”), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import (
"bufio"
"fmt"
"os"
"strings"
)
const (
UpArrow rune = iota - 20
DownArrow
LeftArrow
RightArrow
Delete
Home
End
PageUp
PageDown
Escape rune = 27
NewLine rune = 10
CarriageReturn rune = 13
BackSpace rune = 127
)
var (
width int
)
type buffer struct {
buf []rune
cursor int
maxWidth int
offset int
cursorStart int
prompt string
}
func GetInput(prompt string, content string, cols int) string {
b := buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt}
fmt.Printf("\033[1m%s\033[22m", prompt)
var ch rune
var err error
reader := bufio.NewReader(os.Stdin)
b.seedContent(content)
b.printBuf()
for {
ch, err = ReadKey(reader)
if err != nil {
continue
}
if ch == CarriageReturn || ch == NewLine {
break
}
if isControl(ch) {
b.controlInput(ch)
} else {
b.addChar(ch, true)
}
}
return b.string()
}
func (lb buffer) string() string {
return string(lb.buf)
}
func (lb *buffer) deleteChar() {
if lb.cursor == len(lb.buf) {
return
} else if lb.cursor == len(lb.buf)-1 {
lb.buf = lb.buf[:len(lb.buf)-1]
} else {
lb.buf = append(lb.buf[:lb.cursor], lb.buf[lb.cursor+1:]...)
}
if lb.offset > 0 {
lb.offset--
}
}
func (lb *buffer) addChar(c rune, echo bool) {
if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) {
return
}
if lb.cursor == len(lb.buf) {
lb.buf = append(lb.buf, c)
lb.cursor++
} else {
lb.buf = append(lb.buf[:lb.cursor+1], lb.buf[lb.cursor:]...)
lb.buf[lb.cursor] = c
lb.cursor++
}
if lb.cursor - lb.offset > lb.maxWidth {
lb.offset++
}
if echo {
lb.printBuf()
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (lb buffer) printBuf() {
out := lb.buf[lb.offset:min(len(lb.buf), lb.offset+lb.maxWidth)]
fmt.Printf("\r%s%s\033[0K\r\033[%dC", lb.prompt, string(out), lb.cursor - lb.offset + len(lb.prompt))
}
func (lb *buffer) controlInput(c rune) {
switch c {
case Delete:
lb.deleteChar()
case BackSpace:
if lb.cursor > 0 {
lb.cursor--
lb.deleteChar()
}
case LeftArrow:
if lb.offset > 0 && lb.cursor - lb.offset == 0 {
lb.offset--
}
if lb.cursor > 0 {
lb.cursor--
}
case RightArrow:
// This is still mildly funky, but works enough
for ;lb.cursor - lb.offset >= lb.maxWidth && lb.cursor < len(lb.buf); {
lb.offset++
}
if lb.cursor < len(lb.buf) {
lb.cursor++
}
case Home:
lb.offset = 0
lb.cursor = 0
case End:
lb.cursor = len(lb.buf)
lb.offset = max(lb.cursor - lb.maxWidth, 0)
}
lb.printBuf()
}
func (lb *buffer) seedContent(s string) {
for _, r := range s {
lb.addChar(r, false)
}
}
func parseCursorPosition(esc string) (int, int, error) {
var row, col int
r := strings.NewReader(esc)
_, err := fmt.Fscanf(r, "\033[%d;%dR", &row, &col)
return row, col, err
}
func ReadKey(reader *bufio.Reader) (rune, error) {
char, _, err := reader.ReadRune()
if err != nil {
return 0, err
}
avail := reader.Buffered()
if char == Escape && avail > 0 {
var b strings.Builder
b.WriteRune(27)
for ; avail > 0; avail-- {
c, _, e := reader.ReadRune()
if e != nil {
break
}
b.WriteRune(c)
}
escSeq := b.String()
switch true {
case escSeq == "\033[A":
char = UpArrow
case escSeq == "\033[B":
char = DownArrow
case escSeq == "\033[C":
char = RightArrow
case escSeq == "\033[D":
char = LeftArrow
case escSeq == "\033[5~":
char = PageUp
case escSeq == "\033[6~":
char = PageDown
case isHomeKey(escSeq):
char = Home
case isEndKey(escSeq):
char = End
case isDeleteKey(escSeq):
char = Delete
case escSeq[len(escSeq)-1] == 'R':
// This is a request for cursor position
_, cols, err := parseCursorPosition(escSeq)
if err == nil {
err = fmt.Errorf("response")
}
return rune(cols), err
}
}
return char, nil
}
func isControl(c rune) bool {
switch c {
case UpArrow, DownArrow, RightArrow, LeftArrow, PageUp, PageDown, Home, End, Delete, BackSpace, CarriageReturn, NewLine, Escape:
return true
default:
return false
}
}
func isDeleteKey(seq string) bool {
switch seq {
case "\033[3~", "\033[P":
return true
default:
return false
}
}
func isHomeKey(seq string) bool {
switch seq {
case "\033[1~", "\033[7~", "\033[H", "\033OH":
return true
default:
return false
}
}
func isEndKey(seq string) bool {
switch seq {
case "\033[4~", "\033[8~", "\033[F", "\033OF":
return true
default:
return false
}
}

@ -0,0 +1,10 @@
// +build linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TCGETS
setTermiosIoctl = syscall.TCSETS
)

@ -0,0 +1,10 @@
// +build !linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TIOCGETA
setTermiosIoctl = syscall.TIOCSETAF
)

@ -0,0 +1,65 @@
package termios
import (
"os"
"runtime"
"syscall"
"unsafe"
)
type winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
var fd = os.Stdin.Fd()
var initial = getTermios()
func ioctl(fd, request, argp uintptr) error {
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, request, argp); e != 0 {
return e
}
return nil
}
func GetWindowSize() (int, int) {
var value winsize
ioctl(fd, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&value)))
return int(value.Col), int(value.Row)
}
func getTermios() syscall.Termios {
var value syscall.Termios
err := ioctl(fd, getTermiosIoctl, uintptr(unsafe.Pointer(&value)))
if err != nil {
panic(err)
}
return value
}
func setTermios(termios syscall.Termios) {
err := ioctl(fd, setTermiosIoctl, uintptr(unsafe.Pointer(&termios)))
if err != nil {
panic(err)
}
runtime.KeepAlive(termios)
}
func SetCharMode() {
t := getTermios()
t.Lflag = t.Lflag ^ syscall.ICANON
t.Lflag = t.Lflag ^ syscall.ECHO
setTermios(t)
}
func SetLineMode() {
var t = getTermios()
t.Lflag = t.Lflag | (syscall.ICANON | syscall.ECHO)
setTermios(t)
}
func Restore() {
setTermios(initial)
}
Loading…
Cancel
Save