Skip to content

Commit 307c5e2

Browse files
authored
✨ Support replacements in startup command (#259)
* feat: allow replacements in startup command * chore: update readme
1 parent 6e43f37 commit 307c5e2

File tree

10 files changed

+129
-9
lines changed

10 files changed

+129
-9
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,17 @@ startup_command = "nvim tmux.conf"
295295
preview_command = "bat --color=always ~/c/dotfiles/.config/tmux/tmux.conf"
296296
```
297297

298+
### Path substitution
299+
If you want to use the path of the selected session in your startup or preview command, you can use the `{}` placeholder.
300+
This will be replaced with the session's path when the command is run.
301+
302+
An example of this in use is the following, where the `tmuxinator` default_project uses the path as key/value pair using [ERB syntax](https://github.com/tmuxinator/tmuxinator?tab=readme-ov-file#erb):
303+
```toml
304+
[default_session]
305+
startup_command = "tmuxinator start default_project path={}"
306+
preview_command = "eza --all --git --icons --color=always {}"
307+
```
308+
298309
### Multiple windows
299310

300311
If you want your session to have multiple windows you can define windows in your configuration. You can then use these window layouts in your sessions. These windows can be reused as many times as you want and you can add as many windows to each session as you want.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.21
44

55
require (
66
github.com/pelletier/go-toml/v2 v2.2.1
7+
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745
78
github.com/stretchr/testify v1.9.0
89
github.com/urfave/cli/v2 v2.27.1
910
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
66
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
77
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
8+
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 h1:Vpr4VgAizEgEZsaMohpw6JYDP+i9Of9dmdY4ufNP6HI=
9+
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
810
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
911
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1012
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=

replacer/replacer.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package replacer
2+
3+
import (
4+
ahocorasick "github.com/petar-dambovaliev/aho-corasick"
5+
)
6+
7+
type Replacer interface {
8+
Replace(command string, replacements map[string]string) string
9+
}
10+
11+
type RealReplacer struct{}
12+
13+
func NewReplacer() Replacer {
14+
return &RealReplacer{}
15+
}
16+
17+
func (r *RealReplacer) Replace(command string, replacements map[string]string) string {
18+
dict := make([]string, 0, len(replacements))
19+
replacementArray := make([]string, 0, len(replacements))
20+
for k, v := range replacements {
21+
dict = append(dict, k)
22+
replacementArray = append(replacementArray, v)
23+
}
24+
ac := getAhoCorasick(dict)
25+
replacer := ahocorasick.NewReplacer(ac)
26+
return replacer.ReplaceAll(command, replacementArray)
27+
}
28+
29+
func getAhoCorasick(dictionary []string) ahocorasick.AhoCorasick {
30+
builder := ahocorasick.NewAhoCorasickBuilder(ahocorasick.Opts{
31+
AsciiCaseInsensitive: true,
32+
MatchOnlyWholeWords: true,
33+
MatchKind: ahocorasick.LeftMostFirstMatch,
34+
DFA: true,
35+
})
36+
37+
return builder.Build(dictionary)
38+
}

replacer/replacer_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package replacer
2+
3+
import (
4+
"testing"
5+
)
6+
7+
type testCase struct {
8+
input string
9+
expected string
10+
}
11+
12+
func TestReplace(t *testing.T) {
13+
defaultReplacements := map[string]string{
14+
"{}": "hello",
15+
"~": "/home/test",
16+
}
17+
18+
testCases := map[string]testCase{
19+
"multiple replacements": {
20+
"~/.local/bin/rat {}{}",
21+
"/home/test/.local/bin/rat hellohello",
22+
},
23+
"single replacement": {
24+
"/bin/rat {}",
25+
"/bin/rat hello",
26+
},
27+
"no replacement": {
28+
"/bin/rat",
29+
"/bin/rat",
30+
},
31+
}
32+
33+
for name, test := range testCases {
34+
t.Run(name, func(t *testing.T) {
35+
replacer := NewReplacer()
36+
result := replacer.Replace(test.input, defaultReplacements)
37+
if result != test.expected {
38+
t.Errorf("expected %s, got %s", test.expected, result)
39+
}
40+
})
41+
}
42+
t.Run("", func(t *testing.T) {
43+
})
44+
}

seshcli/seshcli.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/joshmedeski/sesh/v2/oswrap"
2121
"github.com/joshmedeski/sesh/v2/pathwrap"
2222
"github.com/joshmedeski/sesh/v2/previewer"
23+
"github.com/joshmedeski/sesh/v2/replacer"
2324
"github.com/joshmedeski/sesh/v2/runtimewrap"
2425
"github.com/joshmedeski/sesh/v2/shell"
2526
"github.com/joshmedeski/sesh/v2/startup"
@@ -39,6 +40,7 @@ func App(version string) cli.App {
3940
home := home.NewHome(os)
4041
shell := shell.NewShell(exec, home)
4142
json := json.NewJson()
43+
replacer := replacer.NewReplacer()
4244

4345
// resource dependencies
4446
git := git.NewGit(shell)
@@ -60,7 +62,7 @@ func App(version string) cli.App {
6062
// core dependencies
6163
ls := ls.NewLs(config, shell)
6264
lister := lister.NewLister(config, home, tmux, zoxide, tmuxinator)
63-
startup := startup.NewStartup(config, lister, tmux, home)
65+
startup := startup.NewStartup(config, lister, tmux, home, replacer)
6466
namer := namer.NewNamer(path, git, home)
6567
connector := connector.NewConnector(config, dir, home, lister, namer, startup, tmux, zoxide, tmuxinator)
6668
icon := icon.NewIcon(config)

shell/shell_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,14 @@ func TestShellPrepareCmd(t *testing.T) {
5555
assert.Nil(t, err)
5656
assert.Equal(t, []string{"/home/test/.local/bin/rat", "hello"}, cmdParts)
5757
})
58+
59+
// This test case asserts the existing behaviour when the desired replacement is not separated by spaces
60+
t.Run("should not use a partial match", func(t *testing.T) {
61+
mockHome := new(home.MockHome)
62+
shell := &RealShell{home: mockHome}
63+
mockHome.On("ExpandHome", "~/.local/bin/rat").Return("/home/test/.local/bin/rat", nil)
64+
cmdParts, err := shell.PrepareCmd("~/.local/bin/rat localVar={}", map[string]string{"{}": "hello"})
65+
assert.Nil(t, err)
66+
assert.Equal(t, []string{"/home/test/.local/bin/rat", "localVar={}"}, cmdParts)
67+
})
5868
}

startup/config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ func configStrategy(s *RealStartup, session model.SeshSession) (string, error) {
1010
}
1111

1212
if exists && config.StartupCommand != "" {
13-
return config.StartupCommand, nil
13+
replacements := map[string]string{
14+
"{}": session.Path,
15+
}
16+
17+
return s.replacer.Replace(config.StartupCommand, replacements), nil
1418
}
1519
return "", nil
1620
}

startup/defaultconfig.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ func defaultConfigStrategy(s *RealStartup, session model.SeshSession) (string, e
99

1010
defaultConfig := s.config.DefaultSessionConfig
1111
if defaultConfig.StartupCommand != "" {
12-
return defaultConfig.StartupCommand, nil
12+
replacements := map[string]string{
13+
"{}": session.Path,
14+
}
15+
16+
return s.replacer.Replace(defaultConfig.StartupCommand, replacements), nil
1317
}
1418

1519
return "", nil

startup/startup.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/joshmedeski/sesh/v2/home"
77
"github.com/joshmedeski/sesh/v2/lister"
88
"github.com/joshmedeski/sesh/v2/model"
9+
"github.com/joshmedeski/sesh/v2/replacer"
910
"github.com/joshmedeski/sesh/v2/tmux"
1011
)
1112

@@ -14,14 +15,17 @@ type Startup interface {
1415
}
1516

1617
type RealStartup struct {
17-
lister lister.Lister
18-
tmux tmux.Tmux
19-
config model.Config
20-
home home.Home
18+
lister lister.Lister
19+
tmux tmux.Tmux
20+
config model.Config
21+
home home.Home
22+
replacer replacer.Replacer
2123
}
2224

23-
func NewStartup(config model.Config, lister lister.Lister, tmux tmux.Tmux, home home.Home) Startup {
24-
return &RealStartup{lister, tmux, config, home}
25+
func NewStartup(
26+
config model.Config, lister lister.Lister, tmux tmux.Tmux, home home.Home, replacer replacer.Replacer,
27+
) Startup {
28+
return &RealStartup{lister, tmux, config, home, replacer}
2529
}
2630

2731
func (s *RealStartup) Exec(session model.SeshSession) (string, error) {

0 commit comments

Comments
 (0)