feat: Initial working version of tmux package

This commit is contained in:
2019-12-09 07:56:52 +00:00
commit 6f353d6d16
13 changed files with 471 additions and 0 deletions

16
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
- name: Check out the code
uses: actions/checkout@v1
- name: Run all tests
run: make test

18
LICENSE Normal file
View File

@@ -0,0 +1,18 @@
Copyright (c) 2019 Jim Myhrberg
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.

5
Makefile Normal file
View File

@@ -0,0 +1,5 @@
default: test
.PHONY: test
test:
go test -v ./...

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# go-tmux
A small Go package for executing [Tmux](https://github.com/tmux/tmux)
commands. It was created as a helper package for running the test suite in
[tmux-themepack](https://github.com/jimeh/tmux-themepack).

11
exec_runner.go Normal file
View File

@@ -0,0 +1,11 @@
package tmux
import (
"os/exec"
)
type ExecRunner struct{}
func (r *ExecRunner) Run(command string, args ...string) ([]byte, error) {
return exec.Command(command, args...).CombinedOutput()
}

30
exec_runner_test.go Normal file
View File

@@ -0,0 +1,30 @@
package tmux
import (
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExecRunnerRun(t *testing.T) {
tests := []struct {
command string
args []string
}{
{command: "pwd"},
{command: "hostname"},
{command: "uname"},
}
for _, tt := range tests {
runner := &ExecRunner{}
expected, err := exec.Command(tt.command, tt.args...).CombinedOutput()
assert.NoError(t, err)
actual, err := runner.Run(tt.command, tt.args...)
assert.NoError(t, err)
assert.Equal(t, expected, actual)
}
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/jimeh/go-tmux
go 1.13
require github.com/stretchr/testify v1.4.0

11
go.sum Normal file
View File

@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

33
options_scope.go Normal file
View File

@@ -0,0 +1,33 @@
package tmux
// OptionsScope represents one of the five scopes that Tmux holds options
// within.
type OptionsScope int
const (
Server OptionsScope = iota + 1
GlobalSession
Session
GlobalWindow
Window
)
// OptionsScopeFlags converts a given OptionsScope to the command line flags
// needed to restrict "set-option" and "show-options" commands to the scope in
// question.
func OptionsScopeFlags(scope OptionsScope) string {
switch scope {
case 0, Session:
return ""
case Server:
return "-s"
case GlobalSession:
return "-g"
case GlobalWindow:
return "-gw"
case Window:
return "-w"
default:
return ""
}
}

27
options_scope_test.go Normal file
View File

@@ -0,0 +1,27 @@
package tmux
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOptionsScopeFlags(t *testing.T) {
tests := []struct {
scope OptionsScope
flags string
}{
{0, ""},
{Server, "-s"},
{GlobalSession, "-g"},
{Session, ""},
{GlobalWindow, "-gw"},
{Window, "-w"},
{38404, ""},
{934, ""},
}
for _, tt := range tests {
assert.Equal(t, tt.flags, OptionsScopeFlags(tt.scope))
}
}

5
runner.go Normal file
View File

@@ -0,0 +1,5 @@
package tmux
type Runner interface {
Run(string, ...string) ([]byte, error)
}

81
tmux.go Normal file
View File

@@ -0,0 +1,81 @@
package tmux
import (
"bufio"
"bytes"
"regexp"
)
var optMatcher = regexp.MustCompile(`^\s*([@\w-][\w-]+)\s+(.*)$`)
var quote = []byte(`"`)
// Tmux enables easily running tmux commands.
type Tmux struct {
BinPath string
SocketName string
SocketPath string
Runner Runner
}
// New returns a Tmux objects with a Runner capable of executing shell commands.
func New() *Tmux {
return &Tmux{Runner: &ExecRunner{}}
}
// Exec runs the given tmux command.
func (s *Tmux) Exec(args ...string) ([]byte, error) {
args = append(s.Args(), args...)
return s.Runner.Run(s.Binary(), args...)
}
func (s *Tmux) Binary() string {
if s.BinPath != "" {
return s.BinPath
} else {
return "tmux"
}
}
func (s *Tmux) Args() []string {
args := []string{}
if s.SocketPath != "" {
args = append(args, "-S", s.SocketPath)
} else if s.SocketName != "" {
args = append(args, "-L", s.SocketName)
}
return args
}
func (s *Tmux) GetOptions(scope OptionsScope) (map[string]string, error) {
out, err := s.Exec("show-options", OptionsScopeFlags(scope))
if err != nil {
return nil, err
}
return s.parseOptions(out), nil
}
func (s *Tmux) parseOptions(options []byte) map[string]string {
scanner := bufio.NewScanner(bytes.NewBuffer(options))
result := map[string]string{}
for scanner.Scan() {
match := optMatcher.FindSubmatch(scanner.Bytes())
if len(match) > 2 {
result[string(match[1])] = string(s.unwrap(match[2], quote))
}
}
return result
}
func (s *Tmux) unwrap(input, wrap []byte) []byte {
if bytes.HasPrefix(input, wrap) && bytes.HasSuffix(input, wrap) {
return bytes.TrimSuffix(bytes.TrimPrefix(input, wrap), wrap)
}
return input
}

224
tmux_test.go Normal file
View File

@@ -0,0 +1,224 @@
package tmux
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockRunner struct {
mock.Mock
}
func (s *MockRunner) Run(command string, args ...string) ([]byte, error) {
called := s.Called(append([]string{command}, args...))
return called.Get(0).([]byte), called.Error(1)
}
func TestNewTmux(t *testing.T) {
tmux := New()
assert.IsType(t, &Tmux{}, tmux)
assert.IsType(t, &ExecRunner{}, tmux.Runner)
}
func TestTmuxExec(t *testing.T) {
tests := []struct {
binPath string
socketName string
socketPath string
baseArgs []string
args []string
out []byte
error error
}{
{
args: []string{"new-session", "-d"},
baseArgs: []string{"tmux"},
},
{
args: []string{"new-session", "-d"},
baseArgs: []string{"tmux"},
},
{
args: []string{"list-sessions"},
baseArgs: []string{"tmux"},
out: []byte("0: 1 windows (created Fri Dec 6 23:45:19 2019)"),
},
{
binPath: "/opt/tmux/bin/tmux",
args: []string{"list-sessions"},
baseArgs: []string{"/opt/tmux/bin/tmux"},
},
{
binPath: "/opt/tmux/bin/tmux",
socketName: "test-sock",
args: []string{"list-sessions"},
baseArgs: []string{"/opt/tmux/bin/tmux", "-L", "test-sock"},
},
{
binPath: "/opt/tmux/bin/tmux",
socketPath: "/tmp/tmux.sock",
args: []string{"list-sessions"},
baseArgs: []string{"/opt/tmux/bin/tmux", "-S", "/tmp/tmux.sock"},
},
{
binPath: "/opt/tmux/bin/tmux",
socketName: "test-sock",
socketPath: "/tmp/tmux.sock",
args: []string{"list-sessions"},
baseArgs: []string{"/opt/tmux/bin/tmux", "-S", "/tmp/tmux.sock"},
},
{
args: []string{"new-session", "-d"},
baseArgs: []string{"tmux"},
error: errors.New("Something went wrong"),
},
}
for _, tt := range tests {
runner := new(MockRunner)
runner.On("Run", append(tt.baseArgs, tt.args...)).
Return(tt.out, tt.error)
tmux := Tmux{
Runner: runner,
BinPath: tt.binPath,
SocketName: tt.socketName,
SocketPath: tt.socketPath,
}
out, err := tmux.Exec(tt.args...)
if tt.error == nil {
assert.NoError(t, err)
} else {
assert.Equal(t, tt.error, err)
}
if tt.out != nil {
assert.Equal(t, tt.out, out)
}
runner.AssertExpectations(t)
}
}
func TestTmuxBinary(t *testing.T) {
tests := []struct {
binPath string
executable string
}{
{binPath: "/opt/tmux/bin/tmux", executable: "/opt/tmux/bin/tmux"},
{executable: "tmux"},
}
for _, tt := range tests {
tmux := &Tmux{}
tmux.BinPath = tt.binPath
assert.Equal(t, tt.executable, tmux.Binary())
}
}
func TestTmuxArgs(t *testing.T) {
tests := []struct {
socketName string
socketPath string
args []string
}{
{args: []string{}},
{socketName: "foo", args: []string{"-L", "foo"}},
{socketPath: "/tmp/bar", args: []string{"-S", "/tmp/bar"}},
{
socketName: "foo",
socketPath: "/tmp/bar",
args: []string{"-S", "/tmp/bar"},
},
}
for _, tt := range tests {
tmux := &Tmux{}
tmux.SocketName = tt.socketName
tmux.SocketPath = tt.socketPath
assert.Equal(t, tt.args, tmux.Args())
}
}
func TestTmuxGetOptions(t *testing.T) {
tests := []struct {
flags string
scope OptionsScope
opts map[string]string
out []byte
error error
}{
{
opts: map[string]string{"hello-world": "FooBar"},
out: []byte(`hello-world FooBar`),
},
{
scope: Server,
flags: "-s",
opts: map[string]string{"hello-world": "Foo Bar"},
out: []byte(`hello-world "Foo Bar"`),
},
{
scope: GlobalSession,
flags: "-g",
opts: map[string]string{"hello-world": "Foo Bar"},
out: []byte(`hello-world "Foo Bar"`),
},
{
scope: GlobalWindow,
flags: "-gw",
opts: map[string]string{"hello-world": " Foo Bar "},
out: []byte(`hello-world " Foo Bar "`),
},
{
scope: Window,
flags: "-w",
opts: map[string]string{"@foo": "bar"},
out: []byte(`@foo bar`),
},
{
opts: map[string]string{
"@foo": "bar",
"@themepack": "powerline/default/green",
"status-left": "This Is Left",
},
out: []byte(`
@foo bar
@themepack "powerline/default/green"
status-left This Is Left
`),
},
}
for _, tt := range tests {
if tt.scope == 0 {
tt.scope = Session
}
runner := new(MockRunner)
runner.On("Run", append([]string{"tmux", "show-options"}, tt.flags)).
Return(tt.out, tt.error)
tmux := Tmux{Runner: runner}
opts, err := tmux.GetOptions(tt.scope)
if tt.error == nil {
assert.NoError(t, err)
assert.Equal(t, tt.opts, opts)
} else {
assert.Equal(t, tt.error, err)
}
runner.AssertExpectations(t)
}
}