teamocil 0.3.9 → 0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/README.md +28 -15
- data/Rakefile +4 -4
- data/examples/four-splits.yml +8 -0
- data/examples/one-and-three-splits.yml +8 -0
- data/examples/six-splits.yml +10 -0
- data/examples/two-horizontal-splits.yml +6 -0
- data/examples/two-vertical-splits.yml +6 -0
- data/lib/teamocil.rb +5 -4
- data/lib/teamocil/cli.rb +10 -12
- data/lib/teamocil/layout.rb +12 -14
- data/lib/teamocil/layout/session.rb +4 -6
- data/lib/teamocil/layout/split.rb +5 -8
- data/lib/teamocil/layout/window.rb +15 -7
- data/spec/cli_spec.rb +24 -31
- data/spec/layout_spec.rb +74 -63
- data/spec/mock/cli.rb +10 -13
- data/spec/mock/layout.rb +2 -6
- data/spec/spec_helper.rb +2 -4
- data/teamocil.gemspec +1 -1
- metadata +14 -30
- data/examples/simple-four-splits.yml +0 -13
- data/examples/simple-one-and-three-splits.yml +0 -12
- data/examples/simple-six-splits.yml +0 -18
- data/examples/simple-two-horitonzal-splits.yml +0 -6
- data/examples/simple-two-vertical-splits.yml +0 -6
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZjUwMTBkNTkxZjQyMjMzOTU5ZTAxYmZlN2EyZDdlMDNjZmFmOWRlZQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NTY4Nzc1ZjAzMjQzZTYyOTA4NzhhNWRiNjI4NGE0NzFiNWZhYzMzOQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZmVhMDQ3ZWUyOGZkOGYwZWE4NzdlNTcwZjkxNTZmYmEyZjYwMDg1MzBjYjEz
|
10
|
+
MjU5MGEwZmQ5YWM0ZjI0MzJmMDFjYzM3Y2M1ZjVhMGJmMTQxNjk1ZWE5N2Q3
|
11
|
+
ZjZhZjExNDEyZDlhOWQ2MmRjMmQxMDJlYWQ3M2UyYWE4MWMxNjY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NTZjOWZmMTFmMDNkNGFkNGFhZjkxNTdlZjY2MDhiNWYyNWI0ZGYwOTExMTgx
|
14
|
+
MzY4NjYyYWVhOWZlNGViMmQ3ZGJjMWZiY2QyZTliMjExOTdhMjQxNDcxMGM2
|
15
|
+
ZDgwY2EzOTcwNzE5NzdlZDgxNWQ4Yjg0YjU2M2IyNzNiY2IyZWE=
|
data/README.md
CHANGED
@@ -51,9 +51,25 @@ If you are not using a top-level `session` key, then the first key of your layou
|
|
51
51
|
* `root` (the directory in which every split will be created)
|
52
52
|
* `filters` (a hash of `before` and `after` commands to run for each split)
|
53
53
|
* `clear` (whether or not to prepend a `clear` command before the `before` filters list)
|
54
|
-
* `layout` (a layout name or serialized string supported by `tmux
|
54
|
+
* `layout` (a layout name or serialized string supported by the `tmux select-layout` command)
|
55
55
|
* `splits` (an array of split items)
|
56
|
-
* `options` (a hash of tmux options, see `man tmux` for a list)
|
56
|
+
* `options` (a hash of `tmux` options, see `man tmux` for a list)
|
57
|
+
|
58
|
+
#### Notes
|
59
|
+
|
60
|
+
If you want to use a custom value for the `layout` key, running this command will give you the layout of the current window:
|
61
|
+
|
62
|
+
```bash
|
63
|
+
$ tmux list-windows -F "#{window_active} #{window_layout}" | grep "^1" | cut -d " " -f 2
|
64
|
+
```
|
65
|
+
|
66
|
+
You can then use the value as a string, like so:
|
67
|
+
|
68
|
+
```yaml
|
69
|
+
- name: "a-window-with-weird-layout"
|
70
|
+
layout: "4d71,204x51,0,0{101x51,0,0,114,102x51,102,0[102x10,102,0,118,102x40,102,11,115]}"
|
71
|
+
splits: …
|
72
|
+
```
|
57
73
|
|
58
74
|
#### Example
|
59
75
|
|
@@ -66,15 +82,17 @@ windows:
|
|
66
82
|
root: "~/Projects/foo-www"
|
67
83
|
filters:
|
68
84
|
before:
|
69
|
-
- "echo 'Let’s use ruby-1.9.
|
70
|
-
- "
|
85
|
+
- "echo 'Let’s use ruby-1.9.3 for each split in this window.'"
|
86
|
+
- "rbenv local 1.9.3-p374"
|
71
87
|
splits:
|
72
88
|
[splits list]
|
73
89
|
- name: "my-second-window"
|
90
|
+
layout: tiled
|
74
91
|
root: "~/Projects/foo-api"
|
75
92
|
splits:
|
76
93
|
[splits list]
|
77
94
|
- name: "my-third-window"
|
95
|
+
layout: main-vertical
|
78
96
|
root: "~/Projects/foo-daemons"
|
79
97
|
splits:
|
80
98
|
[splits list]
|
@@ -82,7 +100,7 @@ windows:
|
|
82
100
|
|
83
101
|
### Splits
|
84
102
|
|
85
|
-
Every window must define an array of splits that will be created within it. A vertical or horizontal split will be created, depending on whether the `width` or `height` parameter is used.
|
103
|
+
Every window must define an array of splits that will be created within it. A vertical or horizontal split will be created, depending on whether the `width` or `height` parameter is used. If a `layout` option is used for the window, the `width` and `height` attributes won’t have any effect.
|
86
104
|
|
87
105
|
#### Item keys
|
88
106
|
|
@@ -98,18 +116,17 @@ Every window must define an array of splits that will be created within it. A ve
|
|
98
116
|
windows:
|
99
117
|
- name: "my-first-window"
|
100
118
|
root: "~/Projects/foo-www"
|
119
|
+
layout: even-vertical
|
101
120
|
filters:
|
102
|
-
before: "
|
121
|
+
before: "rbenv local 2.0.0-p0"
|
103
122
|
after: "echo 'I am done initializing this split.'"
|
104
123
|
splits:
|
105
124
|
- cmd: "git status"
|
106
125
|
- cmd: "bundle exec rails server --port 4000"
|
107
126
|
focus: true
|
108
|
-
width: 50
|
109
127
|
- cmd:
|
110
128
|
- "sudo service memcached start"
|
111
129
|
- "sudo service mongodb start"
|
112
|
-
height: 50
|
113
130
|
```
|
114
131
|
|
115
132
|
## Layout examples
|
@@ -124,10 +141,10 @@ See more example files in the `examples` directory.
|
|
124
141
|
windows:
|
125
142
|
- name: "sample-two-splits"
|
126
143
|
root: "~/Code/sample/www"
|
144
|
+
layout: even-horizontal
|
127
145
|
splits:
|
128
146
|
- cmd: ["pwd", "ls -la"]
|
129
147
|
- cmd: "rails server --port 3000"
|
130
|
-
width: 50
|
131
148
|
```
|
132
149
|
|
133
150
|
|
@@ -153,16 +170,12 @@ windows:
|
|
153
170
|
windows:
|
154
171
|
- name: "sample-four-splits"
|
155
172
|
root: "~/Code/sample/www"
|
173
|
+
layout: tiled
|
156
174
|
splits:
|
157
175
|
- cmd: "pwd"
|
158
176
|
- cmd: "pwd"
|
159
|
-
width: 50
|
160
177
|
- cmd: "pwd"
|
161
|
-
height: 50
|
162
|
-
target: "bottom-right"
|
163
178
|
- cmd: "pwd"
|
164
|
-
height: 50
|
165
|
-
target: "bottom-left"
|
166
179
|
```
|
167
180
|
|
168
181
|
#### Result of `$ teamocil sample-2`
|
@@ -226,4 +239,4 @@ Take a look at the `spec` folder before you do, and make sure `bundle exec rake
|
|
226
239
|
|
227
240
|
## License
|
228
241
|
|
229
|
-
Teamocil is © 2011-
|
242
|
+
Teamocil is © 2011-2013 [Rémi Prévost](http://exomel.com) and may be freely distributed under the [MIT license](https://github.com/remiprev/teamocil/blob/master/LICENSE). See the `LICENSE` file.
|
data/Rakefile
CHANGED
@@ -7,13 +7,13 @@ require "rspec/core/rake_task"
|
|
7
7
|
task :default => :spec
|
8
8
|
|
9
9
|
desc "Run all specs"
|
10
|
-
RSpec::Core::RakeTask.new(:spec) do |task|
|
10
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
11
11
|
task.pattern = "spec/**/*_spec.rb"
|
12
12
|
task.rspec_opts = "--colour --format=documentation"
|
13
|
-
end
|
13
|
+
end
|
14
14
|
|
15
15
|
desc "Generate YARD Documentation"
|
16
|
-
YARD::Rake::YardocTask.new do |task|
|
16
|
+
YARD::Rake::YardocTask.new do |task|
|
17
17
|
task.options = [
|
18
18
|
"-o", File.expand_path("../doc", __FILE__),
|
19
19
|
"--readme=README.md",
|
@@ -25,4 +25,4 @@ YARD::Rake::YardocTask.new do |task| # {{{
|
|
25
25
|
"--title=Teamocil",
|
26
26
|
]
|
27
27
|
task.files = ["lib/**/*.rb"]
|
28
|
-
end
|
28
|
+
end
|
data/lib/teamocil.rb
CHANGED
data/lib/teamocil/cli.rb
CHANGED
@@ -5,14 +5,13 @@ require 'erb'
|
|
5
5
|
module Teamocil
|
6
6
|
# This class handles interaction with the `tmux` utility.
|
7
7
|
class CLI
|
8
|
-
|
9
8
|
attr_accessor :layout, :layouts
|
10
9
|
|
11
10
|
# Initialize a new run of `tmux`
|
12
11
|
#
|
13
12
|
# @param argv [Hash] the command line parameters hash (usually `ARGV`).
|
14
13
|
# @param env [Hash] the environment variables hash (usually `ENV`).
|
15
|
-
def initialize(argv, env)
|
14
|
+
def initialize(argv, env)
|
16
15
|
parse_options! argv
|
17
16
|
layout_path = env["TEAMOCIL_PATH"] || File.join("#{env["HOME"]}", ".teamocil")
|
18
17
|
|
@@ -44,10 +43,10 @@ module Teamocil
|
|
44
43
|
end
|
45
44
|
@layout.execute_commands(@layout.generate_commands)
|
46
45
|
end
|
47
|
-
end
|
46
|
+
end
|
48
47
|
|
49
48
|
# Parse the command line options
|
50
|
-
def parse_options!(args)
|
49
|
+
def parse_options!(args)
|
51
50
|
@options = {}
|
52
51
|
opts = ::OptionParser.new do |opts|
|
53
52
|
opts.banner = "Usage: teamocil [options] <layout>
|
@@ -76,28 +75,27 @@ module Teamocil
|
|
76
75
|
|
77
76
|
end
|
78
77
|
opts.parse! args
|
79
|
-
end
|
78
|
+
end
|
80
79
|
|
81
80
|
# Return an array of available layouts
|
82
81
|
#
|
83
82
|
# @param path [String] the path used to look for layouts
|
84
|
-
def get_layouts(path)
|
83
|
+
def get_layouts(path)
|
85
84
|
Dir.glob(File.join(path, "*.yml")).map { |file| File.basename(file).gsub(/\..+$/, "") }.sort
|
86
|
-
end
|
85
|
+
end
|
87
86
|
|
88
87
|
# Print each layout on a single line
|
89
|
-
def print_layouts
|
88
|
+
def print_layouts
|
90
89
|
STDOUT.puts @layouts.join("\n")
|
91
90
|
exit 0
|
92
|
-
end
|
91
|
+
end
|
93
92
|
|
94
93
|
# Print an error message and exit the utility
|
95
94
|
#
|
96
95
|
# @param msg [Mixed] something to print before exiting.
|
97
|
-
def bail(msg)
|
96
|
+
def bail(msg)
|
98
97
|
STDERR.puts "[teamocil] #{msg}"
|
99
98
|
exit 1
|
100
|
-
end
|
101
|
-
|
99
|
+
end
|
102
100
|
end
|
103
101
|
end
|
data/lib/teamocil/layout.rb
CHANGED
@@ -1,46 +1,44 @@
|
|
1
|
-
|
1
|
+
require "teamocil/layout/session"
|
2
|
+
require "teamocil/layout/window"
|
3
|
+
require "teamocil/layout/split"
|
2
4
|
|
5
|
+
module Teamocil
|
3
6
|
# This class act as a wrapper around a tmux YAML layout file
|
4
7
|
class Layout
|
5
|
-
autoload :Session, "teamocil/layout/session"
|
6
|
-
autoload :Window, "teamocil/layout/window"
|
7
|
-
autoload :Split, "teamocil/layout/split"
|
8
|
-
|
9
8
|
attr_reader :session
|
10
9
|
|
11
10
|
# Initialize a new layout from a hash
|
12
11
|
#
|
13
12
|
# @param layout [Hash] the parsed layout
|
14
13
|
# @param options [Hash] some options
|
15
|
-
def initialize(layout, options={})
|
14
|
+
def initialize(layout, options={})
|
16
15
|
@layout = layout
|
17
16
|
@options = options
|
18
|
-
end
|
17
|
+
end
|
19
18
|
|
20
19
|
# Generate tmux commands based on the data found in the layout file
|
21
20
|
#
|
22
21
|
# @return [Array] an array of shell commands to send
|
23
|
-
def generate_commands
|
22
|
+
def generate_commands
|
24
23
|
@session.generate_commands
|
25
|
-
end
|
24
|
+
end
|
26
25
|
|
27
26
|
# Compile the layout into objects
|
28
27
|
#
|
29
28
|
# @return [Session]
|
30
|
-
def compile!
|
29
|
+
def compile!
|
31
30
|
if @layout["session"].nil?
|
32
31
|
@session = Session.new @options, "windows" => @layout["windows"]
|
33
32
|
else
|
34
33
|
@session = Session.new @options, @layout["session"]
|
35
34
|
end
|
36
|
-
end
|
35
|
+
end
|
37
36
|
|
38
37
|
# Execute each command in the shell
|
39
38
|
#
|
40
39
|
# @param commands [Array] an array of complete commands to send to the shell
|
41
|
-
def execute_commands(commands)
|
40
|
+
def execute_commands(commands)
|
42
41
|
`#{commands.join("; ")}`
|
43
|
-
end
|
44
|
-
|
42
|
+
end
|
45
43
|
end
|
46
44
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Teamocil
|
2
2
|
class Layout
|
3
|
-
|
4
3
|
# This class represents a session within tmux
|
5
4
|
class Session
|
6
5
|
attr_reader :options, :windows, :name
|
@@ -9,22 +8,21 @@ module Teamocil
|
|
9
8
|
#
|
10
9
|
# @param options [Hash] the options, mostly passed by the CLI
|
11
10
|
# @param attrs [Hash] the session data from the layout file
|
12
|
-
def initialize(options, attrs={})
|
11
|
+
def initialize(options, attrs={})
|
13
12
|
raise Teamocil::Error::LayoutError.new("You must specify a `windows` or `session` key for your layout.") unless attrs["windows"]
|
14
13
|
@name = attrs["name"] || "teamocil-session"
|
15
14
|
@windows = attrs["windows"].each_with_index.map { |window, window_index| Window.new(self, window_index, window) }
|
16
15
|
@options = options
|
17
|
-
end
|
16
|
+
end
|
18
17
|
|
19
18
|
# Generate commands to send to tmux
|
20
19
|
#
|
21
20
|
# @return [Array]
|
22
|
-
def generate_commands
|
21
|
+
def generate_commands
|
23
22
|
commands = []
|
24
23
|
commands << "tmux rename-session \"#{@name}\"" unless @name.nil?
|
25
24
|
commands << @windows.map(&:generate_commands)
|
26
|
-
end
|
27
|
-
|
25
|
+
end
|
28
26
|
end
|
29
27
|
end
|
30
28
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Teamocil
|
2
2
|
class Layout
|
3
|
-
|
4
3
|
# This class represents a split within a tmux window
|
5
4
|
class Split
|
6
5
|
attr_reader :width, :height, :cmd, :index, :target, :focus
|
@@ -10,7 +9,7 @@ module Teamocil
|
|
10
9
|
# @param session [Session] the window where the split is initialized
|
11
10
|
# @param index [Fixnnum] the split index
|
12
11
|
# @param attrs [Hash] the split data from the layout file
|
13
|
-
def initialize(window, index, attrs={})
|
12
|
+
def initialize(window, index, attrs={})
|
14
13
|
raise Teamocil::Error::LayoutError.new("You cannot have empty splits") if attrs.nil?
|
15
14
|
@height = attrs["height"]
|
16
15
|
@width = attrs["width"]
|
@@ -20,17 +19,17 @@ module Teamocil
|
|
20
19
|
|
21
20
|
@window = window
|
22
21
|
@index = index
|
23
|
-
end
|
22
|
+
end
|
24
23
|
|
25
24
|
# Generate commands to send to tmux
|
26
25
|
#
|
27
26
|
# @return [Array]
|
28
|
-
def generate_commands
|
27
|
+
def generate_commands
|
29
28
|
commands = []
|
30
29
|
|
31
30
|
# Is it a vertical or horizontal split?
|
32
31
|
init_command = ""
|
33
|
-
unless @index ==
|
32
|
+
unless @index == @window.pane_base_index
|
34
33
|
if !@width.nil?
|
35
34
|
init_command = "tmux split-window -h -p #{@width}"
|
36
35
|
elsif !@height.nil?
|
@@ -56,9 +55,7 @@ module Teamocil
|
|
56
55
|
commands << "tmux send-keys -t #{@index} Enter"
|
57
56
|
|
58
57
|
commands
|
59
|
-
end
|
60
|
-
|
58
|
+
end
|
61
59
|
end
|
62
|
-
|
63
60
|
end
|
64
61
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module Teamocil
|
2
2
|
class Layout
|
3
|
-
|
4
3
|
# This class represents a window within tmux
|
5
4
|
class Window
|
6
5
|
attr_reader :filters, :root, :splits, :options, :index, :name, :clear, :layout
|
@@ -10,7 +9,7 @@ module Teamocil
|
|
10
9
|
# @param session [Session] the session where the window is initialized
|
11
10
|
# @param index [Fixnnum] the window index
|
12
11
|
# @param attrs [Hash] the window data from the layout file
|
13
|
-
def initialize(session, index, attrs={})
|
12
|
+
def initialize(session, index, attrs={})
|
14
13
|
@name = attrs["name"] || "teamocil-window-#{index+1}"
|
15
14
|
@root = attrs["root"] || "."
|
16
15
|
@clear = attrs["clear"] == true ? "clear" : nil
|
@@ -19,7 +18,7 @@ module Teamocil
|
|
19
18
|
|
20
19
|
@splits = attrs["splits"] || []
|
21
20
|
raise Teamocil::Error::LayoutError.new("You must specify a `splits` key for every window.") if @splits.empty?
|
22
|
-
@splits = @splits.each_with_index.map { |split, split_index| Split.new(self, split_index, split) }
|
21
|
+
@splits = @splits.each_with_index.map { |split, split_index| Split.new(self, split_index + pane_base_index, split) }
|
23
22
|
|
24
23
|
@filters = attrs["filters"] || {}
|
25
24
|
@filters["before"] ||= []
|
@@ -27,12 +26,12 @@ module Teamocil
|
|
27
26
|
|
28
27
|
@index = index
|
29
28
|
@session = session
|
30
|
-
end
|
29
|
+
end
|
31
30
|
|
32
31
|
# Generate commands to send to tmux
|
33
32
|
#
|
34
33
|
# @return [Array]
|
35
|
-
def generate_commands
|
34
|
+
def generate_commands
|
36
35
|
commands = []
|
37
36
|
|
38
37
|
if @session.options.include?(:here) and @index == 0
|
@@ -53,9 +52,18 @@ module Teamocil
|
|
53
52
|
commands << "tmux select-pane -t #{@splits.map(&:focus).index(true) || 0}"
|
54
53
|
|
55
54
|
commands
|
56
|
-
end
|
55
|
+
end
|
57
56
|
|
57
|
+
# Return the `pane-base-index` option for the current window
|
58
|
+
#
|
59
|
+
# @return [Fixnum]
|
60
|
+
def pane_base_index
|
61
|
+
@pane_base_index ||= begin
|
62
|
+
global_setting = `tmux show-window-option -g pane-base-index`.split(/\s/).last
|
63
|
+
local_setting = `tmux show-window-option pane-base-index`.split(/\s/).last
|
64
|
+
(local_setting || global_setting || "0").to_i
|
65
|
+
end
|
66
|
+
end
|
58
67
|
end
|
59
|
-
|
60
68
|
end
|
61
69
|
end
|
data/spec/cli_spec.rb
CHANGED
@@ -2,77 +2,70 @@
|
|
2
2
|
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
3
3
|
|
4
4
|
describe Teamocil::CLI do
|
5
|
-
|
6
5
|
context "executing" do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end # }}}
|
6
|
+
before do
|
7
|
+
@fake_env = { "TMUX" => 1, "HOME" => File.join(File.dirname(__FILE__), "fixtures") }
|
8
|
+
end
|
11
9
|
|
12
10
|
context "not in tmux" do
|
13
|
-
|
14
|
-
it "should allow editing" do # {{{
|
11
|
+
it "should allow editing" do
|
15
12
|
FileUtils.stub(:touch)
|
16
13
|
Kernel.should_receive(:system).with("$EDITOR #{File.join(@fake_env["HOME"], ".teamocil", "my-layout.yml").inspect}")
|
17
14
|
Teamocil::CLI.new(["--edit", "my-layout"], @fake_env)
|
18
|
-
end
|
19
|
-
|
15
|
+
end
|
20
16
|
end
|
21
17
|
|
22
18
|
context "in tmux" do
|
23
|
-
|
24
|
-
before :each do # {{{
|
19
|
+
before :each do
|
25
20
|
Teamocil::CLI.messages = []
|
26
|
-
end
|
21
|
+
end
|
27
22
|
|
28
|
-
it "creates a layout from a name" do
|
23
|
+
it "creates a layout from a name" do
|
29
24
|
@cli = Teamocil::CLI.new(["sample"], @fake_env)
|
30
25
|
@cli.layout.session.name.should == "sample"
|
31
26
|
@cli.layout.session.windows.length.should == 2
|
32
27
|
@cli.layout.session.windows.first.name.should == "foo"
|
33
28
|
@cli.layout.session.windows.last.name.should == "bar"
|
34
|
-
end
|
29
|
+
end
|
35
30
|
|
36
|
-
it "fails to create a layout from a layout that doesn’t exist" do
|
37
|
-
|
31
|
+
it "fails to create a layout from a layout that doesn’t exist" do
|
32
|
+
expect { @cli = Teamocil::CLI.new(["i-do-not-exist"], @fake_env) }.to raise_error SystemExit
|
38
33
|
Teamocil::CLI.messages.should include("There is no file \"#{File.join(File.dirname(__FILE__), "fixtures", ".teamocil", "i-do-not-exist.yml")}\"")
|
39
|
-
end
|
34
|
+
end
|
40
35
|
|
41
|
-
it "creates a layout from a specific file" do
|
36
|
+
it "creates a layout from a specific file" do
|
42
37
|
@cli = Teamocil::CLI.new(["--layout", "./spec/fixtures/.teamocil/sample.yml"], @fake_env)
|
43
38
|
@cli.layout.session.name.should == "sample"
|
44
39
|
@cli.layout.session.windows.length.should == 2
|
45
40
|
@cli.layout.session.windows.first.name.should == "foo"
|
46
41
|
@cli.layout.session.windows.last.name.should == "bar"
|
47
|
-
end
|
42
|
+
end
|
48
43
|
|
49
|
-
it "fails to create a layout from a file that doesn’t exist" do
|
50
|
-
|
44
|
+
it "fails to create a layout from a file that doesn’t exist" do
|
45
|
+
expect { @cli = Teamocil::CLI.new(["--layout", "./spec/fixtures/.teamocil/i-do-not-exist.yml"], @fake_env) }.to raise_error SystemExit
|
51
46
|
Teamocil::CLI.messages.should include("There is no file \"./spec/fixtures/.teamocil/i-do-not-exist.yml\"")
|
52
|
-
end
|
47
|
+
end
|
53
48
|
|
54
|
-
it "lists available layouts" do
|
49
|
+
it "lists available layouts" do
|
55
50
|
@cli = Teamocil::CLI.new(["--list"], @fake_env)
|
56
51
|
@cli.layouts.should == ["sample", "sample-2"]
|
57
|
-
end
|
52
|
+
end
|
58
53
|
|
59
|
-
it "should show the content" do
|
54
|
+
it "should show the content" do
|
60
55
|
FileUtils.stub(:touch)
|
61
56
|
Kernel.should_receive(:system).with("cat #{File.join(@fake_env["HOME"], ".teamocil", "sample.yml").inspect}")
|
62
57
|
Teamocil::CLI.new(["--show", "sample"], @fake_env)
|
63
|
-
end
|
58
|
+
end
|
64
59
|
|
65
|
-
it "looks only in the $TEAMOCIL_PATH environment variable for layouts" do
|
60
|
+
it "looks only in the $TEAMOCIL_PATH environment variable for layouts" do
|
66
61
|
@fake_env = { "TMUX" => 1, "HOME" => File.join(File.dirname(__FILE__), "fixtures"), "TEAMOCIL_PATH" => File.join(File.dirname(__FILE__), "fixtures/.my-fancy-layouts-directory") }
|
67
62
|
|
68
63
|
@cli = Teamocil::CLI.new(["sample-3"], @fake_env)
|
69
64
|
@cli.layout.session.name.should == "sample-3"
|
70
65
|
|
71
|
-
|
66
|
+
expect { @cli = Teamocil::CLI.new(["sample"], @fake_env) }.to raise_error SystemExit
|
72
67
|
Teamocil::CLI.messages.should include("There is no file \"#{@fake_env["TEAMOCIL_PATH"]}/sample.yml\"")
|
73
|
-
end
|
74
|
-
|
68
|
+
end
|
75
69
|
end
|
76
|
-
|
77
70
|
end
|
78
71
|
end
|
data/spec/layout_spec.rb
CHANGED
@@ -2,149 +2,149 @@
|
|
2
2
|
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
3
3
|
|
4
4
|
describe Teamocil::Layout do
|
5
|
+
let(:window_pane_base_index) { 0 }
|
6
|
+
before { Teamocil::Layout::Window.any_instance.stub(:pane_base_index).and_return(window_pane_base_index) }
|
5
7
|
|
6
8
|
context "compiling" do
|
7
|
-
before do
|
9
|
+
before do
|
8
10
|
@layout = Teamocil::Layout.new(layouts["two-windows"], {})
|
9
|
-
end
|
11
|
+
end
|
10
12
|
|
11
|
-
describe "handles bad layouts" do
|
12
|
-
it "does not compile without windows" do
|
13
|
+
describe "handles bad layouts" do
|
14
|
+
it "does not compile without windows" do
|
13
15
|
@layout = Teamocil::Layout.new({ "name" => "foo" }, {})
|
14
|
-
|
15
|
-
end
|
16
|
+
expect { @layout.compile! }.to raise_error Teamocil::Error::LayoutError
|
17
|
+
end
|
16
18
|
|
17
|
-
it "does not compile without splits" do
|
19
|
+
it "does not compile without splits" do
|
18
20
|
@layout = Teamocil::Layout.new({ "windows" => [{ "name" => "foo" }] }, {})
|
19
|
-
|
20
|
-
end
|
21
|
+
expect { @layout.compile! }.to raise_error Teamocil::Error::LayoutError
|
22
|
+
end
|
21
23
|
|
22
|
-
it "does not compile with empty splits" do
|
24
|
+
it "does not compile with empty splits" do
|
23
25
|
@layout = Teamocil::Layout.new({ "windows" => [{ "name" => "foo", "splits" => [nil, nil] }] }, {})
|
24
|
-
|
25
|
-
end
|
26
|
-
end
|
26
|
+
expect { @layout.compile! }.to raise_error Teamocil::Error::LayoutError
|
27
|
+
end
|
28
|
+
end
|
27
29
|
|
28
|
-
describe "windows" do
|
29
|
-
it "creates windows" do
|
30
|
+
describe "windows" do
|
31
|
+
it "creates windows" do
|
30
32
|
session = @layout.compile!
|
31
33
|
session.windows.each do |window|
|
32
34
|
window.should be_an_instance_of Teamocil::Layout::Window
|
33
35
|
end
|
34
|
-
end
|
36
|
+
end
|
35
37
|
|
36
|
-
it "creates windows with names" do
|
38
|
+
it "creates windows with names" do
|
37
39
|
session = @layout.compile!
|
38
40
|
session.windows[0].name.should == "foo"
|
39
41
|
session.windows[1].name.should == "bar"
|
40
|
-
end
|
42
|
+
end
|
41
43
|
|
42
|
-
it "creates windows with root paths" do
|
44
|
+
it "creates windows with root paths" do
|
43
45
|
session = @layout.compile!
|
44
46
|
session.windows[0].root.should == "/foo"
|
45
47
|
session.windows[1].root.should == "/bar"
|
46
|
-
end
|
48
|
+
end
|
47
49
|
|
48
|
-
it "creates windows with clear option" do
|
50
|
+
it "creates windows with clear option" do
|
49
51
|
session = @layout.compile!
|
50
52
|
session.windows[0].clear.should == "clear"
|
51
53
|
session.windows[1].clear.should be_nil
|
52
|
-
end
|
54
|
+
end
|
53
55
|
|
54
|
-
it "creates windows with layout option" do
|
56
|
+
it "creates windows with layout option" do
|
55
57
|
session = @layout.compile!
|
56
58
|
session.windows[0].layout.should == "tiled"
|
57
59
|
session.windows[1].layout.should be_nil
|
58
|
-
end
|
59
|
-
end
|
60
|
+
end
|
61
|
+
end
|
60
62
|
|
61
|
-
describe "splits" do
|
62
|
-
it "creates splits" do
|
63
|
+
describe "splits" do
|
64
|
+
it "creates splits" do
|
63
65
|
session = @layout.compile!
|
64
66
|
session.windows.first.splits.each do |split|
|
65
67
|
split.should be_an_instance_of Teamocil::Layout::Split
|
66
68
|
end
|
67
|
-
end
|
69
|
+
end
|
68
70
|
|
69
|
-
it "creates splits with dimensions" do
|
71
|
+
it "creates splits with dimensions" do
|
70
72
|
session = @layout.compile!
|
71
73
|
session.windows.first.splits[0].width.should == nil
|
72
74
|
session.windows.first.splits[1].width.should == 50
|
73
|
-
end
|
75
|
+
end
|
74
76
|
|
75
|
-
it "creates splits with commands specified in strings" do
|
77
|
+
it "creates splits with commands specified in strings" do
|
76
78
|
session = @layout.compile!
|
77
79
|
session.windows.first.splits[0].cmd.should == "echo 'foo'"
|
78
|
-
end
|
80
|
+
end
|
79
81
|
|
80
|
-
it "creates splits with commands specified in an array" do
|
82
|
+
it "creates splits with commands specified in an array" do
|
81
83
|
session = @layout.compile!
|
82
84
|
session.windows.last.splits[0].cmd.length.should == 2
|
83
85
|
session.windows.last.splits[0].cmd.first.should == "echo 'bar'"
|
84
86
|
session.windows.last.splits[0].cmd.last.should == "echo 'bar in an array'"
|
85
|
-
end
|
87
|
+
end
|
86
88
|
|
87
|
-
it "handles focused splits" do
|
89
|
+
it "handles focused splits" do
|
88
90
|
session = @layout.compile!
|
89
91
|
session.windows.last.splits[1].focus.should be_true
|
90
92
|
session.windows.last.splits[0].focus.should be_false
|
91
|
-
end
|
92
|
-
end
|
93
|
+
end
|
94
|
+
end
|
93
95
|
|
94
|
-
describe "filters" do
|
95
|
-
it "creates windows with before filters" do
|
96
|
+
describe "filters" do
|
97
|
+
it "creates windows with before filters" do
|
96
98
|
layout = Teamocil::Layout.new(layouts["two-windows-with-filters"], {})
|
97
99
|
session = layout.compile!
|
98
100
|
session.windows.first.filters["before"].length.should == 2
|
99
101
|
session.windows.first.filters["before"].first.should == "echo first before filter"
|
100
102
|
session.windows.first.filters["before"].last.should == "echo second before filter"
|
101
|
-
end
|
103
|
+
end
|
102
104
|
|
103
|
-
it "creates windows with after filters" do
|
105
|
+
it "creates windows with after filters" do
|
104
106
|
layout = Teamocil::Layout.new(layouts["two-windows-with-filters"], {})
|
105
107
|
session = layout.compile!
|
106
108
|
session.windows.first.filters["after"].length.should == 2
|
107
109
|
session.windows.first.filters["after"].first.should == "echo first after filter"
|
108
110
|
session.windows.first.filters["after"].last.should == "echo second after filter"
|
109
|
-
end
|
111
|
+
end
|
110
112
|
|
111
|
-
it "should handle blank filters" do
|
113
|
+
it "should handle blank filters" do
|
112
114
|
session = @layout.compile!
|
113
115
|
session.windows.first.filters.should have_key "after"
|
114
116
|
session.windows.first.filters.should have_key "before"
|
115
117
|
session.windows.first.filters["after"].should be_empty
|
116
118
|
session.windows.first.filters["before"].should be_empty
|
117
|
-
end
|
118
|
-
end
|
119
|
+
end
|
120
|
+
end
|
119
121
|
|
120
|
-
describe "targets" do
|
121
|
-
it "should handle splits without a target" do
|
122
|
+
describe "targets" do
|
123
|
+
it "should handle splits without a target" do
|
122
124
|
session = @layout.compile!
|
123
125
|
session.windows.last.splits.last.target.should == nil
|
124
|
-
end
|
126
|
+
end
|
125
127
|
|
126
|
-
it "should handle splits with a target" do
|
128
|
+
it "should handle splits with a target" do
|
127
129
|
session = @layout.compile!
|
128
130
|
session.windows.last.splits.first.target.should == "bottom-right"
|
129
|
-
end
|
130
|
-
end
|
131
|
+
end
|
132
|
+
end
|
131
133
|
|
132
|
-
describe "sessions" do
|
133
|
-
it "should handle windows within a session" do
|
134
|
+
describe "sessions" do
|
135
|
+
it "should handle windows within a session" do
|
134
136
|
layout = Teamocil::Layout.new(layouts["three-windows-within-a-session"], {})
|
135
137
|
session = layout.compile!
|
136
138
|
session.windows.length.should == 3
|
137
139
|
session.name.should == "my awesome session"
|
138
|
-
end
|
139
|
-
end
|
140
|
+
end
|
141
|
+
end
|
140
142
|
end
|
141
143
|
|
142
144
|
context "generating commands" do
|
143
|
-
before
|
144
|
-
@layout = Teamocil::Layout.new(layouts["two-windows"], {})
|
145
|
-
end # }}}
|
145
|
+
before { @layout = Teamocil::Layout.new(layouts["two-windows"], {}) }
|
146
146
|
|
147
|
-
it "should generate split commands" do
|
147
|
+
it "should generate split commands" do
|
148
148
|
session = @layout.compile!
|
149
149
|
commands = session.windows.last.splits[0].generate_commands
|
150
150
|
commands.length.should == 2
|
@@ -156,14 +156,25 @@ describe Teamocil::Layout do
|
|
156
156
|
commands.length.should == 2
|
157
157
|
commands.first.should == "tmux send-keys -t 0 \"export TEAMOCIL=1 && cd \"/foo\" && clear && echo 'foo'\""
|
158
158
|
commands.last.should == "tmux send-keys -t 0 Enter"
|
159
|
-
end
|
159
|
+
end
|
160
160
|
|
161
|
-
it "should generate window commands" do
|
161
|
+
it "should generate window commands" do
|
162
162
|
session = @layout.compile!
|
163
163
|
commands = session.windows.last.generate_commands
|
164
164
|
commands.first.should == "tmux new-window -n \"bar\""
|
165
165
|
commands.last.should == "tmux select-pane -t 1"
|
166
|
-
end
|
167
|
-
|
166
|
+
end
|
167
|
+
|
168
|
+
context "with custom pane-base-index option" do
|
169
|
+
let(:window_pane_base_index) { 2 }
|
168
170
|
|
171
|
+
it "should generate split commands" do
|
172
|
+
session = @layout.compile!
|
173
|
+
commands = session.windows.last.splits[0].generate_commands
|
174
|
+
commands.length.should == 2
|
175
|
+
commands.first.should == "tmux send-keys -t 2 \"export TEAMOCIL=1 && cd \"/bar\" && echo 'bar' && echo 'bar in an array'\""
|
176
|
+
commands.last.should == "tmux send-keys -t 2 Enter"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
169
180
|
end
|
data/spec/mock/cli.rb
CHANGED
@@ -1,36 +1,33 @@
|
|
1
1
|
module Teamocil
|
2
2
|
module Mock
|
3
3
|
module CLI
|
4
|
-
|
5
|
-
def self.included(base) # {{{
|
4
|
+
def self.included(base)
|
6
5
|
base.class_eval do
|
7
6
|
|
8
7
|
# Return all messages
|
9
|
-
def self.messages
|
8
|
+
def self.messages
|
10
9
|
@@messages
|
11
|
-
end
|
10
|
+
end
|
12
11
|
|
13
12
|
# Change messages
|
14
|
-
def self.messages=(messages)
|
13
|
+
def self.messages=(messages)
|
15
14
|
@@messages = messages
|
16
|
-
end
|
15
|
+
end
|
17
16
|
|
18
17
|
# Do not print anything
|
19
|
-
def print_layouts
|
18
|
+
def print_layouts
|
20
19
|
# Nothing
|
21
|
-
end
|
20
|
+
end
|
22
21
|
|
23
22
|
# Print an error message and exit the utility
|
24
23
|
#
|
25
24
|
# @param msg [Mixed] something to print before exiting.
|
26
|
-
def bail(msg)
|
25
|
+
def bail(msg)
|
27
26
|
Teamocil::CLI.messages << msg
|
28
27
|
exit 1
|
29
|
-
end
|
30
|
-
|
28
|
+
end
|
31
29
|
end
|
32
|
-
end
|
33
|
-
|
30
|
+
end
|
34
31
|
end
|
35
32
|
end
|
36
33
|
end
|
data/spec/mock/layout.rb
CHANGED
@@ -1,18 +1,14 @@
|
|
1
1
|
module Teamocil
|
2
2
|
module Mock
|
3
3
|
module Layout
|
4
|
-
|
5
|
-
def self.included(base) # {{{
|
4
|
+
def self.included(base)
|
6
5
|
base.class_eval do
|
7
|
-
|
8
6
|
# Do not execute anything
|
9
7
|
def execute_commands(commands)
|
10
8
|
# Nothing
|
11
9
|
end
|
12
|
-
|
13
10
|
end
|
14
|
-
end
|
15
|
-
|
11
|
+
end
|
16
12
|
end
|
17
13
|
end
|
18
14
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,12 +6,10 @@ require File.join(File.dirname(__FILE__), "./mock/layout.rb")
|
|
6
6
|
require File.join(File.dirname(__FILE__), "./mock/cli.rb")
|
7
7
|
|
8
8
|
module Helpers
|
9
|
-
|
10
|
-
def layouts # {{{
|
9
|
+
def layouts
|
11
10
|
return @@examples if defined?(@@examples)
|
12
11
|
@@examples = YAML.load_file(File.join(File.dirname(__FILE__), "fixtures/layouts.yml"))
|
13
|
-
end
|
14
|
-
|
12
|
+
end
|
15
13
|
end
|
16
14
|
|
17
15
|
RSpec.configure do |c|
|
data/teamocil.gemspec
CHANGED
@@ -23,7 +23,7 @@ spec = Gem::Specification.new do |s|
|
|
23
23
|
|
24
24
|
# Dependencies
|
25
25
|
s.add_development_dependency "rake"
|
26
|
-
s.add_development_dependency "rspec"
|
26
|
+
s.add_development_dependency "rspec", '~> 2.13'
|
27
27
|
s.add_development_dependency "yard"
|
28
28
|
s.add_development_dependency "maruku"
|
29
29
|
end
|
metadata
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: teamocil
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: '0.4'
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Rémi Prévost
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2013-03-03 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rake
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - ! '>='
|
20
18
|
- !ruby/object:Gem::Version
|
@@ -22,7 +20,6 @@ dependencies:
|
|
22
20
|
type: :development
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - ! '>='
|
28
25
|
- !ruby/object:Gem::Version
|
@@ -30,23 +27,20 @@ dependencies:
|
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: rspec
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - ~>
|
36
32
|
- !ruby/object:Gem::Version
|
37
|
-
version: '
|
33
|
+
version: '2.13'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - ~>
|
44
39
|
- !ruby/object:Gem::Version
|
45
|
-
version: '
|
40
|
+
version: '2.13'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: yard
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
45
|
- - ! '>='
|
52
46
|
- !ruby/object:Gem::Version
|
@@ -54,7 +48,6 @@ dependencies:
|
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
52
|
- - ! '>='
|
60
53
|
- !ruby/object:Gem::Version
|
@@ -62,7 +55,6 @@ dependencies:
|
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: maruku
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
59
|
- - ! '>='
|
68
60
|
- !ruby/object:Gem::Version
|
@@ -70,7 +62,6 @@ dependencies:
|
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
66
|
- - ! '>='
|
76
67
|
- !ruby/object:Gem::Version
|
@@ -90,11 +81,11 @@ files:
|
|
90
81
|
- README.md
|
91
82
|
- Rakefile
|
92
83
|
- bin/teamocil
|
93
|
-
- examples/
|
94
|
-
- examples/
|
95
|
-
- examples/
|
96
|
-
- examples/
|
97
|
-
- examples/
|
84
|
+
- examples/four-splits.yml
|
85
|
+
- examples/one-and-three-splits.yml
|
86
|
+
- examples/six-splits.yml
|
87
|
+
- examples/two-horizontal-splits.yml
|
88
|
+
- examples/two-vertical-splits.yml
|
98
89
|
- lib/teamocil.rb
|
99
90
|
- lib/teamocil/cli.rb
|
100
91
|
- lib/teamocil/error.rb
|
@@ -115,33 +106,26 @@ files:
|
|
115
106
|
homepage: http://remiprev.github.com/teamocil
|
116
107
|
licenses:
|
117
108
|
- MIT
|
109
|
+
metadata: {}
|
118
110
|
post_install_message:
|
119
111
|
rdoc_options: []
|
120
112
|
require_paths:
|
121
113
|
- lib
|
122
114
|
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
-
none: false
|
124
115
|
requirements:
|
125
116
|
- - ! '>='
|
126
117
|
- !ruby/object:Gem::Version
|
127
118
|
version: '0'
|
128
|
-
segments:
|
129
|
-
- 0
|
130
|
-
hash: -2208554246657691212
|
131
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
-
none: false
|
133
120
|
requirements:
|
134
121
|
- - ! '>='
|
135
122
|
- !ruby/object:Gem::Version
|
136
123
|
version: '0'
|
137
|
-
segments:
|
138
|
-
- 0
|
139
|
-
hash: -2208554246657691212
|
140
124
|
requirements: []
|
141
125
|
rubyforge_project:
|
142
|
-
rubygems_version:
|
126
|
+
rubygems_version: 2.0.0
|
143
127
|
signing_key:
|
144
|
-
specification_version:
|
128
|
+
specification_version: 4
|
145
129
|
summary: Easy window and split layouts for tmux
|
146
130
|
test_files:
|
147
131
|
- spec/cli_spec.rb
|
@@ -1,13 +0,0 @@
|
|
1
|
-
windows:
|
2
|
-
- name: simple-four-splits
|
3
|
-
splits:
|
4
|
-
- cmd: "echo 'first split'"
|
5
|
-
- cmd: "echo 'second split'"
|
6
|
-
width: 50
|
7
|
-
focus: true
|
8
|
-
- cmd: "echo 'fourth split'"
|
9
|
-
height: 50
|
10
|
-
target: bottom-right
|
11
|
-
- cmd: "echo 'third split'"
|
12
|
-
height: 50
|
13
|
-
target: bottom-left
|
@@ -1,12 +0,0 @@
|
|
1
|
-
windows:
|
2
|
-
- name: simple-one-and-three-splits
|
3
|
-
splits:
|
4
|
-
- cmd: "echo 'first split'"
|
5
|
-
- cmd: "echo 'second split'"
|
6
|
-
width: 50
|
7
|
-
- cmd: "echo 'third split'"
|
8
|
-
height: 66
|
9
|
-
target: bottom-right
|
10
|
-
- cmd: "echo 'fourth split'"
|
11
|
-
height: 50
|
12
|
-
target: bottom-right
|
@@ -1,18 +0,0 @@
|
|
1
|
-
windows:
|
2
|
-
- name: simple-six-splits
|
3
|
-
splits:
|
4
|
-
- cmd: "echo 'first split'"
|
5
|
-
- cmd: "echo 'second split'"
|
6
|
-
width: 50
|
7
|
-
- cmd: "echo 'fourth split'"
|
8
|
-
height: 66
|
9
|
-
target: bottom-right
|
10
|
-
- cmd: "echo 'third split'"
|
11
|
-
height: 66
|
12
|
-
target: bottom-left
|
13
|
-
- cmd: "echo 'sixth split'"
|
14
|
-
height: 50
|
15
|
-
target: bottom-right
|
16
|
-
- cmd: "echo 'fifth split'"
|
17
|
-
height: 50
|
18
|
-
target: bottom-left
|