Compare commits

...

No commits in common. "gh-pages" and "master" have entirely different histories.

30 changed files with 925 additions and 16 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.gem
.rvmrc
.local*
.yardoc
doc
pkg

2
.travis.yml Normal file
View File

@ -0,0 +1,2 @@
rvm: 1.9.2
script: "bundle exec rake spec"

1
CNAME
View File

@ -1 +0,0 @@
teamocil.com

2
Gemfile Normal file
View File

@ -0,0 +1,2 @@
source "http://rubygems.org"
gemspec

32
Gemfile.lock Normal file
View File

@ -0,0 +1,32 @@
PATH
remote: .
specs:
teamocil (0.3.2)
GEM
remote: http://rubygems.org/
specs:
diff-lcs (1.1.3)
maruku (0.6.0)
syntax (>= 1.0.0)
rake (0.9.2)
rspec (2.6.0)
rspec-core (~> 2.6.0)
rspec-expectations (~> 2.6.0)
rspec-mocks (~> 2.6.0)
rspec-core (2.6.4)
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
syntax (1.0.0)
yard (0.7.5)
PLATFORMS
ruby
DEPENDENCIES
maruku
rake
rspec
teamocil!
yard

3
LICENSE Normal file
View File

@ -0,0 +1,3 @@
Copyright 2011-2012 Rémi Prévost.
You may use this work without restrictions, as long as this notice is included.
The work is provided "as is" without warranty of any kind, neither express nor implied.

204
README.md Normal file
View File

