#!/usr/bin/ruby # # @file Launcher # # @copyright (c) 2010-2012, Christoph Kappel # @version $Id$ # # This program can be distributed under the terms of the GNU GPLv2. # See the file COPYING for details. # # Launcher that combines modes/tagging of subtle and a browser search bar. # # It opens uris with your default browser via xdg-open. Easiest way to set # it is to define $BROWSER in your shell rc files. # # Thanks, fauno, for your initial work! # # Examples: # # :urxvt - Call methods defined in the config # g subtle wm - Change to browser view and search for 'subtle wm' via Google # urxvt @editor - Open urxvt on view @editor with random tag # urxvt @editor #work - Open urxvt on view @editor with tag #work # urxvt #work - Open urxvt and tag with tag #work # urxvt -urgentOnBell - Open urxvt with the urgentOnBell option # +urxvt - Open urxvt and set full mode # ^urxvt - Open urxvt and set floating mode # *urxvt - Open urxvt and set sticky mode # =urxvt - Open urxvt and set zaphod mode # urx - Open urxvt (tab completion) # # Keys: # # Up/Down - Cycle through history (per runtime) # ESC - 1) Hide/exit launcher 2) stop reverse history search # Enter - Run command # ^R - Reverse history search # # http://subforge.org/projects/subtle-contrib/wiki/Launcher # require 'singleton' require 'uri' begin require 'subtle/subtlext' rescue LoadError puts ">>> ERROR: Couldn't find subtlext" exit end # Check for subtlext version major, minor, teeny = Subtlext::VERSION.split('.').map(&:to_i) if major == 0 and minor == 10 and 3203 > teeny puts ">>> ERROR: launcher needs at least subtle `0.10.3203' (found: %s)" % [ Subtlext::VERSION ] exit end begin require_relative 'levenshtein.rb' rescue LoadError => err puts ">>> ERROR: Couldn't find `levenshtein.rb'" exit end # Launcher class module Subtle # {{{ module Contrib # {{{ # Precompile regexps RE_COMMAND = Regexp.new(/^([+\^\*]*[A-Za-z0-9_\-\/''\s]+)(\s[@#][A-Za-z0-9_-]+)*$/) RE_MODES = Regexp.new(/^([+\^\*]*)([A-Za-z0-9_\-\/''\s]+)/) RE_SEARCH = Regexp.new(/^[gs]\s+(.*)/) RE_METHOD = Regexp.new(/^[:]\s*(.*)/) RE_URI = Regexp.new(/^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$/ix) RE_BROWSER = Regexp.new(/(chrom[e|ium]|iron|navigator|firefox|opera)/i) # For history search CTRL_R = "\x12".to_sym # Launcher class class Launcher # {{{ include Singleton # Default values @@font_big = '-*-*-*-*-*-*-40-*-*-*-*-*-*-*' @@font_small = '-*-*-*-*-*-*-14-*-*-*-*-*-*-*' @@paths = '/usr/bin' @@screen_num = 0 # Singleton methods ## fonts {{{ # Set font strings # @param [Array] fonts Fonts array ## def self.fonts=(fonts) if fonts.is_a?(Array) @@font_big = fonts.first if(1 <= fonts.size) @@font_small = fonts.last if(2 <= fonts.size) end end # }}} ## paths {{{ # Set launcher path separated by colons # @param [String, Array] paths Path list separated by colon or array ## def self.paths=(paths) if paths.is_a?(String) @@paths = paths elsif paths.is_a?(Array) @@paths = paths.join(':') end end # }}} ## browser_screen_num {{{ # Set screen num to show browser view # @param [Fixnum] num Screen number ## def self.browser_screen_num=(num) @screen_num = num if num.is_a?(Fixnum) end # }}} ## run {{{ # Run the launcher ## def self.run self.instance.run end # }}} # Instance methods ## initialize {{{ # Create launcher instance ## def initialize @candidate = nil @browser = nil @view = nil @x = 0 @y = 0 @width = 0 @height = 0 @completed = nil @reverse = nil # Buffer @buf_input = '' @buf_info = '' # Parsed data @parsed_tags = [] @parsed_views = [] @parsed_app = '' @parsed_modes = '' # Cached data @cached_tags = Subtlext::Tag.all.map(&:name) @cached_views = Subtlext::View.all.map(&:name) @cached_apps = {} @cached_history = [] # FIXME: Find config instance if defined?(Subtle::Config) ObjectSpace.each_object(Subtle::Config) do |c| @cached_sender = c @cached_methods = c.methods(false).map(&:to_s) end end # Something close to a skiplist @@paths.split(':').each do |path| if Dir.exist?(path) Dir.foreach(File.expand_path(path)) do |entry| file = File.basename(entry) sym = file[0].to_sym # Sort in if @cached_apps.has_key?(sym) @cached_apps[sym] << file else @cached_apps[sym] = [ file ] end end else puts ">>> ERROR: Skipping non-existing path `%s'" % [ path ] end end # Init for performance @array1 = Array.new(20, 0) @array2 = Array.new(20, 0) # Get colors colors = Subtlext::Subtle.colors # Create input window @input = Subtlext::Window.new(:x => 0, :y => 0, :width => 1, :height => 1) do |w| w.name = 'Launcher: Input' w.font = @@font_big w.foreground = colors[:focus_fg] w.background = colors[:focus_bg] w.border_size = 0 end # Get font height and y offset of input window @font_height1 = @input.font_height + 6 @font_y1 = @input.font_y # Key down and redraw wrappers @input.on :key_down do |key, mods| begin Launcher.instance.key_down(key, mods) rescue => err puts err, err.backtrace end end @input.on :redraw do begin Launcher.instance.redraw rescue => err puts err, err.backtrace end end # Create info window @info = Subtlext::Window.new(:x => 0, :y => 0, :width => 1, :height => 1) do |w| w.name = 'Launcher: Info' w.font = @@font_small w.foreground = colors[:stipple] w.background = colors.has_key?(:panel_top) ? colors[:panel_top] : colors[:panel] w.border_size = 0 end # Get font height and y offset of info window @font_height2 = @info.font_height + 6 @font_y2 = @info.font_y end # }}} ## key_down {{{ # Key down handler # @param [String] key Input key # @param [Array] mods Modifier list ## def key_down(key, mods) ret = true # Handle keys case key when :up, :down # {{{ idx = (@cached_history.index(@buf_input) + (:up == key ? -1 : 1)) rescue -1 @buf_input = @cached_history[idx] || "" # }}} when :tab # {{{ complete # }}} when :escape # {{{ # Stop reverse-search with ESC if @reverse @reverse = nil else @buf_input = "" @buf_info = "" @reverse = nil @candidate = nil ret = false end # }}} when :backspace # {{{ if @reverse @reverse.chop! reverse_complete else @buf_input.chop! end # }}} when :space # {{{ @buf_input << " " # }}} when :return # {{{ @cached_history << @buf_input @buf_input = "" @buf_info = "" @reverse = nil ret = false # }}} when CTRL_R # {{{ if mods.is_a?(Array) and mods.include?(:control) @reverse = "" reverse_complete else @buf_input << key.to_s end else if @reverse @reverse << key.to_s reverse_complete else @buf_input << key.to_s end # }}} end # Reset completed buffer @completed = nil unless :tab == key parse ret end # }}} ## redraw {{{ # Redraw window contents ## def redraw # Fill input window @input.clear @input.draw_text(3, @font_y1 + 3, @buf_input) unless @buf_input.empty? # Assemble info string str = @buf_info.empty? ? 'Ready..' : @buf_info str << ", reverse-search: " + @reverse if @reverse # Fill info window @info.clear @info.draw_text(3, @font_y2 + 3, str) end # }}} ## move {{{ # Move launcher windows to current screen ## def move # Geometry geo = Subtlext::Screen.current.geometry @width = geo.width * 80 / 100 @x = geo.x + ((geo.width - @width) / 2) @y = geo.y + geo.height - @font_height1 - @font_height2 - 40 @input.geometry = [ @x, @y, @width, @font_height1 ] @info.geometry = [ @x, @y + @font_height1, @width, @font_height2 ] end # }}} ## show {{{ # Show launcher ## def show move # Show info first because input blocks @info.show @input.show end # }}} ## hide # {{{ # Hide launcher ## def hide @input.hide @info.hide end # }}} ## run {{{ # Show and run launcher ## def run show hide # Check if we have a candidate case @candidate when Symbol #{{{ @cached_sender.send(@candidate) # }}} when String # {{{ # Find or create tags @parsed_tags.map! do |t| tag = Subtlext::Tag.first(t) || Subtlext::Tag.new(t) tag.save tag end # Find or create view and add tag @parsed_views.each do |v| view = Subtlext::View.first(v) || Subtlext::View.new(v) view.save view.tag(@parsed_tags) unless view.nil? or @parsed_tags.empty? end # Spawn app, tag it and set modes unless (client = Subtlext::Subtle.spawn(@parsed_app)).nil? client.tags = @parsed_tags unless @parsed_tags.empty? # Set modes unless @parsed_modes.empty? flags = [] # Translate modes @parsed_modes.each_char do |c| case c when '+' then flags << :full when '^' then flags << :float when '*' then flags << :stick when '=' then flags << :zaphod end end client.flags = flags end end # }}} when URI # {{{ find_browser unless @browser.nil? Subtlext::Screen[@@screen_num].view = @view system("xdg-open '%s' &>/dev/null" % [ @candidate.to_s ]) @browser.focus end # }}} end @candidate = nil end # }}} private def reverse_complete # {{{ # Handle reverse search if @reverse and not @reverse.empty? and @cached_history.any? matches = @cached_history.reverse.select { |h| h =~ /#{@reverse}/ } @buf_input = matches.first || "" end end # }}} def complete # {{{ guesses = [] lookup = nil # Clear info field if @buf_input.empty? or @buf_input.nil? redraw return end # Store curret buffer @completed = @buf_input if @completed.nil? # Select lookup cache last = @completed.split(' ').last rescue @completed case last[0] when '#' lookup = @cached_tags prefix = '#' when '@' lookup = @cached_views prefix = '@' when ':' lookup = @cached_methods prefix = ':' when '+', '^', '*' lookup = @cached_apps[last[@parsed_modes.size].to_sym] prefix = @parsed_modes else lookup = @cached_apps[last[0].to_sym] prefix = '' end # Collect guesses unless lookup.nil? lookup.each do |l| guesses << [ '%s%s' %[ prefix, l ], Levenshtein::distance(last.gsub(/^[@#:]/, ''), l, 1, 8, 5, @array1, @array2) ] end # Sort by distance and remove it afterwards guesses.sort! { |a, b| a[1] <=> b[1] } guesses.map! { |a| a.first } last = @buf_input.split(' ').last rescue @buf_input idx = (guesses.index(last) + 1) % guesses.size rescue 0 @candidate = guesses[idx] @buf_input.gsub!(/#{last}$/, guesses[idx]) # Convert to symbol if methods are guessed @candidate = @candidate.delete(':').to_sym if ':' == prefix end rescue => err puts err, err.backtrace end # }}} def parse # {{{ # Handle input unless @buf_input.empty? or @buf_input.nil? if RE_URI.match(@buf_input) @candidate = URI.parse(@buf_input) @buf_info = 'Goto %s' % [ @candidate.to_s ] elsif RE_SEARCH.match(@buf_input) @candidate = URI.parse( 'http://www.google.com/#q=%s' % [ URI.escape($1) ] ) @buf_info = 'Goto %s' % [ @candidate.to_s ] elsif RE_METHOD.match(@buf_input) @candidate = $1.to_sym @buf_info = 'Call :%s' % [ @candidate ] elsif RE_COMMAND.match(@buf_input) @candidate = @buf_input @parsed_tags = [] @parsed_views = [] @parsed_app = '' @parsed_modes = '' # Parse args @candidate.split.each do |arg| case arg[0] when '#' then @parsed_tags << arg[1..-1] when '@' then @parsed_views << arg[1..-1] when '+', '^', '*' app, @parsed_modes, @parsed_app = RE_MODES.match(arg).to_a else if @parsed_app.empty? @parsed_app += arg else @parsed_app += ' ' + arg end end end # Add an ad-hoc tag if we don't have any and need one if @parsed_views.any? and not @parsed_app.empty? and @parsed_tags.empty? @parsed_tags << 'tag_%d' % [ rand(1337) ] end if @parsed_views.any? @buf_info = 'Launch %s%s on %s (via %s)' % [ modes2text(@parsed_modes), @parsed_app, @parsed_views.join(', '), @parsed_tags.join(', ') ] elsif @parsed_tags.any? @buf_info = 'Launch %s%s (via %s)' % [ modes2text(@parsed_modes), @parsed_app, @parsed_tags.join(', ') ] else @buf_info = 'Launch %s%s' % [ modes2text(@parsed_modes), @parsed_app ] end end else @buf_info = "" end redraw end # }}} def modes2text(modes) # {{{ ret = [] # Collect mode verbs modes.each_char do |c| case c when '+' then ret << 'full' when '^' then ret << 'floating' when '*' then ret << 'sticky' when '=' then ret << 'zaphod' end end ret.any? ? '%s ' % [ ret.join(', ') ] : '' end # }}} def find_browser # {{{ begin if @browser.nil? Subtlext::Client.all.each do |c| if c.klass.match(RE_BROWSER) @browser = c @view = c.views.first return end end puts '>>> ERROR: No supported browser found' puts ' (Supported: Chrome, Firefox and Opera)' end rescue @browser = nil @view = nil end end # }}} end # }}} end # }}} end # }}} # Implicitly run if __FILE__ == $0 # Set fonts #Subtle::Contrib::Launcher.fonts = [ # 'xft:DejaVu Sans Mono:pixelsize=80:antialias=true', # 'xft:DejaVu Sans Mono:pixelsize=12:antialias=true' #] # Set paths # Subtle::Contrib::Launcher.paths = [ '/usr/bin', '~/bin' ] # Set browser screen Subtle::Contrib::Launcher.browser_screen_num = 0 Subtle::Contrib::Launcher.run end # vim:ts=2:bs=2:sw=2:et:fdm=marker