Intial commit
This commit is contained in:
parent
71a7f8f306
commit
003921c2d5
31
flock/flock.go
Normal file
31
flock/flock.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// The flock package provides a file-system mediated locking mechanism on linux
|
||||||
|
// using the `flock` system call.
|
||||||
|
|
||||||
|
package flock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Lock gets an exclusive lock on the file at the given path. If the file
|
||||||
|
// doesn't exist, it's created.
|
||||||
|
func Lock(path string) (*os.File, error) {
|
||||||
|
perm := os.O_CREATE | os.O_RDWR
|
||||||
|
fh, err := os.OpenFile(path, perm, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syscall.Flock(int(fh.Fd()), syscall.LOCK_EX); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock releases the lock acquired via the Lock function.
|
||||||
|
func Unlock(f *os.File) error {
|
||||||
|
return f.Close()
|
||||||
|
}
|
43
flock/flock_test.go
Normal file
43
flock/flock_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package flock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Lock_basic(t *testing.T) {
|
||||||
|
ch := make(chan int, 1)
|
||||||
|
f, err := Lock("/tmp/fsutil-test-lock")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
ch <- 10
|
||||||
|
Unlock(f)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case x := <-ch:
|
||||||
|
t.Fatal(x)
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = Lock("/tmp/fsutil-test-lock")
|
||||||
|
select {
|
||||||
|
case i := <-ch:
|
||||||
|
if i != 10 {
|
||||||
|
t.Fatal(i)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("No value available.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Lock_badPath(t *testing.T) {
|
||||||
|
_, err := Lock("./dne/file.lock")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
53
fsutil/fsutil.go
Normal file
53
fsutil/fsutil.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package fsutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return true if os.Stat doesn't return an error.
|
||||||
|
func PathExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if os.Stat doesn't return an error and the path is a regular file.
|
||||||
|
func FileExists(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fi.Mode().IsRegular()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if os.Stat doesn't return an error and the path is a directory.
|
||||||
|
func DirExists(path string) bool {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fi.Mode().IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a file atomically on a POSIX file system.
|
||||||
|
func WriteFileAtomic(data []byte, tmpPath, finalPath string) error {
|
||||||
|
f, err := os.Create(tmpPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := f.Write(data); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Sync(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Rename(tmpPath, finalPath)
|
||||||
|
}
|
62
fsutil/fsutil_test.go
Normal file
62
fsutil/fsutil_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package fsutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPathExists(t *testing.T) {
|
||||||
|
if !PathExists(".") {
|
||||||
|
t.Fatal(".")
|
||||||
|
}
|
||||||
|
if PathExists("./i-dont-exist") {
|
||||||
|
t.Fatal("2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileExists(t *testing.T) {
|
||||||
|
if FileExists(".") {
|
||||||
|
t.Fatal(".")
|
||||||
|
}
|
||||||
|
if FileExists("./i-dont-exist") {
|
||||||
|
t.Fatal("2")
|
||||||
|
}
|
||||||
|
if !FileExists("fsutil.go") {
|
||||||
|
t.Fatal("fsutil.go")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirExists(t *testing.T) {
|
||||||
|
if !DirExists(".") {
|
||||||
|
t.Fatal(".")
|
||||||
|
}
|
||||||
|
if DirExists("./i-dont-exist") {
|
||||||
|
t.Fatal("2")
|
||||||
|
}
|
||||||
|
if DirExists("fsutil.go") {
|
||||||
|
t.Fatal("fsutil.go")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFileAtomic(t *testing.T) {
|
||||||
|
dir, err := ioutil.TempDir("", "123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, "some-file.dat")
|
||||||
|
data := []byte("Hello world!")
|
||||||
|
|
||||||
|
if err := WriteFileAtomic(data, path+".new", path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(b) != "Hello world!" {
|
||||||
|
t.Fatal(b)
|
||||||
|
}
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module git.crumpington.com/public/toolbox
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||||
|
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
10
tui/escapes.go
Normal file
10
tui/escapes.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
var (
|
||||||
|
EscapeNormal = []byte{27, '[', '0', 'm'}
|
||||||
|
EscapeBold = []byte{27, '[', '1', 'm'}
|
||||||
|
EscapeFaint = []byte{27, '[', '2', 'm'}
|
||||||
|
EscapeItalic = []byte{27, '[', '3', 'm'}
|
||||||
|
EscapeUnderline = []byte{27, '[', '4', 'm'}
|
||||||
|
EscapeInvert = []byte{27, '[', '7', 'm'}
|
||||||
|
)
|
54
tui/input.go
Normal file
54
tui/input.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Readline reads a line of input from the user.
|
||||||
|
func Readline(prompt string) string {
|
||||||
|
t.SetPrompt(prompt)
|
||||||
|
s, err := t.ReadLine()
|
||||||
|
must(err)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads a line of input from the user, but doesn't echo to the
|
||||||
|
// screen.
|
||||||
|
func ReadPassword(prompt string) string {
|
||||||
|
s, err := t.ReadPassword(prompt)
|
||||||
|
must(err)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit the string s using the editor defined by the environment variable
|
||||||
|
// EDITOR.
|
||||||
|
func EditInEditor(s string) (string, error) {
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
rand.Read(buf)
|
||||||
|
path := filepath.Join(os.TempDir(), fmt.Sprintf("tui.%x", buf))
|
||||||
|
defer os.RemoveAll(path)
|
||||||
|
if err := os.WriteFile(path, []byte(s), 0600); err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
editor = "emacs"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(editor, path)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
35
tui/output.go
Normal file
35
tui/output.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clear clears the terminal.
|
||||||
|
func Clear() {
|
||||||
|
_, h := TermSize()
|
||||||
|
buf := make([]byte, 2*h)
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = '\n'
|
||||||
|
}
|
||||||
|
mustWrite(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write sends raw bytes to the terminal.
|
||||||
|
func Write(b []byte) {
|
||||||
|
mustWrite(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Print(a ...interface{}) {
|
||||||
|
s := fmt.Sprint(a...)
|
||||||
|
mustWrite([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Println(a ...interface{}) {
|
||||||
|
s := fmt.Sprintln(a...)
|
||||||
|
mustWrite([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Printf(format string, a ...interface{}) {
|
||||||
|
s := fmt.Sprintf(format, a...)
|
||||||
|
mustWrite([]byte(s))
|
||||||
|
}
|
68
tui/stringutil.go
Normal file
68
tui/stringutil.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Split text into words or newlines.
|
||||||
|
func splitText(s string) [][]rune {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
r := []rune(s)
|
||||||
|
out := [][]rune{}
|
||||||
|
|
||||||
|
nextWord := func() []rune {
|
||||||
|
for i := range r {
|
||||||
|
if unicode.IsSpace(r[i]) {
|
||||||
|
ret := r[:i]
|
||||||
|
r = r[i:]
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret := r
|
||||||
|
r = r[:0]
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSpaces := func() []rune {
|
||||||
|
for i := range r {
|
||||||
|
if !unicode.IsSpace(r[i]) {
|
||||||
|
ret := r[:i]
|
||||||
|
r = r[i:]
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Code should never reach these lines.
|
||||||
|
ret := r
|
||||||
|
r = r[:0]
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(r) > 0 {
|
||||||
|
word := nextWord()
|
||||||
|
if len(word) != 0 {
|
||||||
|
out = append(out, word)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
spaces := nextSpaces()
|
||||||
|
count := 0
|
||||||
|
for _, x := range spaces {
|
||||||
|
if x == '\n' {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if count > 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 1 {
|
||||||
|
out = append(out, []rune{'\n'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
49
tui/tui.go
Normal file
49
tui/tui.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lock = sync.Mutex{}
|
||||||
|
t *term.Terminal
|
||||||
|
oldState *term.State
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if t != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
screen := struct {
|
||||||
|
io.Reader
|
||||||
|
io.Writer
|
||||||
|
}{os.Stdin, os.Stdout}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
oldState, err = term.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t = term.NewTerminal(screen, "")
|
||||||
|
t.SetSize(80, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
term.Restore(int(os.Stdin.Fd()), oldState)
|
||||||
|
t = nil
|
||||||
|
oldState = nil
|
||||||
|
}
|
23
tui/util.go
Normal file
23
tui/util.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TermSize() (w, h int) {
|
||||||
|
w, h, _ = term.GetSize(int(os.Stdin.Fd()))
|
||||||
|
return w, h
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustWrite(buf []byte) {
|
||||||
|
_, err := t.Write(buf)
|
||||||
|
must(err)
|
||||||
|
}
|
60
tui/wordwrap.go
Normal file
60
tui/wordwrap.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// WrapText takes the text in s and wraps it to the width. It returns the
|
||||||
|
// individual wrapped lines, and the length of the longest wrapped line.
|
||||||
|
//
|
||||||
|
// If the width is less than or equal to zero, it is set to the terminal width.
|
||||||
|
//
|
||||||
|
// In order to wrap the text in a reasonable way, white space is trimmed and
|
||||||
|
// collapsed througout the text.
|
||||||
|
func WrapText(s string, width int) ([]string, int) {
|
||||||
|
w := width
|
||||||
|
if w <= 0 {
|
||||||
|
w, _ = TermSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
maxLine := 0
|
||||||
|
parts := splitText(s)
|
||||||
|
|
||||||
|
nextLine := func() (string, int) {
|
||||||
|
if parts[0][0] == '\n' {
|
||||||
|
parts = parts[1:]
|
||||||
|
return "", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
words := append([]string{}, string(parts[0]))
|
||||||
|
length := len(parts[0])
|
||||||
|
parts = parts[1:]
|
||||||
|
|
||||||
|
for len(parts) > 0 {
|
||||||
|
p := parts[0]
|
||||||
|
|
||||||
|
if p[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if length+len(p)+1 > w {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
length += len(p) + 1
|
||||||
|
words = append(words, string(p))
|
||||||
|
parts = parts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(words, " "), length
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, 0, 2)
|
||||||
|
for len(parts) > 0 {
|
||||||
|
line, lineLen := nextLine()
|
||||||
|
if lineLen > maxLine {
|
||||||
|
maxLine = lineLen
|
||||||
|
}
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, maxLine
|
||||||
|
}
|
56
tui/wordwrap_test.go
Normal file
56
tui/wordwrap_test.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrapText(t *testing.T) {
|
||||||
|
type TestCase struct {
|
||||||
|
In string
|
||||||
|
W int
|
||||||
|
Lines []string
|
||||||
|
Width int
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []TestCase{
|
||||||
|
{
|
||||||
|
In: "The rain in spain falls mainly in the plane.",
|
||||||
|
W: 9,
|
||||||
|
Lines: []string{
|
||||||
|
"The rain",
|
||||||
|
"in spain",
|
||||||
|
"falls",
|
||||||
|
"mainly in",
|
||||||
|
"the",
|
||||||
|
"plane.",
|
||||||
|
},
|
||||||
|
Width: 9,
|
||||||
|
}, {
|
||||||
|
In: "The\n\nrain in spain falls mainly in the plane.",
|
||||||
|
W: 9,
|
||||||
|
Lines: []string{
|
||||||
|
"The",
|
||||||
|
"",
|
||||||
|
"rain in",
|
||||||
|
"spain",
|
||||||
|
"falls",
|
||||||
|
"mainly in",
|
||||||
|
"the",
|
||||||
|
"plane.",
|
||||||
|
},
|
||||||
|
Width: 9,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
lines, width := WrapText(tc.In, tc.W)
|
||||||
|
if !reflect.DeepEqual(lines, tc.Lines) {
|
||||||
|
t.Fatalf("%#v %#v", lines, tc.Lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
if width != tc.Width {
|
||||||
|
t.Fatal(width, tc.Width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user