mirror of
https://github.com/jimeh/go-tmux.git
synced 2026-02-18 20:36:40 +00:00
feat: Initial working version of tmux package
This commit is contained in:
16
.github/workflows/ci.yml
vendored
Normal file
16
.github/workflows/ci.yml
vendored
Normal 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
18
LICENSE
Normal 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
README.md
Normal file
5
README.md
Normal 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
11
exec_runner.go
Normal 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
30
exec_runner_test.go
Normal 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
5
go.mod
Normal 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
11
go.sum
Normal 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
33
options_scope.go
Normal 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
27
options_scope_test.go
Normal 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
5
runner.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package tmux
|
||||
|
||||
type Runner interface {
|
||||
Run(string, ...string) ([]byte, error)
|
||||
}
|
||||
81
tmux.go
Normal file
81
tmux.go
Normal 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
224
tmux_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user