├── .gitignore ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── infect ├── infect.gemspec ├── lib ├── infect.rb └── infect │ ├── cleanup.rb │ ├── colorize.rb │ ├── command.rb │ ├── command │ ├── package.rb │ ├── plugin.rb │ └── prereqs.rb │ ├── runner.rb │ ├── standalone.rb │ └── version.rb ├── spec ├── command_spec.rb ├── runner_spec.rb └── standalone_spec.rb └── standalone └── infect /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | 1. Fork it 4 | 2. Create your feature branch (`git checkout -b my-new-feature`) 5 | 3. Commit your changes (`git commit -am 'Add some feature'`) 6 | 4. Push to the branch (`git push origin my-new-feature`) 7 | 5. Create new Pull Request 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in infect.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | infect (1.1.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | byebug (2.7.0) 10 | columnize (~> 0.3) 11 | debugger-linecache (~> 1.2) 12 | coderay (1.0.9) 13 | columnize (0.9.0) 14 | debugger-linecache (1.2.0) 15 | diff-lcs (1.1.3) 16 | method_source (0.8.1) 17 | pry (0.9.12.2) 18 | coderay (~> 1.0.5) 19 | method_source (~> 0.8) 20 | slop (~> 3.4) 21 | pry-byebug (1.3.2) 22 | byebug (~> 2.7) 23 | pry (~> 0.9.12) 24 | rake (10.1.0) 25 | rspec (2.12.0) 26 | rspec-core (~> 2.12.0) 27 | rspec-expectations (~> 2.12.0) 28 | rspec-mocks (~> 2.12.0) 29 | rspec-core (2.12.2) 30 | rspec-expectations (2.12.1) 31 | diff-lcs (~> 1.1.3) 32 | rspec-mocks (2.12.1) 33 | slop (3.4.5) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | infect! 40 | pry-byebug 41 | rake 42 | rspec 43 | 44 | BUNDLED WITH 45 | 1.15.3 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Christopher Sexton 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ☣ Infect 2 | 3 | Package manager for [Vim 8](https://github.com/vim/vim/blob/master/runtime/doc/version8.txt#L97-L103). 4 | 5 | The only package manager that makes no impact on start up time. 6 | 7 | Manage your entire vim config with a single `.vimrc` file, while keeping the `.vimrc` file functional on systems with out any custom plugins installed, or with older versions of Vim. 8 | 9 | ## Installation 10 | 11 | Infect has no dependencies other than a recentish version of ruby. And can be installed as a standalone script, perhaps in your `~/bin` directory. 12 | 13 | $ curl https://raw.githubusercontent.com/csexton/infect/master/standalone/infect > ~/bin/infect && chmod +x ~/bin/infect 14 | 15 | Or if you prefer to manage it at a gem: 16 | 17 | $ gem install infect 18 | 19 | ## Objective 20 | 21 | The point of Infect it to make it easy to manage your vim config. You should be able to check in your `.vimrc` into source control and use that **one** file to easily install any plugins or packages you need. 22 | 23 | ## Rationale 24 | 25 | Why invent another way of managing vim plugins? 26 | 27 | * I want to **be able** to use my `.vimrc` when without installing plugins. 28 | * I like having simple command line apps to manage my setup. 29 | * I wanted to the **built-in** plugin loading system. 30 | * I was tired of managing git submodules, nor did I find it very scalable. 31 | * I do not want my plugin manager to **affect the start up time** of my editor. 32 | 33 | Many of the other plugins mangers are really slick, but every one I have seen has violated at least one of those. 34 | 35 | I don't really want to use my editor for installing stuff. Feels like it goes against the Vim philosophy. Bram said "Each program has its own task and should be good at it" and think installing things is better suited for a command line script. 36 | 37 | ## Usage 38 | 39 | Infect reads your `.vimrc` file and looks for magic comments. It uses those to install vim packages and plugins. A minimal `.vimrc` to use with infect might look like this: 40 | 41 | "=plugin tpope/vim-sensible 42 | "=plugin csexton/trailertrash.vim 43 | 44 | syntax on 45 | filetype plugin indent on 46 | 47 | Just put those lines at the top of your `.vimrc` and infect will install plugins and packages for you. 48 | 49 | ## Building plugins 50 | 51 | Some plugins have binaries that need to be compiled, and infect can automatically run those commands for you. For example Shougo's [vimproc](https://github.com/Shougo/vimproc.vim) needs you to call `make` after installing it: 52 | 53 | "=plugin Shougo/vimproc.vim build: make 54 | 55 | ## Plugins vs Packages 56 | 57 | Packages are collections of plugins. Introduced in Vim 8, they provide a way to combine a number of plugins together and have Vim load them for you. One of the nice upsides to this is you don't need any external plugin manager to be able to load plugin bundles, just have to put them in the right folder. 58 | 59 | Infect will do this for you. If you declare a `plugin`, infect will put that in the default package called `plugins`. That will cause it to automatically be loaded when vim is started. 60 | 61 | ## Loading automatically or optionally 62 | 63 | According to Vim docs: 64 | 65 | > Note that the files under "pack/foo/opt" are not loaded automatically, only the 66 | ones under "pack/foo/start". See |pack-add| below for how the "opt" directory 67 | is used. 68 | 69 | This means you have to call `:packadd` to load any optional plugins. This can be handy if you don't want to proactively load up some plugins. 70 | 71 | Tell vim to only load Trailer Trash when requested: 72 | 73 | "=plugin csexton/trailertrash.vim load: opt 74 | 75 | Then to request it to be loaded: 76 | 77 | :packadd trailertrash.vim 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | task :default => :spec 6 | 7 | file "./standalone/infect" => FileList.new("lib/infect.rb", "lib/infect/*.rb") do |task| 8 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 9 | require 'infect/standalone' 10 | Infect::Standalone.save("./standalone/infect") 11 | end 12 | 13 | desc "Build standalone script" 14 | task :standalone => "./standalone/infect" 15 | 16 | namespace :build do 17 | task :standalone_parallel => [:clean] do 18 | Rake::Task[:standalone].invoke 19 | pmap = Gem::Specification.find_by_name('pmap') 20 | pmap_lib = Dir["#{File.join(pmap.lib_dirs_glob, 'pmap.rb')}"].first 21 | content = File.read(pmap_lib) 22 | 23 | preamble = <&1} 58 | unless $?.success? 59 | error "Command failed: #{cmd}\n" 60 | error output.gsub(/^/, " ") 61 | end 62 | 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/infect/command/prereqs.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | module Infect 3 | class Command 4 | class Prereqs < Command 5 | def mkdirs(list) 6 | list.each do |path| 7 | FileUtils.mkdir_p(File.expand_path(path)) 8 | end 9 | end 10 | def call 11 | mkdir PACK_DIR 12 | # create the cache directories for sensible.vim: 13 | # TODO: Support Windows 14 | if RUBY_PLATFORM =~ /darwin/ 15 | mkdirs %w(~/Library/Vim/swap ~/Library/Vim/backup ~/Library/Vim/undo) 16 | else 17 | mkdirs %w(~/.local/share/vim/swap ~/.local/share/vim/backup ~/.local/share/vim/undo") 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/infect/runner.rb: -------------------------------------------------------------------------------- 1 | module Infect 2 | # Globals be global 3 | VIMHOME = ENV['VIM'] || "#{ENV['HOME']}/.vim" 4 | VIMRC = ENV['MYVIMRC'] || "#{ENV['HOME']}/.vimrc" 5 | PACK_DIR = "#{VIMHOME}/pack/" 6 | 7 | class Runner 8 | def self.call(*args) 9 | self.new.call(*args) 10 | end 11 | 12 | def call(*args) 13 | force = args.include? "-f" 14 | 15 | Command::Prereqs.new().call 16 | 17 | commands = get_packages_from_vimrc 18 | commands.compact.each(&:call) 19 | 20 | locations = commands.map(&:location) 21 | Cleanup.new(locations, :force => force).call 22 | end 23 | 24 | private 25 | 26 | def get_packages_from_vimrc 27 | File.readlines(VIMRC).map do |line| 28 | if line =~ /^"=/ 29 | command, arg, opts = parse_command(line.gsub('"=', '')) 30 | Command.build(command, arg, opts) 31 | end 32 | end.compact 33 | end 34 | 35 | def parse_command(line) 36 | # TODO: pass in named params after for things like build commands and 37 | # branches 38 | # 39 | # "bundle BundleName build: "make -f file", branch: awesome 40 | # 41 | # So this will split the command into 3 parts 42 | # Now we can take args and split by ',' the split those by ':' and 43 | # map that to a hash that we can pass into the command builder 44 | 45 | # This splits and perserves "quoted words" 46 | #command, *args = line.split /\s(?=(?:[^"]|"[^"]*")*$)/ 47 | 48 | #command, *args = line.split 49 | 50 | command, arg, opts_string = line.split ' ', 3 51 | [command, arg, parse_opts(opts_string)] 52 | end 53 | 54 | def parse_opts(string) 55 | hash = {} 56 | # Woah now. 57 | # 58 | # The first split and regex will perserver quoted strings" and split on 59 | # whitespace or colons. 60 | # 61 | # The reject removes any duplicate empty strings that the split might 62 | # create when it encounters a colon and space next to each other 63 | # (something like this: ": " will do that) 64 | parts = string.split(/[\s:](?=(?:[^"]|"[^"]*")*$)/).reject! { |c| c.empty? } 65 | if parts 66 | parts.each_slice(2) do |key, val| 67 | hash[key.to_sym] = val 68 | end 69 | end 70 | hash 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/infect/standalone.rb: -------------------------------------------------------------------------------- 1 | module Infect 2 | # Inspired by https://github.com/defunkt/hub/blob/master/lib/hub/standalone.rb 3 | module Standalone 4 | extend self 5 | 6 | ROOT = File.expand_path('../../..', __FILE__) 7 | 8 | PREAMBLE = <<-preamble 9 | # 10 | # This file is generated code. DO NOT send patches for it. 11 | # 12 | # Original source files with comments are at: 13 | # https://github.com/csexton/infect 14 | # 15 | 16 | preamble 17 | 18 | def save(filename, path = '.') 19 | target = File.join(File.expand_path(path), filename) 20 | File.open(target, 'w') do |f| 21 | build f 22 | f.chmod 0755 23 | end 24 | end 25 | 26 | def build io 27 | io.puts "#!#{ruby_executable}" 28 | io << PREAMBLE 29 | 30 | each_source_file do |filename| 31 | File.open(filename, 'r') do |source| 32 | source.each_line {|line| io << line if line !~ /^\s*#/ } 33 | end 34 | io.puts '' 35 | end 36 | 37 | io.puts "Infect::Runner.call(*ARGV)" 38 | end 39 | 40 | def each_source_file 41 | File.open(File.join(ROOT, 'lib/infect.rb'), 'r') do |main| 42 | main.each_line do |req| 43 | if req =~ /^require\s+["'](.+)["']/ 44 | yield File.join(ROOT, 'lib', "#{$1}.rb") 45 | end 46 | end 47 | end 48 | end 49 | 50 | def ruby_executable 51 | if File.executable? '/usr/bin/ruby' then '/usr/bin/ruby' 52 | else 53 | require 'rbconfig' 54 | File.join RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'] 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/infect/version.rb: -------------------------------------------------------------------------------- 1 | module Infect 2 | VERSION = "1.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/command_spec.rb: -------------------------------------------------------------------------------- 1 | require "./lib/infect" 2 | 3 | describe Infect::Command do 4 | it "build a nil class when given a bad command" do 5 | Infect::Command.build("blargl", "bangle", {}).should be_nil 6 | end 7 | 8 | it "build a bundle command" do 9 | Infect::Command.build("bundle", "tpope/vim-fugitive", {}).name.should == "vim-fugitive" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/runner_spec.rb: -------------------------------------------------------------------------------- 1 | require "./lib/infect" 2 | require 'tempfile' 3 | require 'pry' 4 | require 'tmpdir' 5 | 6 | describe Infect::Runner do 7 | before do 8 | stub_const "Infect::VIMRC", file 9 | stub_const "Infect::PACK_DIR", dir 10 | end 11 | 12 | let(:file) { Tempfile.new("rspec") } 13 | let(:dir) { Dir.mktmpdir } 14 | 15 | it "reads bundle command" do 16 | file.puts '"=bundle NoParams' 17 | file.close 18 | 19 | Infect::Command.stub(:builder) 20 | Infect::Command.should_receive(:build).with("bundle", "NoParams", {}) 21 | Infect::Runner.call("-f") 22 | end 23 | 24 | it "reads bundle command" do 25 | file.puts '"=bundle ExtraParams param1:val1 param2: val2' 26 | file.close 27 | 28 | Infect::Command.stub(:builder) 29 | Infect::Command.should_receive(:build).with("bundle", "ExtraParams", {param1:"val1", param2:"val2"}) 30 | Infect::Runner.call("-f") 31 | end 32 | 33 | 34 | end 35 | -------------------------------------------------------------------------------- /spec/standalone_spec.rb: -------------------------------------------------------------------------------- 1 | require "./lib/infect" 2 | 3 | describe 'Infect::Standalone' do 4 | end 5 | -------------------------------------------------------------------------------- /standalone/infect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # 3 | # This file is generated code. DO NOT send patches for it. 4 | # 5 | # Original source files with comments are at: 6 | # https://github.com/csexton/infect 7 | # 8 | 9 | module Infect 10 | VERSION = "1.1.0" 11 | end 12 | 13 | require 'open-uri' 14 | require 'fileutils' 15 | 16 | module Infect 17 | module Colorize 18 | def colorize(code, str) 19 | "\e[#{code}m#{str}\e[0m" 20 | end 21 | def notice(str) 22 | puts colorize(32, str) 23 | end 24 | def error(str) 25 | puts colorize(31, str) 26 | end 27 | end 28 | end 29 | 30 | 31 | require 'open-uri' 32 | require 'fileutils' 33 | 34 | module Infect 35 | class Command 36 | include Infect::Colorize 37 | 38 | def self.build(command, arg ,opts) 39 | case command.to_sym 40 | when :plugin 41 | Plugin.new(arg, opts) 42 | when :bundle, :package 43 | Package.new(arg, opts) 44 | else 45 | $stderr.puts "WARNING: #{command} is not a valid command, ignorning" 46 | end 47 | end 48 | 49 | protected 50 | 51 | def mkdir(path) 52 | expanded_path = File.expand_path(path) 53 | unless File.directory?(expanded_path) 54 | FileUtils.mkdir_p(expanded_path) 55 | end 56 | end 57 | 58 | def chdir(path) 59 | Dir.chdir(path) 60 | end 61 | 62 | def download(url, path) 63 | File.open(File.expand_path(path), "w") do |file| 64 | open(url) do |read_file| 65 | file.write(read_file.read) 66 | end 67 | end 68 | end 69 | end 70 | end 71 | 72 | require 'open3' 73 | 74 | module Infect 75 | class Command 76 | class Plugin < Command 77 | DEFAULT_DIR = "plugins" 78 | 79 | attr_reader :build, :location, :name, :options, :url 80 | 81 | def initialize(arg, opts) 82 | load = opts.fetch(:load) { "start" } 83 | package = opts.fetch(:package) { DEFAULT_DIR } 84 | 85 | @name = File.basename(arg) 86 | @url = "git@github.com:#{arg}.git" 87 | @location = File.expand_path("#{PACK_DIR}/#{package}/#{load}/#{name}") 88 | @build = opts[:build] 89 | end 90 | 91 | def install 92 | notice "Installing #{name} to #{@location}..." 93 | parent_dir = File.expand_path("..", location) 94 | mkdir parent_dir 95 | chdir parent_dir 96 | git "clone --depth 1 '#{url}'" 97 | end 98 | 99 | def update 100 | notice "Updating #{name}..." 101 | chdir location 102 | git "pull" 103 | end 104 | 105 | def call 106 | if File.exists? location 107 | update 108 | else 109 | install 110 | end 111 | 112 | if build 113 | notice " Found build command, running: '#{build}'" 114 | chdir location 115 | quiet_system "#{build.gsub(/^\"|\"?$/, '')}" 116 | end 117 | 118 | location 119 | end 120 | 121 | private 122 | 123 | def git(args) 124 | quiet_system "git #{args}" 125 | end 126 | 127 | def quiet_system(cmd) 128 | output = %x{#{cmd} 2>&1} 129 | unless $?.success? 130 | error "Command failed: #{cmd}\n" 131 | error output.gsub(/^/, " ") 132 | end 133 | 134 | end 135 | end 136 | end 137 | end 138 | 139 | module Infect 140 | class Command 141 | class Package < Plugin 142 | def initialize(arg, opts) 143 | super 144 | @location = File.expand_path("#{PACK_DIR}/#{name}") 145 | end 146 | end 147 | end 148 | end 149 | 150 | require 'fileutils' 151 | module Infect 152 | class Command 153 | class Prereqs < Command 154 | def mkdirs(list) 155 | list.each do |path| 156 | FileUtils.mkdir_p(File.expand_path(path)) 157 | end 158 | end 159 | def call 160 | mkdir PACK_DIR 161 | if RUBY_PLATFORM =~ /darwin/ 162 | mkdirs %w(~/Library/Vim/swap ~/Library/Vim/backup ~/Library/Vim/undo) 163 | else 164 | mkdirs %w(~/.local/share/vim/swap ~/.local/share/vim/backup ~/.local/share/vim/undo") 165 | end 166 | end 167 | end 168 | end 169 | end 170 | 171 | module Infect 172 | class Cleanup 173 | include Infect::Colorize 174 | attr_reader :names, :force 175 | 176 | def initialize(list, args) 177 | @names = list.map{|p| File.basename(p)} 178 | @force = args[:force] || false 179 | end 180 | 181 | def call 182 | install_paths.each do |path| 183 | unless names.include? File.basename(path) 184 | if confirm(path) 185 | notice "Deleting #{path}" 186 | require 'fileutils' 187 | FileUtils.rm_rf path 188 | else 189 | notice "Leaving #{path}" 190 | end 191 | end 192 | end 193 | end 194 | 195 | def confirm(name) 196 | unless force 197 | print "Remove #{name}? [Yn]: " 198 | response = STDIN.gets.chomp 199 | case response.downcase 200 | when '' 201 | true 202 | when 'y' 203 | true 204 | else 205 | false 206 | end 207 | end 208 | end 209 | 210 | private 211 | 212 | def install_paths 213 | 214 | default_dir = Command::Plugin::DEFAULT_DIR 215 | plugins = Dir["#{PACK_DIR}#{default_dir}/*/*"] 216 | packages = Dir["#{PACK_DIR}*"] 217 | packages.delete("#{PACK_DIR}#{default_dir}") 218 | 219 | plugins + packages 220 | end 221 | 222 | end 223 | end 224 | 225 | module Infect 226 | VIMHOME = ENV['VIM'] || "#{ENV['HOME']}/.vim" 227 | VIMRC = ENV['MYVIMRC'] || "#{ENV['HOME']}/.vimrc" 228 | PACK_DIR = "#{VIMHOME}/pack/" 229 | 230 | class Runner 231 | def self.call(*args) 232 | self.new.call(*args) 233 | end 234 | 235 | def call(*args) 236 | force = args.include? "-f" 237 | 238 | Command::Prereqs.new().call 239 | 240 | commands = get_packages_from_vimrc 241 | commands.compact.each(&:call) 242 | 243 | locations = commands.map(&:location) 244 | Cleanup.new(locations, :force => force).call 245 | end 246 | 247 | private 248 | 249 | def get_packages_from_vimrc 250 | File.readlines(VIMRC).map do |line| 251 | if line =~ /^"=/ 252 | command, arg, opts = parse_command(line.gsub('"=', '')) 253 | Command.build(command, arg, opts) 254 | end 255 | end.compact 256 | end 257 | 258 | def parse_command(line) 259 | 260 | 261 | 262 | command, arg, opts_string = line.split ' ', 3 263 | [command, arg, parse_opts(opts_string)] 264 | end 265 | 266 | def parse_opts(string) 267 | hash = {} 268 | parts = string.split(/[\s:](?=(?:[^"]|"[^"]*")*$)/).reject! { |c| c.empty? } 269 | if parts 270 | parts.each_slice(2) do |key, val| 271 | hash[key.to_sym] = val 272 | end 273 | end 274 | hash 275 | end 276 | end 277 | end 278 | 279 | Infect::Runner.call(*ARGV) 280 | --------------------------------------------------------------------------------