Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,52 @@ config.log_file = '/tmp/my-app-appear.log'
Appear.appear(pid, config)
```

## TmuxIde.app: open files in Nvim + Tmux

I've started a project based on Appear's libraries to open files in Nvim +
Tmux. I prefer Tmux and terminal-based programs to GUI editors, so I've always
wanted to set console Vim as my default application for many filetypes.

I've built a proof-of-concept scipt that intelligently opens clicked files in
Nvim sessions inside Tmux windows. We use
[Platypus](http://sveinbjorn.org/platypus) to turn the script into a Mac native
app, and [`duti`](http://duti.org/) to assign our new app as the default
editor.

This project will eventually be split into its own repo, but lives in Appear
for now.


Set up Neovim:

1. set up Neovim
1. Set up [Neovim Remote](https://github.com/mhinz/neovim-remote).
Follow the `nvr` install instructions. Make sure the binary ends up in
/usr/local/bin, /usr/bin, or /bin.
1. make sure you've got `tmux` in /usr/local/bin, too. `brew install tmux` if not.
1. stick this in your .bashrc or .zshrc so that each Neovim process gets it's own socket:
```bash
mkdir -p "$HOME/.vim/sockets/"
NVIM_LISTEN_ADDRESS="$HOME/.vim/sockets/vim-zsh-$$.sock"
```
Without this snippet, we'll be unable to query or control existing nvim sessions.

Build the app:

1. `brew install platypus`, which is the app builder
1. `bundle exec rake app` builds into ./build/TmuxIde.app

Use it as the default for all of your source code files:

1. Notify OS X that your app exits by running it once, you can find it in
./build/TmuxIde.app. It'll display a dialog and then exit.
1. `brew install duti`, which is a tool we use to change default application for filetypes.
1. `bundle exec rake app_defaults` will assign all source code extensions to
open in TmuxIde by default.

If you have any issues with the app, you can `tail -f /tmp/tmux-ide-*.log` to
view log messages.

## contributing

First, get yourself set up:
Expand Down
165 changes: 165 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
require "bundler/gem_tasks"
require "yard"
require 'rspec/core/rake_task'
require 'appear'
require 'Open3'
require 'fileutils'
require 'rexml/document'


RSpec::Core::RakeTask.new(:spec)
ROOT_PATH = Pathname.new(File.dirname(File.expand_path(__FILE__)))

module BasicDocCoverage
MINIMUM_COVERAGE = 25.0
Expand Down Expand Up @@ -48,6 +54,165 @@ module BasicDocCoverage
end
end

class IdeAppBuilder
NAME = 'TmuxIde'

def initialize(opts = {})
@release = opts[:release] || false
end

def bundle_identifier
"tl.jake.#{NAME}"
end

def development?
!@release
end

def base
::Appear::Util::CommandBuilder.new('platypus')
end

def suffixes
f = File.new(assets_dir.join('MacVimInfo.plist'))
doc = REXML::Document.new(f)
array_key = REXML::XPath.first(doc, '/plist/dict/key[contains(., "CFBundleDocumentTypes")]')
types_array = array_key.next_element

all_exts = []

REXML::XPath.each(types_array, './dict/key[contains(., "CFBundleTypeExtensions")]') do |k|
ext_array = k.next_element
ext_array.elements.each do |ext|
all_exts << ext.text.to_s
end
end
all_exts
end

# see https://developer.apple.com/library/prerelease/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
def utis
%w(
public.source-code
public.item
public.folder
)
end

def command
cmd = base.flags(
:name => NAME,
'interface-type' => 'None',
'app-icon' => assets_dir.join('MacVim.icns'),
'document-icon' => assets_dir.join('MacVim-generic.icns'),
:interpreter => '/usr/bin/ruby',
'app-version' => ::Appear::VERSION,
:author => 'Jake Teton-Landis',
'bundle-identifier' => bundle_identifier,
:droppable => true,
'quit-after-execution' => true,
:suffixes => suffixes.join('|'),
'uniform-type-identifiers' => utis.join('|'),
)

cmd.flags(:symlink => true) if development?

# read script from STDIN,
# build to build/TmuxIde.app
cmd.args(
ROOT_PATH.join('mac_desktop_app/entrypoint.rb'),
output_app
)

cmd
end

def assets_dir
ROOT_PATH.join('mac_desktop_app/assets')
end

def build_dir
ROOT_PATH.join('build')
end

def output_app
build_dir.join("#{NAME}.app")
end

def resources
output_app.join('Contents/Resources')
end

def app_gem
resources.join('appear-gem')
end

def files
%w(bin lib tools appear.gemspec Gemfile Gemfile.lock)
end

def link_files!
files.each do |name|
src = ROOT_PATH.join(name)
dest = app_gem.join(name)
FileUtils.ln_s(src, dest)
end
end

def copy_files!
files.each do |name|
src = ROOT_PATH.join(name)
dest = app_gem.join(name)
FileUtils.cp_r(src, dest)
end
end

def run!
FileUtils.rm_rf(build_dir)
FileUtils.mkdir_p(build_dir)

# build the app with platypus
args = command.to_a
puts args
out, err, status = Open3.capture3(*args)
puts out unless out.empty?
raise err unless status.success?

# copy or link gem files into place
FileUtils.mkdir_p(app_gem)
if development?
link_files!
else
copy_files!
end
end
end

desc "build a mac app that can appear stuff"
task :app_dev do
builder = IdeAppBuilder.new
builder.run!
end

desc "build a mac app that can appear stuff"
task :app do
builder = IdeAppBuilder.new(:release => true)
builder.run!
end

desc "use mac app for all source code files"
task :default_app do
builder = IdeAppBuilder.new
sh "duti -s #{builder.bundle_identifier} public.source-code editor"
builder.suffixes.map {|s| '.' + s}.each do |ext|
begin
sh "duti -s #{builder.bundle_identifier} #{ext} editor"
rescue => err
puts err
end
end
end

BasicDocCoverage.define_task(:doc)

task default: [:spec, :doc]
2 changes: 1 addition & 1 deletion appear.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
# raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
# end

spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|mac_desktop_app)/}) }
spec.bindir = "bin"
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
Expand Down
34 changes: 21 additions & 13 deletions lib/appear/editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def find_or_create_ide(filename)
#
# @param filename [String]
def create_ide(filename)
dir = project_root(filename)
is_root, dir = project_root(filename)

# find or create session
tmux_session = services.tmux.sessions.sort_by { |s| s.windows.length }.last
Expand All @@ -132,16 +132,21 @@ def create_ide(filename)
# remember our pid list
existing_nvims = services.processes.pgrep(Nvim::NEOVIM)

# split window across the middle, into a big and little pane
# split window across the middle, into a big and little pane if we're
# editing a project. Otherwise, we don't need the little terminal
# buddies.
main = window.panes.first
main.send_keys([Nvim.edit_command(filename).to_s, "\n"], :l => true)
left = main.split(:p => 30, :v => true, :c => dir)
# cut the smaller bottom pane in half
right = left.split(:p => 50, :h => true, :c => dir)
# put a vim in the top pane, and select it
#[left, right].each_with_index do |pane, idx|
#pane.send_keys(["bottom pane ##{idx}"], :l => true)
#end

if is_root
left = main.split(:p => 30, :v => true, :c => dir)
# cut the smaller bottom pane in half
right = left.split(:p => 50, :h => true, :c => dir)
# put a vim in the top pane, and select it
#[left, right].each_with_index do |pane, idx|
#pane.send_keys(["bottom pane ##{idx}"], :l => true)
#end
end

# Hacky way to wait for nvim to launch! This should take at most 2
# seconds, otherwise your vim is launching too slowley ;)
Expand Down Expand Up @@ -216,12 +221,12 @@ def project_root(filename)
path.join(marker).exist?
end

return path if is_root
return [true, path] if is_root
end

# no markers were found
return p.to_s if p.directory?
return p.dirname.to_s
return [true, p.to_s] if p.directory?
return [false, p.dirname.to_s]
end

private
Expand All @@ -232,13 +237,16 @@ def update_nvims
@nvims ||= {}
@nvim_to_cwd ||= {}
@cwd_to_nvim ||= {}
@cwd_by_depth ||= []
new_nvims = false

Nvim.sockets.each do |sock|
next if @nvims[sock]

new_nvims = true
nvim = Nvim.new(sock, services)
next unless nvim.alive?

new_nvims = true
@nvims[sock] = nvim
cwd = nvim.cwd
@nvim_to_cwd[nvim] = cwd
Expand Down
9 changes: 9 additions & 0 deletions lib/appear/editor/nvim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ def initialize(socket, svc = {})
@expr_memo = ::Appear::Util::Memoizer.new
end

# @return [Boolean] true if there is a live Nvim connected to this socket
def alive?
begin
return pid
rescue
false
end
end

# evaluate a Vimscript expression
#
# @param vimstring [String] the expression, eg "fnamemodify('~', ':p')"
Expand Down
Binary file added mac_desktop_app/assets/MacVim-generic.icns
Binary file not shown.
Binary file added mac_desktop_app/assets/MacVim.icns
Binary file not shown.
Loading