From 0f93be19cd36cf26f8adf6ee40e8b1482b03dc1b Mon Sep 17 00:00:00 2001 From: Josh Medeski Date: Thu, 1 Aug 2024 20:02:56 -0500 Subject: [PATCH] feat: add startup command --- cloner/mock_Cloner.go | 88 ++++++++++++++++++++++++++++++++++++++ connector/config_test.go | 3 ++ connector/connect.go | 3 +- connector/connector.go | 18 ++++---- connector/tmux_test.go | 3 ++ lister/config.go | 7 ++-- model/config.go | 3 +- model/sesh_session.go | 7 ++-- namer/git.go | 5 +-- namer/namer_test.go | 20 --------- seshcli/seshcli.go | 4 +- startup/config.go | 11 +++++ startup/defaultconfig.go | 11 +++++ startup/mock_Startup.go | 91 ++++++++++++++++++++++++++++++++++++++++ startup/startup.go | 40 ++++++++++++++++++ tmux/mock_Tmux.go | 57 +++++++++++++++++++++++++ tmux/tmux.go | 3 +- 17 files changed, 333 insertions(+), 41 deletions(-) create mode 100644 cloner/mock_Cloner.go create mode 100644 startup/config.go create mode 100644 startup/defaultconfig.go create mode 100644 startup/mock_Startup.go create mode 100644 startup/startup.go diff --git a/cloner/mock_Cloner.go b/cloner/mock_Cloner.go new file mode 100644 index 0000000..d6f9c74 --- /dev/null +++ b/cloner/mock_Cloner.go @@ -0,0 +1,88 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package cloner + +import mock "github.com/stretchr/testify/mock" + +// MockCloner is an autogenerated mock type for the Cloner type +type MockCloner struct { + mock.Mock +} + +type MockCloner_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCloner) EXPECT() *MockCloner_Expecter { + return &MockCloner_Expecter{mock: &_m.Mock} +} + +// Clone provides a mock function with given fields: path +func (_m *MockCloner) Clone(path string) (string, error) { + ret := _m.Called(path) + + if len(ret) == 0 { + panic("no return value specified for Clone") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string) (string, error)); ok { + return rf(path) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(path) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCloner_Clone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Clone' +type MockCloner_Clone_Call struct { + *mock.Call +} + +// Clone is a helper method to define mock.On call +// - path string +func (_e *MockCloner_Expecter) Clone(path interface{}) *MockCloner_Clone_Call { + return &MockCloner_Clone_Call{Call: _e.mock.On("Clone", path)} +} + +func (_c *MockCloner_Clone_Call) Run(run func(path string)) *MockCloner_Clone_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockCloner_Clone_Call) Return(_a0 string, _a1 error) *MockCloner_Clone_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCloner_Clone_Call) RunAndReturn(run func(string) (string, error)) *MockCloner_Clone_Call { + _c.Call.Return(run) + return _c +} + +// NewMockCloner creates a new instance of MockCloner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCloner(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCloner { + mock := &MockCloner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/connector/config_test.go b/connector/config_test.go index e206efa..ca6c1e8 100644 --- a/connector/config_test.go +++ b/connector/config_test.go @@ -8,6 +8,7 @@ import ( "github.com/joshmedeski/sesh/lister" "github.com/joshmedeski/sesh/model" "github.com/joshmedeski/sesh/namer" + "github.com/joshmedeski/sesh/startup" "github.com/joshmedeski/sesh/tmux" "github.com/joshmedeski/sesh/zoxide" "github.com/stretchr/testify/assert" @@ -19,6 +20,7 @@ func TestConfigStrategy(t *testing.T) { mockHome := new(home.MockHome) mockLister := new(lister.MockLister) mockNamer := new(namer.MockNamer) + mockStartup := new(startup.MockStartup) mockTmux := new(tmux.MockTmux) mockZoxide := new(zoxide.MockZoxide) @@ -28,6 +30,7 @@ func TestConfigStrategy(t *testing.T) { mockHome, mockLister, mockNamer, + mockStartup, mockTmux, mockZoxide, } diff --git a/connector/connect.go b/connector/connect.go index c7d300a..3526ce7 100644 --- a/connector/connect.go +++ b/connector/connect.go @@ -21,8 +21,8 @@ func (c *RealConnector) Connect(name string, opts model.ConnectOpts) (string, er for _, strategy := range strategies { if connection, err := strategy(c, name); err != nil { - } else if connection.Found { return "", fmt.Errorf("failed to establish connection: %w", err) + } else if connection.Found { // TODO: allow CLI flag to disable zoxide and overwrite all settings? // sesh connect --ignore-zoxide "dotfiles" if connection.AddToZoxide { @@ -30,6 +30,7 @@ func (c *RealConnector) Connect(name string, opts model.ConnectOpts) (string, er } if connection.New { c.tmux.NewSession(connection.Session.Name, connection.Session.Path) + c.startup.Exec(connection.Session) } // TODO: configure the ability to create a session in a detached way (like update) // TODO: configure the ability to create a popup instead of switching (with no tmux bar?) diff --git a/connector/connector.go b/connector/connector.go index c204f4a..b94d3a3 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -6,6 +6,7 @@ import ( "github.com/joshmedeski/sesh/lister" "github.com/joshmedeski/sesh/model" "github.com/joshmedeski/sesh/namer" + "github.com/joshmedeski/sesh/startup" "github.com/joshmedeski/sesh/tmux" "github.com/joshmedeski/sesh/zoxide" ) @@ -15,13 +16,14 @@ type Connector interface { } type RealConnector struct { - config model.Config - dir dir.Dir - home home.Home - lister lister.Lister - namer namer.Namer - tmux tmux.Tmux - zoxide zoxide.Zoxide + config model.Config + dir dir.Dir + home home.Home + lister lister.Lister + namer namer.Namer + startup startup.Startup + tmux tmux.Tmux + zoxide zoxide.Zoxide } func NewConnector( @@ -30,6 +32,7 @@ func NewConnector( home home.Home, lister lister.Lister, namer namer.Namer, + startup startup.Startup, tmux tmux.Tmux, zoxide zoxide.Zoxide, ) Connector { @@ -39,6 +42,7 @@ func NewConnector( home, lister, namer, + startup, tmux, zoxide, } diff --git a/connector/tmux_test.go b/connector/tmux_test.go index fe57bb9..ed5be7e 100644 --- a/connector/tmux_test.go +++ b/connector/tmux_test.go @@ -8,6 +8,7 @@ import ( "github.com/joshmedeski/sesh/lister" "github.com/joshmedeski/sesh/model" "github.com/joshmedeski/sesh/namer" + "github.com/joshmedeski/sesh/startup" "github.com/joshmedeski/sesh/tmux" "github.com/joshmedeski/sesh/zoxide" "github.com/stretchr/testify/assert" @@ -19,6 +20,7 @@ func TestEstablishTmuxConnection(t *testing.T) { mockHome := new(home.MockHome) mockLister := new(lister.MockLister) mockNamer := new(namer.MockNamer) + mockStartup := new(startup.MockStartup) mockTmux := new(tmux.MockTmux) mockZoxide := new(zoxide.MockZoxide) @@ -28,6 +30,7 @@ func TestEstablishTmuxConnection(t *testing.T) { mockHome, mockLister, mockNamer, + mockStartup, mockTmux, mockZoxide, } diff --git a/lister/config.go b/lister/config.go index 6946dae..30129c1 100644 --- a/lister/config.go +++ b/lister/config.go @@ -22,9 +22,10 @@ func listConfig(l *RealLister) (model.SeshSessions, error) { return model.SeshSessions{}, fmt.Errorf("couldn't expand home: %q", err) } directory[key] = model.SeshSession{ - Src: "config", - Name: session.Name, - Path: path, + Src: "config", + Name: session.Name, + Path: path, + StartupCommand: session.StartupCommand, } } } diff --git a/model/config.go b/model/config.go index 2c9638d..42129ee 100644 --- a/model/config.go +++ b/model/config.go @@ -8,7 +8,8 @@ type ( } DefaultSessionConfig struct { - StartupScript string `toml:"startup_script"` + // TODO: mention breaking change in v2 release notes + // StartupScript string `toml:"startup_script"` StartupCommand string `toml:"startup_command"` Tmuxp string `toml:"tmuxp"` Tmuxinator string `toml:"tmuxinator"` diff --git a/model/sesh_session.go b/model/sesh_session.go index 4d8b6c7..b6a7937 100644 --- a/model/sesh_session.go +++ b/model/sesh_session.go @@ -15,9 +15,10 @@ type ( Name string // The display name Path string // The absolute directory path - Attached int // Whether the session is currently attached - Windows int // The number of windows in the session - Score float64 // The score of the session (from Zoxide) + StartupCommand string // The command to run when the session is started + Attached int // Whether the session is currently attached + Windows int // The number of windows in the session + Score float64 // The score of the session (from Zoxide) } SeshSrcs struct { diff --git a/namer/git.go b/namer/git.go index fac3bd1..0ea1798 100644 --- a/namer/git.go +++ b/namer/git.go @@ -5,10 +5,7 @@ import "strings" // Gets the name from a git bare repository func gitBareName(n *RealNamer, path string) (string, error) { var name string - isGit, commonDir, err := n.git.GitCommonDir(path) - if err != nil { - return "", err - } + isGit, commonDir, _ := n.git.GitCommonDir(path) if isGit && strings.HasSuffix(commonDir, "/.bare") { topLevelDir := strings.TrimSuffix(commonDir, "/.bare") relativePath := strings.TrimPrefix(path, topLevelDir) diff --git a/namer/namer_test.go b/namer/namer_test.go index c99f7a6..1a664af 100644 --- a/namer/namer_test.go +++ b/namer/namer_test.go @@ -38,23 +38,3 @@ func TestFromPath(t *testing.T) { assert.Equal(t, "neovim", name) }) } - -func TestConvertToValidName(t *testing.T) { - t.Run("Test with dot", func(t *testing.T) { - input := "test.name" - want := "test_name" - assert.Equal(t, want, convertToValidName(input)) - }) - - t.Run("Test with colon", func(t *testing.T) { - input := "test:name" - want := "test_name" - assert.Equal(t, want, convertToValidName(input)) - }) - - t.Run("Test with multiple special characters", func(t *testing.T) { - input := "test.name:with.multiple" - want := "test_name_with_multiple" - assert.Equal(t, want, convertToValidName(input)) - }) -} diff --git a/seshcli/seshcli.go b/seshcli/seshcli.go index d54e7c8..cf796e5 100644 --- a/seshcli/seshcli.go +++ b/seshcli/seshcli.go @@ -13,6 +13,7 @@ import ( "github.com/joshmedeski/sesh/pathwrap" "github.com/joshmedeski/sesh/runtimewrap" "github.com/joshmedeski/sesh/shell" + "github.com/joshmedeski/sesh/startup" "github.com/joshmedeski/sesh/tmux" "github.com/joshmedeski/sesh/zoxide" "github.com/urfave/cli/v2" @@ -44,8 +45,9 @@ func App(version string) cli.App { // core dependencies lister := lister.NewLister(config, home, tmux, zoxide) + startup := startup.NewStartup(config, lister, tmux) namer := namer.NewNamer(path, git) - connector := connector.NewConnector(config, dir, home, lister, namer, tmux, zoxide) + connector := connector.NewConnector(config, dir, home, lister, namer, startup, tmux, zoxide) return cli.App{ Name: "sesh", diff --git a/startup/config.go b/startup/config.go new file mode 100644 index 0000000..db05ae1 --- /dev/null +++ b/startup/config.go @@ -0,0 +1,11 @@ +package startup + +import "github.com/joshmedeski/sesh/model" + +func configStrategy(s *RealStartup, session model.SeshSession) (string, error) { + config, exists := s.lister.FindConfigSession(session.Name) + if exists && config.StartupCommand != "" { + return config.StartupCommand, nil + } + return "", nil +} diff --git a/startup/defaultconfig.go b/startup/defaultconfig.go new file mode 100644 index 0000000..f7fcce3 --- /dev/null +++ b/startup/defaultconfig.go @@ -0,0 +1,11 @@ +package startup + +import "github.com/joshmedeski/sesh/model" + +func defaultConfigStrategy(s *RealStartup, session model.SeshSession) (string, error) { + defaultConfig := s.config.DefaultSessionConfig + if defaultConfig.StartupCommand != "" { + return defaultConfig.StartupCommand, nil + } + return "", nil +} diff --git a/startup/mock_Startup.go b/startup/mock_Startup.go new file mode 100644 index 0000000..9ae248f --- /dev/null +++ b/startup/mock_Startup.go @@ -0,0 +1,91 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package startup + +import ( + model "github.com/joshmedeski/sesh/model" + mock "github.com/stretchr/testify/mock" +) + +// MockStartup is an autogenerated mock type for the Startup type +type MockStartup struct { + mock.Mock +} + +type MockStartup_Expecter struct { + mock *mock.Mock +} + +func (_m *MockStartup) EXPECT() *MockStartup_Expecter { + return &MockStartup_Expecter{mock: &_m.Mock} +} + +// Exec provides a mock function with given fields: session +func (_m *MockStartup) Exec(session model.SeshSession) (string, error) { + ret := _m.Called(session) + + if len(ret) == 0 { + panic("no return value specified for Exec") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(model.SeshSession) (string, error)); ok { + return rf(session) + } + if rf, ok := ret.Get(0).(func(model.SeshSession) string); ok { + r0 = rf(session) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(model.SeshSession) error); ok { + r1 = rf(session) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStartup_Exec_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exec' +type MockStartup_Exec_Call struct { + *mock.Call +} + +// Exec is a helper method to define mock.On call +// - session model.SeshSession +func (_e *MockStartup_Expecter) Exec(session interface{}) *MockStartup_Exec_Call { + return &MockStartup_Exec_Call{Call: _e.mock.On("Exec", session)} +} + +func (_c *MockStartup_Exec_Call) Run(run func(session model.SeshSession)) *MockStartup_Exec_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(model.SeshSession)) + }) + return _c +} + +func (_c *MockStartup_Exec_Call) Return(_a0 string, _a1 error) *MockStartup_Exec_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStartup_Exec_Call) RunAndReturn(run func(model.SeshSession) (string, error)) *MockStartup_Exec_Call { + _c.Call.Return(run) + return _c +} + +// NewMockStartup creates a new instance of MockStartup. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockStartup(t interface { + mock.TestingT + Cleanup(func()) +}) *MockStartup { + mock := &MockStartup{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/startup/startup.go b/startup/startup.go new file mode 100644 index 0000000..d7f754f --- /dev/null +++ b/startup/startup.go @@ -0,0 +1,40 @@ +package startup + +import ( + "fmt" + + "github.com/joshmedeski/sesh/lister" + "github.com/joshmedeski/sesh/model" + "github.com/joshmedeski/sesh/tmux" +) + +type Startup interface { + Exec(session model.SeshSession) (string, error) +} + +type RealStartup struct { + lister lister.Lister + tmux tmux.Tmux + config model.Config +} + +func NewStartup(config model.Config, lister lister.Lister, tmux tmux.Tmux) Startup { + return &RealStartup{lister, tmux, config} +} + +func (s *RealStartup) Exec(session model.SeshSession) (string, error) { + strategies := []func(*RealStartup, model.SeshSession) (string, error){ + configStrategy, + defaultConfigStrategy, + } + + for _, strategy := range strategies { + if command, err := strategy(s, session); err != nil { + return "", fmt.Errorf("failed to determine startup command: %w", err) + } else if command != "" { + s.tmux.SendKeys(session.Name, command) + return fmt.Sprintf("executing startup command: %s", command), nil + } + } + return "", nil // no command to run +} diff --git a/tmux/mock_Tmux.go b/tmux/mock_Tmux.go index e2d6d24..1b3bc09 100644 --- a/tmux/mock_Tmux.go +++ b/tmux/mock_Tmux.go @@ -235,6 +235,63 @@ func (_c *MockTmux_NewSession_Call) RunAndReturn(run func(string, string) (strin return _c } +// SendKeys provides a mock function with given fields: name, command +func (_m *MockTmux) SendKeys(name string, command string) (string, error) { + ret := _m.Called(name, command) + + if len(ret) == 0 { + panic("no return value specified for SendKeys") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok { + return rf(name, command) + } + if rf, ok := ret.Get(0).(func(string, string) string); ok { + r0 = rf(name, command) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(name, command) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTmux_SendKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendKeys' +type MockTmux_SendKeys_Call struct { + *mock.Call +} + +// SendKeys is a helper method to define mock.On call +// - name string +// - command string +func (_e *MockTmux_Expecter) SendKeys(name interface{}, command interface{}) *MockTmux_SendKeys_Call { + return &MockTmux_SendKeys_Call{Call: _e.mock.On("SendKeys", name, command)} +} + +func (_c *MockTmux_SendKeys_Call) Run(run func(name string, command string)) *MockTmux_SendKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockTmux_SendKeys_Call) Return(_a0 string, _a1 error) *MockTmux_SendKeys_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTmux_SendKeys_Call) RunAndReturn(run func(string, string) (string, error)) *MockTmux_SendKeys_Call { + _c.Call.Return(run) + return _c +} + // SwitchClient provides a mock function with given fields: targetSession func (_m *MockTmux) SwitchClient(targetSession string) (string, error) { ret := _m.Called(targetSession) diff --git a/tmux/tmux.go b/tmux/tmux.go index 7f0d17d..106e48d 100644 --- a/tmux/tmux.go +++ b/tmux/tmux.go @@ -11,6 +11,7 @@ type Tmux interface { NewSession(sessionName string, startDir string) (string, error) IsAttached() bool AttachSession(targetSession string) (string, error) + SendKeys(name string, command string) (string, error) SwitchClient(targetSession string) (string, error) SwitchOrAttach(name string, opts model.ConnectOpts) (string, error) } @@ -33,7 +34,7 @@ func (t *RealTmux) SwitchClient(targetSession string) (string, error) { } func (t *RealTmux) SendKeys(targetPane string, keys string) (string, error) { - return t.shell.Cmd("tmux", "send-keys", "-t", targetPane, keys) + return t.shell.Cmd("tmux", "send-keys", "-t", targetPane, keys, "Enter") } func (t *RealTmux) NewSession(sessionName string, startDir string) (string, error) {