@ -0,0 +1,204 @@
# Teamocil
Teamocil is a simple tool used to automatically create sessions, windows and splits in [tmux](http://tmux.sourceforge.net/) with YAML files.
[![Build Status](https://secure.travis-ci.org/remiprev/teamocil.png)](http://travis-ci.org/remiprev/teamocil)
## Usage
```bash
$ gem install teamocil
$ mkdir ~/.teamocil
$ teamocil --edit sample
$ tmux
$ teamocil sample
```
## Options
* `--here` opens the session in the current window, it doesnt create an empty first window.
* `--layout` takes a custom file path to a YAML layout file.
* `--edit` opens the layout file (whether or not `--layout` is used) with `$EDITOR`.
* `--list` lists all available layouts.
## Layout file structure
A layout file is a single YAML file located in `~/.teamocil` (eg. `~/.teamocil/my-project.yml`).
### Session
You can wrap your entire layout file in a `session` and Teamocil will rename the current session (so that you can find it more easily when running `tmux list-sessions`) before creating your windows.
#### Keys
* `name` (the name of the session)
#### Example
```yaml
session:
name: "my-awesome-session"
windows:
[windows list]
```
### Windows
If you are not using a top-level `session` key, then the first key of your layout file will be `windows`, an array of window items.
#### Item keys
* `name` (the name that will appear in `tmux` statusbar)
* `root` (the directory in which every split will be created)
* `filters` (a hash of `before` and `after` commands to run for each split)
* `splits` (an array of split items)
* `options` (a hash of tmux options, see `man tmux` for a list)
#### Example
```yaml
windows:
- name: "my-first-window"
options:
synchronize-panes: true
root: "~/Projects/foo-www"
filters:
before:
- "echo 'Lets use ruby-1.9.2 for each split in this window.'"
- "rvm use 1.9.2"
splits:
[splits list]
- name: "my-second-window"
root: "~/Projects/foo-api"
splits:
[splits list]
- name: "my-third-window"
root: "~/Projects/foo-daemons"
splits:
[splits list]
```
### Splits
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.
#### Item keys
* `cmd` (the commands to initially execute in the split)
* `width` (the split width, in percentage)
* `height` (the split width, in percentage)
* `target` (the split to set focus on, before creating the current one)
#### Example
```yaml
windows:
- name: "my-first-window"
root: "~/Projects/foo-www"
filters:
before: "rvm use 1.9.2"
after: "echo 'I am done initializing this split.'"
splits:
- cmd: "git status"
- cmd: "bundle exec rails server --port 4000"
width: 50
- cmd:
- "sudo service memcached start"
- "sudo service mongodb start"
height: 50
```
## Layout examples
See more example files in the `examples` directory.
### Simple two splits window
#### Content of `~/.teamocil/sample-1.yml`
```yaml
windows:
- name: "sample-two-splits"
root: "~/Code/sample/www"
splits:
- cmd: ["pwd", "ls -la"]
- cmd: "rails server --port 3000"
width: 50
```
#### Result of `$ teamocil sample-1`
.------------------.------------------.
| (0) | (1) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
'------------------'------------------'
### Four tiled splits window
#### Content of `~/.teamocil/sample-2.yml`
```yaml
windows:
- name: "sample-four-splits"
root: "~/Code/sample/www"
splits:
- cmd: "pwd"
- cmd: "pwd"
width: 50
- cmd: "pwd"
height: 50
target: "bottom-right"
- cmd: "pwd"
height: 50
target: "bottom-left"
```
#### Result of `$ teamocil sample-2`
.------------------.------------------.
| (0) | (1) |
| | |
| | |
| | |
|------------------|------------------|
| (3) | (2) |
| | |
| | |
| | |
'------------------'------------------'
## Extras
### Zsh autocompletion
To get autocompletion when typing `teamocil <Tab>` in a zsh session, add this line to your `~/.zshrc` file:
```zsh
compctl -g '~/.teamocil/*(:t:r)' teamocil
```
## Todo list
* Making sure the layout is valid before executing it (ie. throw exceptions).
* Add more specs.
## Contributors
Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/teamocil/issues), just like these fine folks did:
* Samuel Garneau ([garno](https://github.com/garno))
* Jimmy Bourassa ([jbourassa](https://github.com/jbourassa))
Take a look at the `spec` folder before you do, and make sure `bundle exec rake spec` passes after your modifications :)
## License
Teamocil is © 2011-2012 [Rémi Prévost](http://exomel.com) and may be freely distributed under the [LITL license](http://litl.info/). See the `LICENSE` file.

28
Rakefile Normal file
View File

@ -0,0 +1,28 @@
require "bundler"
Bundler.require(:development)
require "bundler/gem_tasks"
require "rspec/core/rake_task"
task :default => :spec
desc "Run all specs"
RSpec::Core::RakeTask.new(:spec) do |task| # {{{
task.pattern = "spec/**/*_spec.rb"
task.rspec_opts = "--colour --format=documentation"
end # }}}
desc "Generate YARD Documentation"
YARD::Rake::YardocTask.new do |task| # {{{
task.options = [
"-o", File.expand_path("../doc", __FILE__),
"--readme=README.md",
"--markup=markdown",
"--markup-provider=maruku",
"--no-private",
"--no-cache",
"--protected",
"--title=Teamocil",
]
task.files = ["lib/**/*.rb"]
end # }}}

8
bin/teamocil Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env ruby
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
require 'yaml'
require 'teamocil'
Teamocil::CLI.new(ARGV, ENV)

View File

@ -0,0 +1,12 @@
windows:
- name: simple-four-splits
splits:
- cmd: "echo 'first split'"
- cmd: "echo 'second split'"
width: 50
- cmd: "echo 'fourth split'"
height: 50
target: bottom-right
- cmd: "echo 'third split'"
height: 50
target: bottom-left

View File

@ -0,0 +1,12 @@
windows:
- name: simple-one-and-three-splits
splits:
- cmd: "echo 'first split'"
- cmd: "echo 'second split'"
width: 50
- cmd: "echo 'third split'"
height: 66
target: bottom-right
- cmd: "echo 'fourth split'"
height: 50
target: bottom-right

View File

@ -0,0 +1,18 @@
windows:
- name: simple-six-splits
splits:
- cmd: "echo 'first split'"
- cmd: "echo 'second split'"
width: 50
- cmd: "echo 'fourth split'"
height: 66
target: bottom-right
- cmd: "echo 'third split'"
height: 66
target: bottom-left
- cmd: "echo 'sixth split'"
height: 50
target: bottom-right
- cmd: "echo 'fifth split'"
height: 50
target: bottom-left

View File

@ -0,0 +1,6 @@
windows:
- name: simple-two-horizontal-splits
splits:
- cmd: "echo 'first split'"
- cmd: "echo 'second split'"
height: 50

View File

@ -0,0 +1,6 @@
windows:
- name: simple-two-vertical-splits
splits:
- cmd: "echo 'first split'"
- cmd: "echo 'second split'"
width: 50

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="http://cdnjs.cloudflare.com/ajax/libs/documentup/latest.min.js"></script>
<script>
DocumentUp.document({
repo: "remiprev/teamocil",
travis: true
});
</script>
</head>
<body></body>
</html>

5
lib/teamocil.rb Normal file
View File

@ -0,0 +1,5 @@
module Teamocil
VERSION = "0.3.2"
autoload :Layout, "teamocil/layout"
autoload :CLI, "teamocil/cli"
end

94
lib/teamocil/cli.rb Normal file
View File

@ -0,0 +1,94 @@
require 'optparse'
require 'fileutils'
module Teamocil
# This class handles interaction with the `tmux` utility.
class CLI
attr_accessor :layout, :layouts
# Initialize a new run of `tmux`
#
# @param argv [Hash] the command line parameters hash (usually `ARGV`).
# @param env [Hash] the environment variables hash (usually `ENV`).
def initialize(argv, env) # {{{
parse_options! argv
layout_path = File.join("#{env["HOME"]}", ".teamocil")
if @options.include?(:list)
@layouts = get_layouts(layout_path)
return print_layouts
end
if @options.include?(:layout)
file = @options[:layout]
else
file = ::File.join(layout_path, "#{argv[0]}.yml")
end
if @options[:edit]
::FileUtils.touch file unless File.exists?(file)
Kernel.system("$EDITOR \"#{file}\"")
else
bail "There is no file \"#{file}\"" unless File.exists?(file)
bail "You must be in a tmux session to use teamocil" unless env["TMUX"]
parsed_layout = YAML.load_file(file)
@layout = Teamocil::Layout.new(parsed_layout, @options)
@layout.compile!
@layout.execute_commands(@layout.generate_commands)
end
end # }}}
# Parse the command line options
def parse_options!(args) # {{{
@options = {}
opts = ::OptionParser.new do |opts|
opts.banner = "Usage: teamocil [options] <layout>
Options:
"
opts.on("--here", "Set up the first window in the current window") do
@options[:here] = true
end
opts.on("--edit", "Edit the YAML layout file instead of using it") do
@options[:edit] = true
end
opts.on("--layout [LAYOUT]", "Use a specific layout file, instead of `~/.teamocil/<layout>.yml`") do |layout|
@options[:layout] = layout
end
opts.on("--list", "List all available layouts in `~/.teamocil/`") do
@options[:list] = true
end
end
opts.parse! args
end # }}}
# Return an array of available layouts
#
# @param path [String] the path used to look for layouts
def get_layouts(path) # {{{
Dir.glob(File.join(path, "*.yml")).map { |file| File.basename(file).gsub(/\..+$/, "") }.sort
end # }}}
# Print each layout on a single line
def print_layouts # {{{
STDOUT.puts @layouts.join("\n")
exit 0
end # }}}
# Print an error message and exit the utility
#
# @param msg [Mixed] something to print before exiting.
def bail(msg) # {{{
STDERR.puts "[teamocil] #{msg}"
exit 1
end # }}}
end
end

46
lib/teamocil/layout.rb Normal file
View File

@ -0,0 +1,46 @@
module Teamocil
# This class act as a wrapper around a tmux YAML layout file
class Layout
autoload :Session, "teamocil/layout/session"
autoload :Window, "teamocil/layout/window"
autoload :Split, "teamocil/layout/split"
attr_reader :session
# Initialize a new layout from a hash
#
# @param layout [Hash] the parsed layout
# @param options [Hash] some options
def initialize(layout, options={}) # {{{
@layout = layout
@options = options
end # }}}
# Generate tmux commands based on the data found in the layout file
#
# @return [Array] an array of shell commands to send
def generate_commands # {{{
@session.generate_commands
end # }}}
# Compile the layout into objects
#
# @return [Session]
def compile! # {{{
if @layout["session"].nil?
@session = Session.new @options, "windows" => @layout["windows"]
else
@session = Session.new @options, @layout["session"]
end
end # }}}
# Execute each command in the shell
#
# @param commands [Array] an array of complete commands to send to the shell
def execute_commands(commands) # {{{
`#{commands.join("; ")}`
end # }}}
end
end

View File

@ -0,0 +1,31 @@
module Teamocil
class Layout
# This class represents a session within tmux
class Session
attr_reader :options, :windows, :name
# Initialize a new tmux session
#
# @param options [Hash] the options, mostly passed by the CLI
# @param attrs [Hash] the session data from the layout file
def initialize(options, attrs={}) # {{{
@name = attrs["name"]
@windows = attrs["windows"].each_with_index.map { |window, window_index| Window.new(self, window_index, window) }
@options = options
end # }}}
# Generate commands to send to tmux
#
# @return [Array]
def generate_commands # {{{
commands = []
commands << "tmux rename-session \"#{@name}\"" unless @name.nil?
commands << @windows.map(&:generate_commands)
commands << "tmux select-pane -t 0"
commands
end # }}}
end
end
end

View File

@ -0,0 +1,64 @@
module Teamocil
class Layout
# This class represents a split within a tmux window
class Split
attr_reader :width, :height, :cmd, :index, :target
# Initialize a new tmux split
#
# @param session [Session] the window where the split is initialized
# @param index [Fixnnum] the split index
# @param attrs [Hash] the split data from the layout file
def initialize(window, index, attrs={}) # {{{
@height = attrs["height"]
@width = attrs["width"]
@cmd = attrs["cmd"]
@target = attrs["target"]
@window = window
@index = index
end # }}}
# Generate commands to send to tmux
#
# @return [Array]
def generate_commands # {{{
commands = []
# Is it a vertical or horizontal split?
init_command = ""
unless @index == 0
if !@width.nil?
init_command = "tmux split-window -h -p #{@width}"
elsif !@height.nil?
init_command = "tmux split-window -p #{@height}"
else
init_command = "tmux split-window"
end
init_command << " -t #{@target}" unless @target.nil?
commands << init_command
end
# Wrap all commands around filters
@cmd = [@window.filters["before"]] + [@cmd] + [@window.filters["after"]]
# If a `root` key exist, start each split in this directory
@cmd.unshift "cd \"#{@window.root}\"" unless @window.root.nil?
# Set the TEAMOCIL environment variable
@cmd.unshift "export TEAMOCIL=1"
# Execute each split command
@cmd.flatten.compact.each do |command|
commands << "tmux send-keys -t #{@index} \"#{command}\""
commands << "tmux send-keys -t #{@index} Enter"
end
commands
end # }}}
end
end
end

View File

@ -0,0 +1,55 @@
module Teamocil
class Layout
# This class represents a window within tmux
class Window
attr_reader :filters, :root, :splits, :options, :index, :name
# Initialize a new tmux window
#
# @param session [Session] the session where the window is initialized
# @param index [Fixnnum] the window index
# @param attrs [Hash] the window data from the layout file
def initialize(session, index, attrs={}) # {{{
@name = attrs["name"]
@root = attrs["root"]
@options = attrs["options"] || {}
@splits = attrs["splits"] || []
@splits = @splits.each_with_index.map { |split, split_index| Split.new(self, split_index, split) }
@filters = attrs["filters"] || {}
@filters["before"] ||= []
@filters["after"] ||= []
@index = index
@session = session
end # }}}
# Generate commands to send to tmux
#
# @return [Array]
def generate_commands # {{{
commands = []
if @session.options.include?(:here) and @index == 0
commands << "tmux rename-window \"#{@name}\""
else
commands << "tmux new-window -n \"#{@name}\""
end
commands << @splits.map(&:generate_commands)
@options.each_pair do |option, value|
value = "on" if value === true
value = "off" if value === false
commands << "tmux set-window-option #{option} #{value}"
end
commands
end # }}}
end
end
end

43
spec/cli_spec.rb Normal file
View File

@ -0,0 +1,43 @@
require File.join(File.dirname(__FILE__), "spec_helper.rb")
describe Teamocil::CLI do
context "executing" do
context "not in tmux" do
before do # {{{
@fake_env = { "TMUX" => 1, "HOME" => File.join(File.dirname(__FILE__), "fixtures") }
end # }}}
it "should allow editing" do # {{{
FileUtils.stub(:touch)
Kernel.should_receive(:system).with(any_args())
@cli = Teamocil::CLI.new(["--edit", "my-layout"], @fake_env)
end # }}}
end
context "in tmux" do
before do # {{{
@fake_env = { "TMUX" => 1, "HOME" => File.join(File.dirname(__FILE__), "fixtures") }
end # }}}
it "creates a layout" do # {{{
@cli = Teamocil::CLI.new(["sample"], @fake_env)
@cli.layout.session.name.should == "sample"
@cli.layout.session.windows.length.should == 2
@cli.layout.session.windows.first.name.should == "foo"
@cli.layout.session.windows.last.name.should == "bar"
end # }}}
it "lists available layouts" do # {{{
@cli = Teamocil::CLI.new(["--list"], @fake_env)
@cli.layouts.should == ["sample", "sample-2"]
end # }}}
end
end
end

10
spec/fixtures/.teamocil/sample-2.yml vendored Normal file
View File

@ -0,0 +1,10 @@
session:
name: sample-2
root: ~
windows:
- name: "foo"
splits:
- cmd: "pwd"
- name: "bar"
splits:
- cmd: "pwd"

10
spec/fixtures/.teamocil/sample.yml vendored Normal file
View File

@ -0,0 +1,10 @@
session:
name: sample
root: ~
windows:
- name: "foo"
splits:
- cmd: "pwd"
- name: "bar"
splits:
- cmd: "pwd"

43
spec/fixtures/layouts.yml vendored Normal file
View File

@ -0,0 +1,43 @@
# Simple two windows layout
two-windows:
windows:
- name: "foo"
root: "/foo"
splits:
- cmd: "echo 'foo'"
- cmd: "echo 'foo again'"
width: 50
- name: "bar"
root: "/bar"
splits:
- cmd:
- "echo 'bar'"
- "echo 'bar in an array'"
target: bottom-right
- cmd: "echo 'bar again'"
width: 50
# Simple two windows layout with filters
two-windows-with-filters:
windows:
- name: "foo"
root: "/foo"
filters:
before:
- "echo first before filter"
- "echo second before filter"
after:
- "echo first after filter"
- "echo second after filter"
splits:
- cmd: "echo 'foo'"
- cmd: "echo 'foo again'"
width: 50
three-windows-within-a-session:
session:
name: "my awesome session"
windows:
- name: "first window"
- name: "second window"
- name: "third window"

98
spec/layout_spec.rb Normal file
View File

@ -0,0 +1,98 @@
require File.join(File.dirname(__FILE__), "spec_helper.rb")
describe Teamocil::Layout do
context "compiling" do
before do # {{{
@layout = Teamocil::Layout.new(layouts["two-windows"], {})
end # }}}
it "creates windows" do # {{{
session = @layout.compile!
session.windows.each do |window|
window.should be_an_instance_of Teamocil::Layout::Window
end
end # }}}
it "creates windows with names" do # {{{
session = @layout.compile!
session.windows[0].name.should == "foo"
session.windows[1].name.should == "bar"
end # }}}
it "creates windows with root paths" do # {{{
session = @layout.compile!
session.windows[0].root.should == "/foo"
session.windows[1].root.should == "/bar"
end # }}}
it "creates splits" do # {{{
session = @layout.compile!
session.windows.first.splits.each do |split|
split.should be_an_instance_of Teamocil::Layout::Split
end
end # }}}
it "creates splits with dimensions" do # {{{
session = @layout.compile!
session.windows.first.splits[0].width.should == nil
session.windows.first.splits[1].width.should == 50
end # }}}
it "creates splits with commands specified in strings" do # {{{
session = @layout.compile!
session.windows.first.splits[0].cmd.should == "echo 'foo'"
end # }}}
it "creates splits with commands specified in an array" do # {{{
session = @layout.compile!
session.windows.last.splits[0].cmd.length.should == 2
session.windows.last.splits[0].cmd.first.should == "echo 'bar'"
session.windows.last.splits[0].cmd.last.should == "echo 'bar in an array'"
end # }}}
it "creates windows with before filters" do # {{{
layout = Teamocil::Layout.new(layouts["two-windows-with-filters"], {})
session = layout.compile!
session.windows.first.filters["before"].length.should == 2
session.windows.first.filters["before"].first.should == "echo first before filter"
session.windows.first.filters["before"].last.should == "echo second before filter"
end # }}}
it "creates windows with after filters" do # {{{
layout = Teamocil::Layout.new(layouts["two-windows-with-filters"], {})
session = layout.compile!
session.windows.first.filters["after"].length.should == 2
session.windows.first.filters["after"].first.should == "echo first after filter"
session.windows.first.filters["after"].last.should == "echo second after filter"
end # }}}
it "should handle blank filters" do # {{{
session = @layout.compile!
session.windows.first.filters.should have_key "after"
session.windows.first.filters.should have_key "before"
session.windows.first.filters["after"].should be_empty
session.windows.first.filters["before"].should be_empty
end # }}}
it "should handle splits without a target" do # {{{
session = @layout.compile!
session.windows.last.splits.last.target.should == nil
end # }}}
it "should handle splits with a target" do # {{{
session = @layout.compile!
session.windows.last.splits.first.target.should == "bottom-right"
end # }}}
it "should handle windows within a session" do # {{{
layout = Teamocil::Layout.new(layouts["three-windows-within-a-session"], {})
session = layout.compile!
session.windows.length.should == 3
session.name.should == "my awesome session"
end # }}}
end
end

20
spec/mock/cli.rb Normal file
View File

@ -0,0 +1,20 @@
module Teamocil
module Mock
module CLI
def self.included(base) # {{{
base.class_eval do
# Do not print anything
def print_layouts
# Nothing
end
end
end # }}}
end
end
end
Teamocil::CLI.send :include, Teamocil::Mock::CLI

20
spec/mock/layout.rb Normal file
View File

@ -0,0 +1,20 @@
module Teamocil
module Mock
module Layout
def self.included(base) # {{{
base.class_eval do
# Do not execute anything
def execute_commands(commands)
# Nothing
end
end
end # }}}
end
end
end
Teamocil::Layout.send :include, Teamocil::Mock::Layout

19
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,19 @@
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
require 'yaml'
require 'teamocil'
require File.join(File.dirname(__FILE__), "./mock/layout.rb")
require File.join(File.dirname(__FILE__), "./mock/cli.rb")
module Helpers
def layouts # {{{
return @@examples if defined?(@@examples)
@@examples = YAML.load_file(File.join(File.dirname(__FILE__), "fixtures/layouts.yml"))
end # }}}
end
RSpec.configure do |c|
c.include Helpers
end

28
teamocil.gemspec Normal file
View File

@ -0,0 +1,28 @@
# encoding: utf-8
$:.push File.expand_path("../lib", __FILE__)
require "teamocil"
spec = Gem::Specification.new do |s|
# Metadata
s.name = "teamocil"
s.version = Teamocil::VERSION
s.platform = Gem::Platform::RUBY
s.authors = "Rémi Prévost"
s.email = "remi@exomel.com"
s.homepage = "http://github.com/remiprev/teamocil"
s.summary = "Easy window and split layouts for tmux"
s.description = "Teamocil helps you set up window and splits layouts for tmux using YAML configuration files."
# Manifest
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
# Dependencies
s.add_development_dependency "rake"
s.add_development_dependency "rspec"
s.add_development_dependency "yard"
s.add_development_dependency "maruku"
end