In a previous post, we tried to source our shell aliases and functions in remote environments thru Terminator. One month later, I found still too much keystorkes for running my shortcuts. It gets worse when executing commands on multiple VMs without devops tools. So this time let’s play around Tmux.
The goal is to execute commands from a pre-defined library, on any environment, directly without any loading or sourcing.
The Tmux Key Binding
Paste line below in your tmux.conf.
1
bind-key -n "M-c" run 'bash -ic "ask-and-run"'
This line simply say when you press Alt-C, execute the shell command ask-and-run.
Implemention
Now let’s implement the ask-and-run function in .bash-aliases.
12345678910111213
# used in tmux, alt c, for quickly triggering pre-defined commandsfunction ask-and-run(){items=("ifdown eth0; ifup eth0""vim /etc/network/interfaces")cmd=$(zenity --list --column=Commands --height=500 --width=400 "${items[@]}")if[["$cmd" !=""]]; thenchars=$(echo`echo"$cmd" | wc -c`"/2 -1" | bc)final=$(echo$cmd | head -c$chars) tmux send-keys "$final" Enter
fi}
The 2 lines after zenity --list is for converting output from ls|ls to ls. This is needed in zenity 3.8.0. Simply skip that if your zenity is 3.4.0.
That’s all for the setup! Put your favourite commands inside the array items, reload tmux and enjoy Alt-C.
As a newbie of docker, I created some bash functions with auto-completion to help exploring docker. With tips in the previous post, you may use them in both local and remote environment.
The Code
Paste file below in e.g. ~/.bash_aliases_shared.
Updated on 4 Jun, so shortcuts below may be different comparing to the gif above.
Developers have their own tools to work. May not be the coolest but best suit their workflow. Let’s write a Terminator (v. 0.97) plugin and source those shortcuts into any environment.
The Terminator Plugin
Paste file below in ~/.config/terminator/plugins/source_helpers.py.
#!/usr/bin/python"""source_helpers.py - Terminator Plugin to source shared cli shortcuts"""importosimportgtkimportterminatorlib.pluginaspluginfromterminatorlib.translationimport_fromterminatorlib.utilimportwidget_pixbuf# Every plugin you want Terminator to load *must* be listed in 'AVAILABLE'AVAILABLE=['SourceHelper']classSourceHelper(plugin.MenuItem):"""Add custom commands to the terminal menu"""capabilities=['terminal_menu']dialog_action=gtk.FILE_CHOOSER_ACTION_SAVEdialog_buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)def__init__(self):plugin.MenuItem.__init__(self)defcallback(self,menuitems,menu,terminal):"""Add our menu items to the menu"""item=gtk.MenuItem(_('Terminal SourceHelper'))item.connect("activate",self.terminal_source_helpers,terminal)menuitems.append(item)defterminal_source_helpers(self,_widget,terminal):"""Handle the source_helpers of terminal"""f=open(os.getenv("HOME")+'/loki/env/bash_aliases_shared','r')output=''forlineinf:line=line.strip()iflen(line)>0andline[0]!="#":output=output+line+" ; "output=base64.b64encode(output.encode('zlib'))output="echo "+output+" | base64 --decode | python -c 'import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))' > /tmp/source; source /tmp/source; rm /tmp/source; clear"terminal.vte.feed_child(" "+output+"\n")
Terminator is not a must, a key binding for tmux will do if you get the idea.
Aliases and Functions
Extract aliases and functions that should be shared into a separated file, e.g. ~/.bash_aliases_shared.
123456789
# aliases / functions defined here will be usable in both local and remote machine# do not store cmd started with whitespace into historyHISTCONTROL=ignoreboth
# better output jsonalias json='python -mjson.tool'# you can add your own stuffs here, even auto-completion setup
Add line below in your ~/.bash_aliases, so they can be used in local as well.
1
source ~/.bash_aliases_shared
Bonus
Terminator usually come in handy when reading local settings on remote environment. One use case is to reset the terminal size. You should encountered that ssh session won’t resize when the terminal window is resized. New area cannot be filled. Simply craft another Terminator plugin to solve this problem. It is much like the previous plugin but we resize the session with stty instead.
123456
# ... plugin instantiation and menu registration ...defterminal_resize(self,_widget,terminal):cols=terminal.vte.get_column_count()rows=terminal.vte.get_row_count()terminal.vte.feed_child("stty cols "+str(cols)+"; stty rows "+str(rows)+"\n")
Hacking VIM is one of my favorite activity because I use it almost everyday. Interacting with shell inside VIM give us vim’s editing power, snippets, undo and so many great features. I wrote shellbridge 3 months ago and I mostly use it for redis operations and mass file management with SCM.
shellbridge is a daemon written in javascript, enabling an interactive shell experience inside editors like vim. Inspired by xiki but in a different way.
I have used zsh for 2+ years and I am really happy with it. Sometime I have to insert path of files, usually I have to type in characters, tab or /, characters, tab or /, etc… I want to have something like Ctrl-P for vim for me to find the path quickly and insert to my current editing command. I also want to do it again and again, like committing multiple files into git with one single command.
A plugin of zaw
Building from scratch is painful so I decided to build as a plugin on top of zaw.
123456789101112131415161718192021
function zaw-src-fuzzy(){# Get the file list by find .OLDIFS=$IFSIFS=$'\n'candidates=($(find .))candidates=(${(iou)candidates[@]})IFS=$OLDIFS# Define what kind of action can be performed on the selected item# first: accept-line# second: accept-searchactions=("zaw-callback-execute""zaw-callback-append-to-buffer")act_descriptions=("execute""append to edit buffer")}# Register our pluginzaw-register-src -n fuzzy zaw-src-fuzzy
# Setup Ctrl-F shortcut to triggerfunction fuzzy-start {kill-line; zaw-fuzzy }zle -N fuzzy-start
bindkey '^F' fuzzy-start
Source in your .zshrc file, press ctrl-f in zsh and enjoy.
Pry is a powerful tool for debugging ruby codes. It’s idea is brilliantly awesome. It’s source code is easy to read and understand. Pry is already an irreplaceable part of my dev. env.. Below I am going to share a bit how I tuned it, hope that is also interesting to you. This one is quite tricky so before moving on, please try Pry a while if you haven’t used it before.
Focus when Entering Session
Pry allows us to add hooks, block below will be executed every time entering Pry session. Add it into your ~/.pryrc and it will ask Tmux to select the window running Pry, and bring the terminal on top with wmctrl.
12345678
Pry.config.hooks.add_hook(:before_session,:focus)do_pane=ENV['TMUX_PANE']if_pane_wid=`tmux list-window -t main -F '\#{window_index} \#{window_id}' | grep '@#{_pane[1..-1]}$'`.split(' ')[0]`tmux select-window -t main:#{_wid}`end`wmctrl -a "Main Tmux Session"`end
As you can see, this is very specific for my env. (assuming a tmux has session named ‘main’ and is running under terminal with specific title). Please feel free to modify it to fit you.
Source Code Editing
In a Pry session, you can dynamically modify source codes with the edit command. Pry allows us to configure which editor to use. Do you remember how I open file in project-based vim using a custom launcher? We can apply it in Pry too.
Powerful tool to travel inside call stacks. When error occurred, show-stack returns the detailed call stacks. Locate the possible method caused problem then up <#id> to go into the context and debug.
When modifying a view, we check the HTML output very often. Sometime refreshing browser is expensive because of the complex controller logic like fetching external resources. We can stub data at the very beginning but we still have to test with real data in some point of time. I wrote a simple rails helper, to memorize all instance variables defined before rendering a view. Then it can be reused for rendering next time.
The Helper
1234567891011121314151617
defiv_cacheraise"Block is expected"unlessblock_given?returnyieldifRails.env!='development'id="#{self.controller_path}##{self.action_name}"$iv_cache.try:delete,idifparams.has_key?(:no_iv_cache)if$iv_cache&&$iv_cache.has_key?(id)$iv_cache[id].each{|k,v|self.instance_variable_set(k,v)}elseyield$iv_cache||={}$iv_cache[id]=self.instance_variables.each_with_object({}){|key,h|h[key.to_s]=self.instance_variable_get(key)}endend
Launching VIM from browser empowered us to build efficient workflow. Last time I introduced how to open a partial with Chrome Native Messaging. Another way is using launcher. rails-footnotes launching editor using launcher by default. We will take gvim as an example.
Let’s design the protocol first. In this setup, I want gvim to be launched when accessing vim:///home/loki/some/file.txt:9.
The Launcher
Setup below script and named vimp.
123456789101112131415161718192021222324
#!/usr/bin/env bash# xdg-open "vim:///home/loki/loki/loper/trunk/project.vim:9"# grab filepath from first parameter# eg. vim:///home/loki/loki/loper/trunk/project.vim:9fullpath=$1# split filepath to filename and line numberarr=(`echo$fullpath | sed -e 's/^vim:\/\/\(.*\):\(.*\)$/\1 \2/'`)# determine stage from filenamestage=$(echo${arr[0]} | sed -e "s#$HOME/##" | cut -d'/' -f1,2 )# if stage found in server list (opened with servername)if vim --serverlist | grep -qi $stage ; then# open file and that line on the remote vim launch $stage vim --servername $stage --remote-send ":tab drop ${arr[0]}<cr>${arr[1]}G"else# open new gvim session gvim ${arr[0]} +${arr[1]}fi
The Launcher Install Script
Please place file below next to the launcher script
#!/usr/bin/env bashset -e
echo"Get vimp executable path"# Absolute path to this script, e.g. /home/user/bin/foo.shSCRIPT=$(readlink -f "$0")# Absolute path this script is in, thus /home/user/binbase=$(dirname "$SCRIPT")vimp_path="$base/vimp"echo"Generate vimp.desktop and write to /usr/share/applications"echo"[Desktop Entry]Encoding=UTF-8Name=GVIMComment=Edit text files in GVIM sessionExec=$vimp_path %UTerminal=falseType=ApplicationIcon=/usr/src/vim/runtime/vim48x48.xpmCategories=Application;Utility;TextEditor;MimeType=text/plain;x-scheme-handler/vim;StartupNotify=trueStartupWMClass=VIMP" > ~/vimp.desktop
sudo mv ~/vimp.desktop /usr/share/applications/vimp.desktop
echo"Update Desktop Database"sudo update-desktop-database
echo"Done!"
After setup, you may execute ./path/to/install/script.sh to register our vim launcher.
Verify
1
xdg-open "vim:///home/loki/.vimrc:9"
Make it Work with rails-footnotes
123
#...skipped from initializers/rails_footnotes.rbf.prefix='vim://%s:%d'#...skipped
When debugging or revamping web pages, we inspect views and partials inside. When complicated page comes in, locating codes can be time consuming, especially when we are not familiar with the page.
The Idea
Chrome extension for finding out partials rendered.
Click the partial name to hit the exact file.
To implement, we need
an automated way to add extra markups
a chrome extension to visualize partials and listen to user action
a background script to launch the text editor
The Markups
The partial info must come from somewhere. Below I take sinatra and erb as an example.
12345678910111213141516
# inside the configure do block (not production)# override the prepare method of tiltTilt::ERBTemplate.class_evaldodefprepare@outvar=options[:outvar]||self.class.default_output_variableoptions[:trim]='<>'if!(options[:trim]==false)&&(options[:trim].nil?||options[:trim]==true)unlessdata.lines.first.start_with?"<!doctype"dataanddata.prepend<<-TAG <i class="erb-locator" style="display:none; visibility:hidden">#{file}</i> TAGend@engine=::ERB.new(data,options[:safe],options[:trim],@outvar)endendunless$production
Now <i> tags should be generated right above any partials rendered.
As you can see, a fixed extension ID and Native Messaging support are required.
12345678910111213141516171819202122
// background.js// Execute the erb locator script when user clicked on a locatorchrome.runtime.onMessage.addListener(function(request,sender,sendResponse){if(request.erb){console.log("Received",request);chrome.runtime.sendNativeMessage('com.loki.erb_locator',{erb:request.erb},function(){console.log("Native Message Sent");});sendResponse({});}});// Extension entry pointchrome.browserAction.onClicked.addListener(function(tab){chrome.tabs.executeScript(null,{file:"jquery-1.10.2.min.js"},function(){chrome.tabs.executeScript(null,{file:"inject.js"});});});
We have done 2 things here.
added a listener to trigger the erb locator ruby script
injected inject.js to the active tab when user activating our extension
Finally, inject.js for visualizing locators and listening to user action.
// inject.js// the backdrop, we will place our locators on itvar$mask=null;// Open partial in text editor thru Native MessagingfunctionopenERB(erb){chrome.runtime.sendMessage({erb:erb},function(){console.log("Message Sent: "+erb);});}// locate and visualize those locatorfunctionbuildLinks(){// keep track on locators with same position for avoid overlappingvarcount={};$('i.erb-locator').each(function(){var$this=$(this);varpath=$this.text();varerbName=path.replace(/.*\/(.*).erb/,"$1");var$a=$('<a class="_erb">'+erbName+'</a>').attr({"href":"javascript:void(0)","data-erb":path}).css({"position":"absolute","border":"1px solid #888","padding":"2px 6px","font-size":"14px","color":"#111","background":"white"});// slightly add offsets to overlapped nodesvaro=$this.next().offset();vark=o.top*1000+o.left;count[k]=count[k]==undefined?0:count[k]+1;$a.offset({top:o.top+count[k]*19,left:o.left});$mask.append($a);});$('body').append($mask);}// select the backdrop out$mask=$('#_mask');// remove it when it is already exist// user triggered the extension again without selecting any oneif(!!$mask.length){$mask.remove();}else{$mask=$('<div id="_mask">');$mask.css({"position":"absolute","top":"0","left":"0","right":"0","bottom":"0","background-color":"rgba(255, 255, 255, 0.5)","z-index":"99999"});buildLinks();$mask.on('click','a._erb',function(){openERB($(this).data('erb'));$mask.remove();returnfalse;});}
The Backend
Mentioned quite a lot the locator ruby script, below is an example and you may roll out your own.
12345678910111213141516171819202122232425262728
#!/usr/bin/env rubyrequire'syslog'require'json'Syslog.open# start hereSyslog.info"Native Message Received, RUBY_VERSION: #{RUBY_VERSION}"# read number of bytes from stdindefreadc(n)n.times.map{STDIN.getc}.joinend# read msg from chrome native messagingdefreadmsglen=readc(4).unpack("i")[0]readclenend# read filenamemsg=readmsgSyslog.info"msg: #{msg}"filename=JSON.load(msg)["erb"]system"vim --servername main --remote-send ':tab drop #{filename}<cr><cr>'"system"wmctrl -a 'GVIM main'"
To let chrome talk to the script, we have to register a Native Messaging Host. For Linux host, place file below under ~/.config/google-chrome/NativeMessagingHosts.