diff options
Diffstat (limited to 'bin/subtle-contrib/ruby/launcher.rb')
-rw-r--r-- | bin/subtle-contrib/ruby/launcher.rb | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/bin/subtle-contrib/ruby/launcher.rb b/bin/subtle-contrib/ruby/launcher.rb new file mode 100644 index 0000000..453f36a --- /dev/null +++ b/bin/subtle-contrib/ruby/launcher.rb @@ -0,0 +1,632 @@ +#!/usr/bin/ruby +# +# @file Launcher +# +# @copyright (c) 2010-2012, Christoph Kappel <unexist@dorfelite.net> +# @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<Tab> - 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 |