Compare commits

..

No commits in common. "master" and "feature-refactor-classes" have entirely different histories.

22 changed files with 240 additions and 466 deletions

1
.gitignore vendored
View File

@ -3,4 +3,3 @@
.local*
.yardoc
doc
pkg

View File

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

13
.yardopts Normal file
View File

@ -0,0 +1,13 @@
--title "Teamocil"
--no-cache
--protected
--no-private
--markup "markdown"
--markup-provider "maruku"
--format html
"README.mkd"
"lib/**/*.rb"

View File

@ -1,7 +1,7 @@
PATH
remote: .
specs:
teamocil (0.3.2)
teamocil (0.1.11)
GEM
remote: http://rubygems.org/
@ -19,7 +19,7 @@ GEM
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
syntax (1.0.0)
yard (0.7.5)
yard (0.7.2)
PLATFORMS
ruby

View File

@ -1,3 +1,3 @@
Copyright 2011-2012 Rémi Prévost.
Copyright 2011 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.

157
README.md
View File

@ -2,24 +2,19 @@
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
```
$ 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
@ -35,12 +30,10 @@ You can wrap your entire layout file in a `session` and Teamocil will rename the
#### Example
```yaml
session:
name: "my-awesome-session"
windows:
[windows list]
```
session:
name: my-awesome-session
windows:
[windows list]
### Windows
@ -56,27 +49,27 @@ If you are not using a top-level `session` key, then the first key of your layou
#### 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]
```
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
filters:
after: "rvm use 1.9.2"
splits:
[splits list]
- name: my-third-window
root: ~/Projects/foo-daemons
splits:
[splits list]
### Splits
@ -91,22 +84,20 @@ Every window must define an array of splits that will be created within it. A ve
#### 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
```
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
@ -116,16 +107,15 @@ See more example files in the `examples` directory.
#### 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
```
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`
@ -145,21 +135,19 @@ windows:
#### 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"
```
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`
@ -181,14 +169,13 @@ windows:
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
```
compctl -g '~/.teamocil/*(:t:r)' teamocil
## Todo list
* Making sure the layout is valid before executing it (ie. throw exceptions).
* Add more specs.
* Support tmux options for windows and splits (eg. `synchronize-panes`)
* Making sure the layout is valid before executing it
* Refactor the `Layout` class to make it “test-friendly”
## Contributors
@ -197,8 +184,6 @@ Feel free to contribute and submit issues/pull requests [on GitHub](https://gith
* 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.
Teamocil is © 2011 [Rémi Prévost](http://exomel.com) and may be freely distributed under the [LITL license](http://litl.info/). See the `LICENSE` file.

View File

@ -1,28 +1,11 @@
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"
desc "Run specs"
task :spec do # {{{
sh "bundle exec rspec --color --format=nested #{Dir.glob(File.join(File.dirname(__FILE__), "spec/**/*_spec.rb")).join(" ")}"
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"]
desc "Generate documentation"
task :doc do # {{{
sh "bundle exec yard doc"
end # }}}

View File

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

View File

@ -5,44 +5,34 @@ 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
bail "You must be in a tmux session to use teamocil" unless env["TMUX"]
parse_options!
if @options.include?(:layout)
file = @options[:layout]
file = options[:layout]
else
file = ::File.join(layout_path, "#{argv[0]}.yml")
file = ::File.join("#{env["HOME"]}/.teamocil", "#{argv[0]}.yml")
end
if @options[:edit]
::FileUtils.touch file unless File.exists?(file)
Kernel.system("$EDITOR \"#{file}\"")
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)
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) # {{{
def parse_options! # {{{
@options = {}
opts = ::OptionParser.new do |opts|
opts.banner = "Usage: teamocil [options] <layout>
@ -57,36 +47,19 @@ module Teamocil
@options[:edit] = true
end
opts.on("--layout [LAYOUT]", "Use a specific layout file, instead of `~/.teamocil/<layout>.yml`") do |layout|
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
opts.parse!
end # }}}
# Print an error message and exit the utility
#
# @param msg [Mixed] something to print before exiting.
def bail(msg) # {{{
STDERR.puts "[teamocil] #{msg}"
puts "[teamocil] #{msg}"
exit 1
end # }}}

View File

@ -2,11 +2,135 @@ 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
# 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, index| Window.new(self, 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
# 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"]
@filters = attrs["filters"]
@splits = attrs["splits"].each_with_index.map { |split, index| Split.new(self, index, split) }
@index = index
@session = session
@options ||= {}
@filters ||= {}
@filters["before"] ||= []
@filters["after"] ||= []
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
# 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?
unless @index == 0
if !@width.nil?
commands << "tmux split-window -h -p #{@width}"
elsif !@height.nil?
commands << "tmux split-window -p #{@height}"
else
commands << "tmux split-window"
end
commands << " -t #{@target}" unless @target.nil?
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?
# 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
# Initialize a new layout from a hash
#

View File

@ -1,31 +0,0 @@
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

@ -1,64 +0,0 @@
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

@ -1,55 +0,0 @@
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

View File

@ -1,43 +0,0 @@
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

View File

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

View File

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

View File

@ -13,7 +13,6 @@ two-windows:
- cmd:
- "echo 'bar'"
- "echo 'bar in an array'"
target: bottom-right
- cmd: "echo 'bar again'"
width: 50
@ -33,11 +32,3 @@ two-windows-with-filters:
- 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"

View File

@ -2,19 +2,15 @@ require File.join(File.dirname(__FILE__), "spec_helper.rb")
describe Teamocil::Layout do
context "initializing" do
end
context "compiling" do
before do # {{{
before :each 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"
@ -27,13 +23,6 @@ describe Teamocil::Layout do
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
@ -68,31 +57,5 @@ describe Teamocil::Layout do
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

View File

@ -1,20 +0,0 @@
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

View File

@ -1,20 +0,0 @@
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

View File

@ -2,8 +2,6 @@ $:.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

View File

@ -1,5 +1,4 @@
# encoding: utf-8
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "teamocil"
@ -25,4 +24,5 @@ spec = Gem::Specification.new do |s|
s.add_development_dependency "rspec"
s.add_development_dependency "yard"
s.add_development_dependency "maruku"
end