summaryrefslogtreecommitdiffstats
path: root/bin/subtle-contrib/ruby
diff options
context:
space:
mode:
authorRasmus Steinke <rasi@xssn.at>2012-08-11 02:40:34 +0200
committerRasmus Steinke <rasi@xssn.at>2012-08-11 02:40:34 +0200
commitf140a1642ebfde198946ad6760c1003c1cb9a8c3 (patch)
tree7538aa90092f99c661ec3abb4a69944767315dc5 /bin/subtle-contrib/ruby
parente3fd45703267ce67d8b1e1fb53a4a1b4ccda551e (diff)
downloaddotfiles-f140a1642ebfde198946ad6760c1003c1cb9a8c3.tar.gz
dotfiles-f140a1642ebfde198946ad6760c1003c1cb9a8c3.tar.xz
scripts
Diffstat (limited to 'bin/subtle-contrib/ruby')
-rw-r--r--bin/subtle-contrib/ruby/graviton.rb481
-rw-r--r--bin/subtle-contrib/ruby/launcher.rb632
-rw-r--r--bin/subtle-contrib/ruby/levenshtein.rb74
-rw-r--r--bin/subtle-contrib/ruby/merger.rb292
-rw-r--r--bin/subtle-contrib/ruby/positioner.rb300
-rw-r--r--bin/subtle-contrib/ruby/selector.rb344
-rw-r--r--bin/subtle-contrib/ruby/styler.rb859
-rw-r--r--bin/subtle-contrib/ruby/termstyler.rb289
-rw-r--r--bin/subtle-contrib/ruby/vitag.rb127
9 files changed, 3398 insertions, 0 deletions
diff --git a/bin/subtle-contrib/ruby/graviton.rb b/bin/subtle-contrib/ruby/graviton.rb
new file mode 100644
index 0000000..09d8a16
--- /dev/null
+++ b/bin/subtle-contrib/ruby/graviton.rb
@@ -0,0 +1,481 @@
+#!/usr/bin/ruby
+#
+# @file Graviton
+#
+# @copyright (c) 2010-2011, 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.
+#
+# Graviton is a helper to create custom gravities
+#
+# http://subforge.org/projects/subtle-contrib/wiki/Graviton
+#
+
+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 0 == major and 9 == minor and 2829 > teeny
+ puts ">>> ERROR: launcher needs at least subtle `0.9.2829' (found: %s)" % [
+ Subtlext::VERSION
+ ]
+ exit
+end
+
+begin
+ require "gtk2"
+rescue LoadError
+ puts <<EOF
+>>> ERROR: Couldn't find the gem `gtk2'
+>>> Please install it with following command:
+>>> gem install gtk2
+EOF
+ exit
+end
+
+# Styler class
+module Subtle # {{{
+ module Contrib # {{{
+ # Colors
+ COLORS = {
+ :cyan => "#00FFFF",
+ :green => "#008000",
+ :olive => "#808000",
+ :teal => "#008080",
+ :blue => "#0000FF",
+ :silver => "#C0C0C0",
+ :lime => "#00FF00",
+ :navy => "#000080",
+ :purple => "#800080",
+ :magenta => "#FF00FF",
+ :maroon => "#800000",
+ :red => "#FF0000",
+ :yellow => "#FFFF00",
+ :gray => "#808080"
+ }
+
+ # Rectangle class
+ class Rectangle # {{{
+ attr_accessor :x, :y, :width, :height, :color
+
+ ## initialize {{{
+ # Create a new rectangle
+ # @param [Fixnum] x X position
+ # @param [Fixnum] y Y position
+ # @param [Fixnum] width Rectangle width
+ # @param [Fixnum] height Rectangle height
+ ##
+
+ def initialize(x, y, width, height, color)
+ @x = x
+ @y = y
+ @width = width
+ @height = height
+ @color = color
+ end # }}}
+
+ ## normalize # {{{
+ # Normalize width/height and set left top border to origin
+ ##
+
+ def normalize
+ if 0 > @width
+ @width *= -1
+ @x -= @width
+ end
+
+ if 0 > @height
+ @height *= -1
+ @y -= @height
+ end
+ end # }}}
+
+ ## is_edge? {{{
+ # Check if x/y is edge of rectangle
+ # @param [Fixnum] x X position
+ # @param [Fixnum] y Y position
+ # @return [Symbol, nil] Selected edge or nil
+ ##
+
+ def is_edge?(x, y)
+ if x == @x and y == @y
+ :top_left
+ elsif x == (@x + @width) and y == @y
+ :top_right
+ elsif x == @x and y == (@y + @height)
+ :bottom_left
+ elsif x == (@x + @width) and y == (@y + @height)
+ :bottom_right
+ else
+ nil
+ end
+ end # }}}
+
+ ## to_gravity {{{
+ # Calculate gravity from rectangle values for given width/height
+ # @param [Fixnum] width Width for calculation
+ # @param [Fixnum] height Height for calculation
+
+ def to_gravity(width, height)
+ x = ratio(@x, width)
+ y = ratio(@y, height)
+ w = ratio(@width, width)
+ h = ratio(@height, height)
+
+ "gravity :%s, [ %d, %d, %d, %d ]" % [ @color, x, y, w, h ]
+ end # }}}
+
+ ## to_s {{{
+ # Convert rectable to string
+ ##
+
+ def to_s
+ "x=%d, y=%d, width=%d, height=%d" % [ @x, @y, @width, @height ]
+ end # }}}
+
+ private
+
+ def ratio(value, ratio) # {{{
+ (value.to_f * 100.0 / ratio).to_i
+ end # }}}
+ end # }}}
+
+ # Graviton class
+ class Graviton < Gtk::Window # {{{
+
+ ## initialize {{{
+ # Init window
+ ##
+
+ def initialize
+ super
+
+ # Init sizes
+ @geom = Subtlext::Screen.current.geometry
+ @gridsize = 20
+ @panel_x = 7
+ @panel_y = 7
+ @panel_width = @geom.width / 3
+ @panel_height = @geom.height / 3
+
+ # Shrink grid
+ @panel_width -= @panel_width % @gridsize
+ @panel_height -= @panel_height % @gridsize
+
+ @grid_x = @panel_width / @gridsize
+ @grid_y = @panel_height / @gridsize
+
+ @window_width = @panel_width + 14
+ @window_height = @panel_height + 44
+
+ # Variables
+ @rectangles = []
+ @cur_rect = nil
+ @cur_edge = nil
+ @sx = 0
+ @sy = 0
+ @x = 0
+ @y = 0
+
+ # Options
+ set_title("Graviton for subtle #{Subtlext::VERSION}")
+ set_wmclass("graviton", "subtle")
+ set_resizable(false)
+ set_keep_above(true)
+ set_size_request(@window_width, @window_height)
+ set_window_position(Gtk::Window::POS_CENTER)
+ stick
+
+ # Signals
+ signal_connect("delete_event") do
+ false
+ end
+
+ signal_connect("destroy") do
+ Gtk.main_quit
+ end
+
+ # Alignment
+ align = Gtk::Alignment.new(0.5, 0.5, 0.5, 0.5)
+ align.set_padding(7, 7, 7, 7)
+
+ add(align)
+
+ # Vbox
+ vbox = Gtk::VBox.new
+ align.add(vbox)
+
+ # Frame
+ frame = Gtk::Frame.new
+ frame.set_border_width(0)
+ frame.set_shadow_type(Gtk::SHADOW_NONE)
+ vbox.pack_start(frame)
+
+ # Area
+ @area = Gtk::DrawingArea.new
+ @area.set_size_request(@panel_width, @panel_height)
+ @area.add_events(
+ Gdk::Event::POINTER_MOTION_MASK|
+ Gdk::Event::BUTTON1_MOTION_MASK|
+ Gdk::Event::BUTTON_PRESS_MASK|
+ Gdk::Event::BUTTON_RELEASE_MASK|
+ Gdk::Event::POINTER_MOTION_HINT_MASK
+ )
+ frame.add(@area)
+
+ # Create context and surface
+ @surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32,
+ @panel_width, @panel_width
+ )
+ @ctxt = Cairo::Context.new(@surface)
+
+ # Area events
+ @area.signal_connect("expose_event") do |*args|
+ expose_event(*args)
+ end
+
+ @area.signal_connect("motion_notify_event") do |*args|
+ motion_event(*args)
+ end
+
+ @area.signal_connect("button_press_event") do |*args|
+ press_event(*args)
+ end
+
+ @area.signal_connect("button_release_event") do |*args|
+ release_event(*args)
+ end
+
+ # Hbox
+ hbox = Gtk::HButtonBox.new
+ vbox.pack_start(hbox, false, false, 2)
+
+ # Print button
+ button = Gtk::Button.new("Print")
+ hbox.pack_start(button)
+
+ button.signal_connect("clicked") do |*args|
+ button_print(*args)
+ end
+
+ # Reset button
+ button = Gtk::Button.new("Reset")
+ hbox.pack_start(button)
+
+ button.signal_connect("clicked") do
+ @rectangles = []
+ @area.signal_emit("expose_event", nil)
+ end
+
+ # Exit button
+ button = Gtk::Button.new("Exit")
+ hbox.pack_start(button)
+
+ button.signal_connect("clicked") do
+ Gtk.main_quit
+ end
+
+ show_all
+ end # }}}
+
+ private
+
+ def expose_event(widget, event) # {{{
+ # Clear
+ @ctxt.set_source_rgba(0.0, 0.0, 0.0, 1.0)
+ @ctxt.set_operator(Cairo::OPERATOR_SOURCE)
+ @ctxt.paint
+
+ # Grid
+ @ctxt.set_line_width(1)
+ @ctxt.set_source_rgba(0.6, 0.6, 0.6, 0.4)
+
+ # Vertical lines
+ (0..@panel_width).step(@grid_x) do |x|
+ @ctxt.move_to(x, 0)
+ @ctxt.line_to(x, @panel_height)
+ end
+
+ # Horizontal lines
+ (0..@panel_height).step(@grid_y) do |y|
+ @ctxt.move_to(0, y)
+ @ctxt.line_to(@panel_width, y)
+ end
+ @ctxt.stroke
+
+ # Center
+ @ctxt.set_source_rgba(0.6, 0.0, 0.0, 0.4)
+ x = @grid_x * (@gridsize / 2)
+ y = @grid_y * (@gridsize / 2)
+
+ # Vertical line
+ @ctxt.move_to(x, 0)
+ @ctxt.line_to(x, @panel_height)
+
+ # Horizontal line
+ @ctxt.move_to(0, y)
+ @ctxt.line_to(@panel_width, y)
+ @ctxt.stroke
+
+ # Rectangles
+ @ctxt.set_line_width(1.0)
+ @rectangles.each do |r|
+ #@ctxt.set_dash((@cur_rect == r ? 2 : 0))
+ @ctxt.set_source_color(Gdk::Color.parse(COLORS[r.color]))
+ @ctxt.rectangle(r.x, r.y, r.width, r.height)
+
+ # Cross
+ @ctxt.move_to(r.x, r.y)
+ @ctxt.line_to(r.x + r.width, r.y + r.height)
+ @ctxt.move_to(r.x + r.width, r.y)
+ @ctxt.line_to(r.x, r.y + r.height)
+ @ctxt.stroke
+ end
+
+ # Position
+ @ctxt.move_to(@x, @y)
+ @ctxt.set_source_color(Gdk::Color.parse("#ff0000"))
+ @ctxt.rectangle(@x - 2, @y - 2, 4, 4)
+ @ctxt.stroke
+
+ # Swap context
+ ctxt = widget.window.create_cairo_context
+ ctxt.set_source(@surface)
+ ctxt.paint
+ end # }}}
+
+ def motion_event(widget, event) # {{{
+ # Snap to closest grid knot
+ modx = event.x % @grid_x
+ mody = event.y % @grid_y
+
+ @x = @grid_x / 2 < modx ? event.x - modx + @grid_x : event.x - modx
+ @y = @grid_y / 2 < mody ? event.y - mody + @grid_y : event.y - mody
+
+ # Calculate new width/height
+ unless @cur_rect.nil?
+ case @cur_edge
+ when :top_left
+ @cur_rect.x = @x
+ @cur_rect.y = @y
+ @cur_rect.width = @sx - @x
+ @cur_rect.height = @sy - @y
+ when :top_right
+ @cur_rect.x = @sx
+ @cur_rect.y = @y
+ @cur_rect.width = @x - @sx
+ @cur_rect.height = @sy - @y
+ when :bottom_left
+ @cur_rect.x = @x
+ @cur_rect.y = @sy
+ @cur_rect.width = @sx - @x
+ @cur_rect.height = @y - @sy
+ when :bottom_right
+ @cur_rect.x = @sx
+ @cur_rect.y = @sy
+ @cur_rect.width = @x - @sx
+ @cur_rect.height = @y - @sy
+ end
+ end
+
+ expose_event(widget, event)
+ end # }}}
+
+ def press_event(widget, event) # {{{
+ if 1 == event.button
+ # Find rectangles by edge
+ @rectangles.each do |r|
+ case r.is_edge?(@x, @y)
+ when :top_left
+ @cur_rect = r
+ @cur_edge = :top_left
+ @sx = r.x + r.width
+ @sy = r.y + r.height
+
+ return
+ when :top_right
+ @cur_rect = r
+ @cur_edge = :top_right
+ @sx = r.x
+ @sy = r.y + r.height
+
+ return
+ when :bottom_left
+ @cur_rect = r
+ @cur_edge = :bottom_left
+ @sx = r.x + r.width
+ @sy = r.y
+
+ return
+ when :bottom_right
+ @cur_rect = r
+ @cur_edge = :bottom_right
+ @sx = r.x
+ @sy = r.y
+
+ return
+ else
+ end
+ end
+
+ if @rectangles.size < COLORS.size
+ # Create new rectangle
+ @mode = :resize
+ @cur_edge = :bottom_right
+ @cur_rect = Rectangle.new(
+ @x, @y, 20, 20,
+ COLORS.keys[@rectangles.size]
+ )
+ @rectangles << @cur_rect
+
+ @sx = @x
+ @sy = @y
+ @x += @gridsize
+ @y += @gridsize
+
+ expose_event(widget, event)
+ else
+ puts ">>> ERROR: Out of colors"
+ end
+ end
+ end # }}}
+
+ def release_event(widget, event) # {{{
+ if 1 == event.button
+ @cur_rect.normalize unless @cur_rect.nil?
+ @cur_rect = nil
+ @cur_edge = nil
+ @sx = 0
+ @sy = 0
+ end
+ end # }}}
+
+ def button_print(widget) # {{{
+ puts
+
+ # Print rectangles
+ @rectangles.each do |r|
+ puts r.to_gravity(@panel_width, @panel_height)
+ end
+
+ puts
+ end # }}}
+ end # }}}
+ end # }}}
+end # }}}
+
+# Implicitly run<
+if __FILE__ == $0
+ Gtk.init
+ Subtle::Contrib::Graviton.new
+ Gtk.main
+end
+
+# vim:ts=2:bs=2:sw=2:et:fdm=marker
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
diff --git a/bin/subtle-contrib/ruby/levenshtein.rb b/bin/subtle-contrib/ruby/levenshtein.rb
new file mode 100644
index 0000000..6122149
--- /dev/null
+++ b/bin/subtle-contrib/ruby/levenshtein.rb
@@ -0,0 +1,74 @@
+#
+# @file Calculate Levenshtein distance
+#
+# @copyright (c) 2010-2011, 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.
+#
+
+module Levenshtein
+ MAX_LENGTH = 255
+
+ ## distance {{{
+ # @brief Calculate the Levenshtein Distance
+ # @param [String] s1 First string
+ # @param [String] s2 Second string
+ # @param [Fixnum] cost_ins Cost for insertion
+ # @param [Fixnum] cost_rep Cost for replace
+ # @param [Fixnum] cost_del Cost for deletion
+ # @param [Array] a1 First array
+ # @param [Array] a2 Second array
+ # @raise [String] Error
+ # @return [Fixnum] Calculated distance
+ ##
+
+ def self.distance(s1, s2, cost_ins = 1, cost_rep = 1,
+ cost_del = 1, a1 = nil, a2 = nil)
+ # Step 1: Check string length
+ l1 = s1.length
+ l2 = s2.length
+
+ # Check length
+ return l2 * cost_ins if 0 == l1
+ return l1 * cost_del if 0 == l2
+
+ raise "Max length" if l1 > MAX_LENGTH || l2 > MAX_LENGTH
+
+ # Step 2: Create and init arrays
+ p1 = a1 || Array.new(l2 + 1, 0)
+ p2 = a2 || Array.new(l2 + 1, 0)
+
+ (0..l2).each { |i| p1[i] = i * cost_ins }
+
+ # Step 3: Iterate over string s1
+ (0..(l1 - 1)).each do |i|
+ p2[0] = p1[0] + cost_del
+
+ # Step 4: Iterate over string s2
+ (0..(l2 - 1)).each do |j|
+ # Step 5: Get cost
+ c0 = p1[j] + ((s1[i] == s2[j]) ? 0 : cost_rep)
+ c1 = p1[j + 1] + cost_del
+ c0 = c1 if c1 < c0
+
+ c2 = p2[j] + cost_ins
+ c0 = c2 if c2 < c0
+
+ # Step 6: Store min value in matrix
+ p2[j + 1] = c0
+ end
+
+ # Swap arrays
+ tmp = p1
+ p1 = p2
+ p2 = tmp
+ end
+
+ # Step 7: Return distance
+ c0 = p1[l2]
+
+ c0
+ end # }}}
+end # }}}
diff --git a/bin/subtle-contrib/ruby/merger.rb b/bin/subtle-contrib/ruby/merger.rb
new file mode 100644
index 0000000..30839ac
--- /dev/null
+++ b/bin/subtle-contrib/ruby/merger.rb
@@ -0,0 +1,292 @@
+#!/usr/bin/ruby
+#
+# @file Merger
+#
+# @copyright (c) 2011, 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.
+#
+# Select and tag/untag visible views of current client window
+#
+# Colors:
+#
+# Focus - Currently selected view
+# View - Other views
+# Occupied - Views client is visible
+# Urgent - Selected views
+#
+# Keys:
+#
+# Left, Up - Move to left
+# Right, Down - Move to right
+# Escape - Hide/exit
+# Space - Select view
+# Return - Tag/untag selected views and exit hide/exit
+#
+# http://subforge.org/projects/subtle-contrib/wiki/Positioner
+#
+
+require "singleton"
+
+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 3006 > teeny)
+ puts ">>> ERROR: merger needs at least subtle `0.10.3006' (found: %s)" % [
+ Subtlext::VERSION
+ ]
+ exit
+end
+
+# Merger class
+module Subtle # {{{
+ module Contrib # {{{
+ class Merger # {{{
+ include Singleton
+
+ # Default values
+ @@font = "-*-*-medium-*-*-*-14-*-*-*-*-*-*-*"
+
+ # Singleton methods
+
+ ## fonts {{{
+ # Set font strings
+ # @param [String] fonts Fonts array
+ ##
+
+ def self.font=(font)
+ @@font = font
+ end # }}}
+
+ ## run {{{
+ # Run expose
+ ##
+
+ def self.run
+ self.instance.run
+ end # }}}
+
+ # Instance methods
+
+ ## initialize {{{
+ # Create expose instance
+ ##
+
+ def initialize
+ # Values
+ @colors = Subtlext::Subtle.colors
+ @merged = {}
+ @backup = {}
+
+ # Create main window
+ @win = Subtlext::Window.new(:x => 0, :y => 0, :width => 1, :height => 1) do |w|
+ w.name = "Merger"
+ w.font = @@font
+ w.foreground = @colors[:title_fg]
+ w.background = @colors[:title_bg]
+ w.border_size = 0
+ end
+
+ # Font metrics
+ @font_height = @win.font_height + 6
+ @font_y = @win.font_y
+
+ # Handler
+ @win.on :key_down, method(:key_down)
+ @win.on :draw, method(:redraw)
+ end # }}}
+
+ ## run {{{
+ # Show and run positioner
+ ##
+
+ def run
+ update
+ show
+ hide
+ end # }}}
+
+ private
+
+ ## key_down {{{
+ # Key down handler
+ # @param [String] key Pressed key
+ ##
+
+ def key_down(key)
+ ret = true
+
+ case key
+ when :left, :up # {{{
+ idx = @views.index(@selected)
+ idx -= 1 if(1 < idx)
+ @selected = @views[idx] # }}}
+ when :right, :down # {{{
+ idx = @views.index(@selected)
+ idx += 1 if(idx < (@views.size - 1))
+ @selected = @views[idx] # }}}
+ when :space # {{{
+ if(@merged[@current.name].include?(@selected))
+ @merged[@current.name].delete(@selected)
+ else
+ @merged[@current.name] << @selected
+ end # }}}
+
+ p @merged
+ when :return # {{{
+ # Restore tags or update
+ if(@merged[@current.name].empty?)
+ @merged.delete(@current.name)
+ @current.tags = @backup[@current.name]
+ else
+ @current.tags = @merged[@current.name].inject(
+ @backup[@current.name]) { |r, v| r | v.tags }
+ end
+
+ ret = false # }}}
+ when :escape # {{{
+ @merged.delete(@current.name)
+ ret = false # }}}
+ end
+
+ redraw(@win) if(ret)
+
+ ret
+ end # }}}
+
+ ## update # {{{
+ # Update clients and windows
+ ##
+
+ def update
+ @current = Subtlext::View.current
+ @views = Subtlext::View.all.select { |v| v != @current }
+ @selected = @views.first
+
+ @views.unshift(@current)
+
+ # Backup tags of current view
+ unless(@backup.keys.include?(@current.name))
+ @backup[@current.name] = @current.tags
+ end
+
+ # Create empty array
+ unless(@merged.keys.include?(@current.name))
+ @merged[@current.name] = []
+ end
+
+ arrange
+ end # }}}
+
+ ## arrange {{{
+ # Arrange window and subwindows
+ ##
+
+ def arrange
+ geo = Subtlext::Screen.current.geometry
+ width = geo.width * 50 / 100 #< Max width
+ height = @font_height
+ wx = 0
+ wy = 0
+ len = 0
+ wwidth = 0
+
+ # Arrange client windows
+ @views.each_with_index do |v, i|
+ len = @win.font_width(v.name) + 6
+
+ # Wrap lines
+ if(wx + len > width)
+ wwidth = wx if(wx > wwidth)
+ wx = 0
+ wy += @font_height
+ end
+
+ wx += len
+ end
+
+ # Update geometry
+ width = 0 == wwidth ? wx : wwidth
+ height += wy
+ x = geo.x + ((geo.width - width) / 2)
+ y = geo.y + ((geo.height - height) / 2)
+
+ @win.geometry = [ x , y, width, height ]
+ end # }}}
+
+ ## redraw {{{
+ # Redraw window content
+ # @param [Window] w Window instance
+ ##
+
+ def redraw(w)
+ @win.clear
+
+ wx = 0
+ wy = 0
+ len = 0
+
+ @views.each_with_index do |v, i|
+ len = @win.font_width(v.name) + 6
+
+ # Select color
+ if(v == @selected)
+ fg = @colors[:focus_fg]
+ bg = @colors[:focus_bg]
+ elsif(v == @current)
+ fg = @colors[:occupied_fg]
+ bg = @colors[:occupied_bg]
+ else
+ fg = @colors[:unoccupied_fg]
+ bg = @colors[:unoccupied_bg]
+ end
+
+ if(@merged[@current.name].include?(v))
+ fg = @colors[:urgent_fg]
+ end
+
+ @win.draw_rect(wx, wy, len, @font_height, bg, true)
+ @win.draw_text(wx + 3, wy + @font_y + 3, v.name, fg)
+
+ wx += len
+ end
+ end # }}}
+
+ ## show {{{
+ # Show launcher
+ ##
+
+ def show
+ @win.show
+ end # }}}
+
+ ## hide # {{{
+ # Hide launcher
+ ##
+
+ def hide
+ @win.hide
+ end # }}}
+
+ end # }}}
+ end # }}}
+end # }}}
+
+# Implicitly run
+if(__FILE__ == $0)
+ # Set font
+ #Subtle::Contrib::Merger.font =
+ # "xft:DejaVu Sans Mono:pixelsize=80:antialias=true"
+
+ Subtle::Contrib::Merger.run
+end
+
+# vim:ts=2:bs=2:sw=2:et:fdm=marker
diff --git a/bin/subtle-contrib/ruby/positioner.rb b/bin/subtle-contrib/ruby/positioner.rb
new file mode 100644
index 0000000..153b759
--- /dev/null
+++ b/bin/subtle-contrib/ruby/positioner.rb
@@ -0,0 +1,300 @@
+#!/usr/bin/ruby
+#
+# @file Positioner
+#
+# @copyright (c) 2011-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.
+#
+# Select and tag/untag visible views of current client window
+#
+# Colors:
+#
+# Focus - Currently selected view
+# View - Other views
+# Occupied - Views client is visible
+# Urgent - Selected views
+#
+# Keys:
+#
+# Left, Up - Move to left
+# Right, Down - Move to right
+# Escape - Hide/exit
+# Space - Select view
+# Return - Tag/untag selected views and exit hide/exit
+#
+# http://subforge.org/projects/subtle-contrib/wiki/Positioner
+#
+
+require 'singleton'
+
+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 3216 > teeny
+ puts ">>> ERROR: positioner needs at least subtle `0.10.3216' (found: %s)" % [
+ Subtlext::VERSION
+ ]
+ exit
+end
+
+# Positioner class
+module Subtle # {{{
+ module Contrib # {{{
+ class Positioner # {{{
+ include Singleton
+
+ # Default values
+ @@font = '-*-*-medium-*-*-*-14-*-*-*-*-*-*-*'
+
+ # Singleton methods
+
+ ## fonts {{{
+ # Set font strings
+ # @param [String] fonts Fonts array
+ ##
+
+ def self.font=(font)
+ @@font = font
+ end # }}}
+
+ ## run {{{
+ # Run expose
+ ##
+
+ def self.run
+ self.instance.run
+ end # }}}
+
+ # Instance methods
+
+ ## initialize {{{
+ # Create expose instance
+ ##
+
+ def initialize
+ # Values
+ @colors = Subtlext::Subtle.colors
+
+ # Create main window
+ @win = Subtlext::Window.new(:x => 0, :y => 0, :width => 1, :height => 1) do |w|
+ w.name = 'Positioner'
+ w.font = @@font
+ w.foreground = @colors[:title_fg]
+ w.background = @colors[:title_bg]
+ w.border_size = 0
+ end
+
+ # Font metrics
+ @font_height = @win.font_height + 6
+ @font_y = @win.font_y
+
+ # Handler
+ @win.on(:key_down, method(:key_down))
+ @win.on(:draw, method(:redraw))
+ end # }}}
+
+ ## run {{{
+ # Show and run positioner
+ ##
+
+ def run
+ update
+ show
+ hide
+ end # }}}
+
+ private
+
+ ## key_down {{{
+ # Key down handler
+ # @param [String] key Pressed key
+ ##
+
+ def key_down(key, mods)
+ ret = true
+
+ case key
+ when :left, :up # {{{
+ idx = @views.index(@cur_sel)
+ idx -= 1 if 0 < idx
+ @cur_sel = @views[idx] # }}}
+ when :right, :down # {{{
+ idx = @views.index(@cur_sel)
+ idx += 1 if idx < (@views.size - 1)
+ @cur_sel = @views[idx] # }}}
+ when :space # {{{
+ if @selected.include?(@cur_sel)
+ @selected.delete(@cur_sel)
+ @unselected << @cur_sel
+ else
+ @selected << @cur_sel
+ end # }}}
+ when :return # {{{
+ tags = @cur_client.tags
+
+ # Add view tags
+ @selected.each do |sel|
+ unless @cur_views.include?(sel)
+ # Find or create tag
+ tag = Subtlext::Tag.first(sel.name) || Subtlext::Tag.new(sel.name)
+ tag.save
+
+ # Add tag to view
+ sel.tag(tag) unless sel.tags.include?(sel.name)
+
+ tags << tag
+ end
+ end
+
+ # Remove unselected views from tags
+ tags -= @unselected.map(&:name).map { |n| Subtlext::Tag.first(n) }
+
+ # Finally apply tags
+ @cur_client.tags = tags
+
+ ret = false # }}}
+ when :escape # {{{
+ ret = false # }}}
+ end
+
+ redraw(@win) if ret
+
+ ret
+ end # }}}
+
+ ## update # {{{
+ # Update clients and windows
+ ##
+
+ def update
+ @views = Subtlext::View.all
+ @selected = []
+ @unselected = []
+ @cur_client = Subtlext::Client.current
+ @cur_views = @cur_client.views
+ @cur_sel = Subtlext::View.current
+
+ names = @views.map(&:name)
+
+ # Updated selected list
+ @cur_client.tags.each do |t|
+ if names.include?(t.name)
+ @selected << @views[names.index(t.name)]
+ end
+ end
+
+ arrange
+ end # }}}
+
+ ## arrange {{{
+ # Arrange window and subwindows
+ ##
+
+ def arrange
+ geo = @cur_client.geometry
+ width = geo.width #< Max width
+ height = @font_height
+ wx = 0
+ wy = 0
+ len = 0
+ wwidth = 0
+
+ # Arrange client windows
+ @views.each_with_index do |v, i|
+ len = @win.font_width(v.name) + 6
+
+ # Wrap lines
+ if wx + len > width
+ wwidth = wx if wx > wwidth
+ wx = 0
+ wy += @font_height
+ end
+
+ wx += len
+ end
+
+ # Update geometry
+ width = 0 == wwidth ? wx : wwidth
+ height += wy
+ x = geo.x + ((geo.width - width) / 2)
+ y = geo.y + ((geo.height - height) / 2)
+
+ @win.geometry = [ x , y, width, height ]
+ end # }}}
+
+ ## redraw {{{
+ # Redraw window content
+ # @param [Window] w Window instance
+ ##
+
+ def redraw(w)
+ @win.clear
+
+ wx = 0
+ wy = 0
+ len = 0
+
+ @views.each_with_index do |v, i|
+ len = @win.font_width(v.name) + 6
+
+ # Select color
+ if @views[i] == @cur_sel
+ fg = @colors[:focus_fg]
+ bg = @colors[:focus_bg]
+ elsif @cur_views.include?(@views[i])
+ fg = @colors[:occupied_fg]
+ bg = @colors[:occupied_bg]
+ else
+ fg = @colors[:views_fg]
+ bg = @colors[:views_bg]
+ end
+
+ if @selected.include?(@views[i])
+ fg = @colors[:urgent_fg]
+ end
+
+ @win.draw_rect(wx, wy, len, @font_height, bg, true)
+ @win.draw_text(wx + 3, wy + @font_y + 3, v.name, fg)
+
+ wx += len
+ end
+ end # }}}
+
+ ## show {{{
+ # Show launcher
+ ##
+
+ def show
+ @win.show
+ end # }}}
+
+ ## hide # {{{
+ # Hide launcher
+ ##
+
+ def hide
+ @win.hide
+ end # }}}
+ end # }}}
+ end # }}}
+end # }}}
+
+# Implicitly run
+if __FILE__ == $0
+ # Set font
+ #Subtle::Contrib::Merger.font =
+ # 'xft:DejaVu Sans Mono:pixelsize=80:antialias=true'
+
+ Subtle::Contrib::Positioner.run
+end
+
+# vim:ts=2:bs=2:sw=2:et:fdm=marker
diff --git a/bin/subtle-contrib/ruby/selector.rb b/bin/subtle-contrib/ruby/selector.rb
new file mode 100644
index 0000000..13857df
--- /dev/null
+++ b/bin/subtle-contrib/ruby/selector.rb
@@ -0,0 +1,344 @@
+#!/usr/bin/ruby
+#
+# @file Selector
+#
+# @copyright (c) 2011-2012, Christoph Kappel <unexist@dorfelite.net>
+# @version $Id$
+#
+# Client selector that works like the subscription selector in google reader.
+#
+# Colors:
+#
+# Focus - Currently selected client
+# Occupied - Visible clients on current views
+# Unoccupied - Currently no visible clients
+#
+# Keys:
+#
+# Left, Up - Move to left
+# Right, Down - Move to right
+# Tab - Cycle through windows/matches
+# Escape - Leave input mode/exit selector
+# Return - Focus currently selected and hide/exit selector
+# Any capital/digit - Select client prefixed with capital letter/digit
+# Any text - Select client with matching instance name
+#
+# http://subforge.org/projects/subtle-contrib/wiki/Selector
+#
+
+require 'singleton'
+
+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 3216 > teeny
+ puts ">>> ERROR: selector needs at least subtle `0.10.3216' (found: %s)" % [
+ Subtlext::VERSION
+ ]
+ exit
+end
+
+# Launcher class
+module Subtle # {{{
+ module Contrib # {{{
+ class Selector # {{{
+ include Singleton
+
+ # Prefix letters
+ LETTERS = ((49..57).to_a|(65..89).to_a).map(&:chr)
+
+ # Default values
+ @@font = '-*-*-medium-*-*-*-14-*-*-*-*-*-*-*'
+
+ # Singleton methods
+
+ ## fonts {{{
+ # Set font strings
+ # @param [String] fonts Fonts array
+ ##
+
+ def self.font=(font)
+ @@font = font
+ end # }}}
+
+ ## run {{{
+ # Run expose
+ ##
+
+ def self.run
+ self.instance.run
+ end # }}}
+
+ # Instance methods
+
+ ## initialize {{{
+ # Create expose instance
+ ##
+
+ def initialize
+ # Values
+ @colors = Subtlext::Subtle.colors
+ @expanded = false
+ @buffer = ''
+ @x = 0
+ @y = 0
+ @width = 0
+ @height = 0
+
+ # Create main window
+ @win = Subtlext::Window.new(:x => 0, :y => 0, :width => 1, :height => 1) do |w|
+ w.name = 'Selector'
+ w.font = @@font
+ w.foreground = @colors[:title_fg]
+ w.background = @colors[:title_bg]
+ w.border_size = 0
+ end
+
+ # Font metrics
+ @font_height = @win.font_height + 6
+ @font_y = @win.font_y
+
+ # Handler
+ @win.on :key_down, method(:key_down)
+ @win.on :draw, method(:redraw)
+ end # }}}
+
+ ## run {{{
+ # Show and run launcher
+ ##
+
+ def run
+ update
+ show
+ hide
+ end # }}}
+
+ private
+
+ ## key_down {{{
+ # Key down handler
+ # @param [String] key Pressed key
+ ##
+
+ def key_down(key, mods)
+ ret = true
+
+ case key
+ when :left, :up # {{{
+ idx = @clients.index(@current) || 0
+ idx -= 1 if 0 < idx
+ @current = @clients[idx] # }}}
+ when :right, :down # {{{
+ idx = @clients.index(@current) || 0
+ idx += 1 if idx < (@clients.size - 1)
+ @current = @clients[idx] # }}}
+ when :return # {{{
+ @current.focus
+
+ ret = false # }}}
+ when :escape # {{{
+ if @expanded
+ @buffer = ''
+ @expanded = false
+ else
+ ret = false
+ end
+
+ arrange # }}}
+ when :backspace # {{{
+ if @expanded
+ @expanded = (0 < @buffer.chop!.size)
+
+ arrange
+ end # }}}
+ when :tab # {{{
+ if @buffer.empty?
+ clients = @clients
+ else
+ # Select matching clients
+ clients = @clients.select do |c|
+ c.instance.downcase.start_with?(@buffer)
+ end
+ end
+
+ unless (idx = clients.index(@current)).nil?
+ # Cycle between clients
+ if idx < (clients.size - 1)
+ idx += 1
+ else
+ idx = 0
+ end
+
+ @current = clients[idx]
+ end # }}}
+ else # {{{
+ str = key.to_s
+
+ if !(idx = LETTERS.index(str)).nil? and idx < @clients.size
+ @clients[idx].focus
+
+ ret = false
+ elsif !str.empty?
+ @buffer << str.downcase
+
+ @clients.each do |c|
+ if c.instance.downcase.start_with?(@buffer)
+ @current = c
+
+ break
+ end
+ end
+
+ arrange
+ end # }}}
+ end
+
+ redraw(@win) if ret
+
+ ret
+ end # }}}
+
+ ## update # {{{
+ # Update clients and windows
+ ##
+
+ def update
+ @buffer = ''
+ @clients = Subtlext::Client.all
+ @visible = Subtlext::Client.visible
+ @current = Subtlext::Client.current rescue nil
+
+ arrange
+ end # }}}
+
+ ## arrange {{{
+ # Arrange window
+ ##
+
+ def arrange
+ geo = Subtlext::Screen.current.geometry
+ @width = geo.width * 50 / 100 #< Max width
+ @height = @font_height
+ wx = 0
+ wy = 0
+ len = 0
+ wwidth = 0
+
+ # Toggle expand
+ if @buffer.empty?
+ @height = @font_height
+ @expanded = false
+ else
+ @height += @font_height
+ @expanded = true
+ end
+
+ # Calculate window width
+ @clients.each_with_index do |c, i|
+ str = '%s:%s' % [ LETTERS[i], c.instance ]
+ len = @win.font_width(str) + 6
+
+ # Wrap lines
+ if wx + len > @width
+ wwidth = wx if wx > wwidth
+ wx = 0
+ wy += @font_height
+ end
+
+ wx += len
+ end
+
+ # Update window geometry
+ @width = 0 == wwidth ? wx : wwidth
+ @height += wy
+ @x = geo.x + ((geo.width - @width) / 2)
+ @y = geo.y + ((geo.height - @height) / 2)
+
+ @win.geometry = [ @x , @y, @width, @height ]
+ end # }}}
+
+ ## redraw {{{
+ # Redraw window content
+ # @param [Window] w Window instance
+ ##
+
+ def redraw(w)
+ wx = 0
+ wy = 0
+ len = 0
+ wwidth = 0
+
+ @win.clear
+
+ # Render window
+ @clients.each_with_index do |c, i|
+ str = '%s:%s' % [ LETTERS[i], c.instance ]
+ len = @win.font_width(str) + 6
+
+ # Wrap lines
+ if wx + len > @width
+ wwidth = wx if wx > wwidth
+ wx = 0
+ wy += @font_height
+ end
+
+ # Select color
+ if @clients[i] == @current
+ fg = @colors[:focus_fg]
+ bg = @colors[:focus_bg]
+ elsif @visible.include?(@clients[i])
+ fg = @colors[:occupied_fg]
+ bg = @colors[:occupied_bg]
+ else
+ fg = @colors[:views_fg]
+ bg = @colors[:views_bg]
+ end
+
+ @win.draw_rect(wx, wy, len, @font_height, bg, true)
+ @win.draw_text(wx + 3, wy + @font_y + 3, str, fg)
+
+ wx += len
+ end
+
+ # Draw input buffer
+ unless @buffer.empty?
+ @win.draw_text(6, @height - @font_height + @font_y + 3,
+ 'Input: %s' % [ @buffer ])
+ end
+ end # }}}
+
+ ## show {{{
+ # Show launcher
+ ##
+
+ def show
+ @win.show
+ end # }}}
+
+ ## hide # {{{
+ # Hide launcher
+ ##
+
+ def hide
+ @win.hide
+ end # }}}
+ end # }}}
+ end # }}}
+end # }}}
+
+# Implicitly run
+if __FILE__ == $0
+ # Set font
+ #Subtle::Contrib::Selector.font =
+ # 'xft:DejaVu Sans Mono:pixelsize=80:antialias=true'
+
+ Subtle::Contrib::Selector.run
+end
+
+# vim:ts=2:bs=2:sw=2:et:fdm=marker
diff --git a/bin/subtle-contrib/ruby/styler.rb b/bin/subtle-contrib/ruby/styler.rb
new file mode 100644
index 0000000..a3d6e58
--- /dev/null
+++ b/bin/subtle-contrib/ruby/styler.rb
@@ -0,0 +1,859 @@
+#!/usr/bin/ruby
+#
+# @file Styler
+#
+# @copyright (c) 2010-2011, 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.
+#
+# Styler is a helper to create or change subtle color themes
+#
+# http://subforge.org/projects/subtle-contrib/wiki/Styler
+#
+
+require 'singleton'
+
+begin
+ require 'subtle/subtlext'
+rescue LoadError
+ puts ">>> ERROR: Couldn't find subtlext"
+ exit
+end
+
+begin
+ require 'gtk2'
+rescue LoadError
+ puts <<EOF
+>>> ERROR: Couldn't find the gem `gtk2'
+>>> Please install it with following command:
+>>> gem install gtk2
+EOF
+ exit
+end
+
+# Check whether subtle is running
+unless(Subtlext::Subtle.running?)
+ puts ">>> WARNING: Couldn't find running subtle"
+ #exit
+end
+
+# Check for subtlext version
+major, minor, teeny = Subtlext::VERSION.split(".").map(&:to_i)
+if(major == 0 and minor == 10 and 2945 > teeny)
+ puts ">>> ERROR: styler needs at least subtle `0.10.2945' (found: %s)" % [
+ Subtlext::VERSION
+ ]
+ exit
+end
+
+# Styler class
+module Subtle # {{{
+ module Contrib # {{{
+ class ColorButton
+ DEFAULT_COLOR = '#000000'
+
+ attr_reader :value
+
+ ## initialize {{{
+ # Create a new color button
+ # @param [String] color Default color
+ ##
+
+ def initialize(color = DEFAULT_COLOR, &block)
+ @value = color
+ @original = color
+ @callback = nil
+
+ # Create color button
+ @button = Gtk::ColorButton.new(
+ Gdk::Color.parse(color)
+ )
+
+ # Signal handler
+ @button.signal_connect('color-set') do |button|
+ # Assemble hex color
+ @value = '#%02x%02x%02x' % [
+ factor_round((button.color.red.to_f / 65535), 255),
+ factor_round((button.color.green.to_f / 65535), 255),
+ factor_round((button.color.blue.to_f / 65535), 255)
+ ]
+
+ @callback.call(@value) unless(@callback.nil?)
+
+ Styler.instance.render
+ end
+ end # }}}
+
+ ## value= {{{
+ # Set value
+ # @param [Fixnum] value New value
+ ##
+
+ def value=(value)
+ @value = value
+ @button.set_color(Gdk::Color.parse(@value))
+ end # }}}
+
+ ## attach {{{
+ # Attach color button to table
+ # @param [Table] table A #Gtk::Table
+ # @param [Fixnum] x X slot
+ # @param [Fixnum] y Y slot
+ ##
+
+ def attach(table, x, y)
+ table.attach(@button, x, x + 1, y, y + 1, Gtk::FILL, Gtk::SHRINK)
+ end # }}}
+
+ ## reset {{{
+ # Reset color button to default color
+ ##
+
+ def reset
+ @value = @original
+ @button.set_color(Gdk::Color.parse(@original))
+ end # }}}
+
+ ## callback {{{
+ # Add event callback
+ ##
+
+ def callback(&block)
+ @callback = block
+ end # }}}
+
+ private
+
+ def factor_round(val, factor) # {{{
+ val = (val * factor + 0.5).floor
+ val = val > 0 ? val : 0 #< Min
+ val = val > factor ? factor : val #< Max
+
+ val
+ end # }}}
+ end
+
+ class SpinButton
+ attr_reader :value
+
+ ## initialize {{{
+ # Create a new spin button
+ # @param [Fixnum] value Default value
+ # @param [Fixnum] min Minimum value
+ # @param [Fixnum] max Maximum value
+ ##
+
+ def initialize(value = 0, min = 0, max = 100)
+ @value = value
+ @original = value
+ @callback = nil
+
+ # Create adjustment
+ adjustment = Gtk::Adjustment.new(value, min, max, 1, 5, 0)
+
+ # Create spin button
+ @spinner = Gtk::SpinButton.new(adjustment, 0, 0)
+ @spinner.set_size_request(40, -1)
+
+ # Signal handler
+ @spinner.signal_connect('value_changed') do |spinner|
+ @value = spinner.value_as_int
+
+ @callback.call(@value) unless(@callback.nil?)
+
+ Styler.instance.render
+ end
+ end # }}}
+
+ ## value= {{{
+ # Set value
+ # @param [Fixnum] value New value
+ ##
+
+ def value=(value)
+ @value = value
+ @spinner.value = value
+ end # }}}
+
+ ## attach {{{
+ # Attach spin button to table
+ # @param [Table] table A #Gtk::Table
+ # @param [Fixnum] x X slot
+ # @param [Fixnum] y Y slot
+ ##
+
+ def attach(table, x, y)
+ table.attach(@spinner, x, x + 1, y, y + 1, Gtk::FILL, Gtk::SHRINK)
+ end # }}}
+
+ ## reset {{{
+ # Reset spin button to default value
+ ##
+
+ def reset
+ @value = @original
+ @spinner.value = @original
+ end # }}}
+
+ ## callback {{{
+ # Add event callback
+ ##
+
+ def callback(&block)
+ @callback = block
+ end # }}}
+ end
+
+ class Style
+ attr_accessor :buttons
+
+ ## reset {{{
+ # Reset elements of style
+ ##
+
+ def reset
+ @buttons.each do |k, v|
+ if v.respond_to?(:reset)
+ v.reset
+ elsif(v.is_a?(Hash))
+ v.each do |side, spin|
+ spin.reset if(spin.respond_to?(:reset))
+ end
+ end
+ end
+ end # }}}
+
+ ## append {{{
+ # Append to notebook
+ # @param [Notebook] notebook A #Gtk::Notebook
+ ##
+
+ def append(notebook)
+ label = Gtk::Label.new(@name)
+ notebook.append_page(@table, label)
+ end # }}}
+
+ ## [] {{{
+ # Acces values
+ # @param [Symbol] name Value name
+ ##
+
+ def [](name)
+ case @buttons[name]
+ when ColorButton then @buttons[name].value
+ when Hash then @buttons[name]
+ end
+ end # }}}
+
+ ## attach_label {{{
+ # Attach a label to the table
+ # @param [Fixnum] x X slot
+ # @param [Fixnum] y Y slot
+ # @param [String] caption Label caption
+ ##
+
+ def attach_label(x, y, caption)
+ label = Gtk::Label.new(caption)
+ @table.attach(label, x, x + 1, y, y + 1)
+ end # }}}
+
+ ## sum {{{
+ # Get total width of given styles
+ # @param [Array] List of styles
+ # @return [Fixnum] Width in pixel
+ ##
+
+ def sum(list)
+ width = 0
+
+ [ :border, :padding, :margin ].each do |button|
+ list.each do |l|
+ if(@buttons[button][l].respond_to?(:value))
+ width += @buttons[button][l].value
+ end
+ end
+ end
+
+ width
+ end # }}}
+
+ private
+
+ def compact(name) # {{{
+ ret = []
+ val = @buttons[name]
+
+ # Compact padding/margin values
+ if(val[:top].value == val[:bottom].value and
+ val[:top].value == val[:right].value and
+ val[:top].value == val[:left].value)
+ ret << val[:top].value
+ elsif(val[:top].value == val[:bottom].value and
+ val[:left].value == val[:right].value)
+ ret << val[:top].value
+ ret << val[:left].value
+ elsif(val[:left].value == val[:right].value)
+ ret << val[:top].value
+ ret << val[:left].value
+ ret << val[:bottom].value
+ else
+ ret = val.values.map(&:value)
+ end
+
+ ret.join(', ')
+ end # }}}
+ end
+
+ class StyleNormal < Style
+
+ ## initialize {{{
+ # Create a new style element
+ # @param [String] name Element name
+ # @param [Array] colors Color array
+ ##
+
+ def initialize(name, colors)
+ @name = name
+ @buttons = {}
+
+ # Table
+ @table = Gtk::Table.new(5, 7)
+
+ # Labels: Vertical
+ attach_label(0, 1, 'Foreground')
+ attach_label(0, 2, 'Background')
+ attach_label(0, 3, 'Top')
+ attach_label(0, 4, 'Right')
+ attach_label(0, 5, 'Bottom')
+ attach_label(0, 6, 'Left')
+
+ # Labels: Horizontal
+ attach_label(1, 0, 'Color')
+ attach_label(2, 0, 'Border')
+ attach_label(3, 0, 'Padding')
+ attach_label(4, 0, 'Margin')
+
+ # Color buttons
+ y = 1
+
+ {
+ :fg => 'fg', :bg => 'bg', :top => 'bo_top',
+ :right => 'bo_right', :bottom => 'bo_bottom', :left => 'bo_left'
+ }.each do |name, suffix|
+ @buttons[name] = ColorButton.new(
+ colors[('%s_%s' % [ @name.downcase, suffix ]).to_sym] ||
+ ColorButton::DEFAULT_COLOR
+ )
+ @buttons[name].attach(@table, 1, y)
+
+ y += 1
+ end
+
+ # Spin buttons
+ x = 2
+
+ [ :border, :padding, :margin ].each do |name|
+ @buttons[name] = {}
+ y = 3
+
+ [ :top, :right, :bottom, :left ].each do |side|
+ @buttons[name][side] = SpinButton.new(0)
+ @buttons[name][side].attach(@table, x, y)
+
+ y += 1
+ end
+
+ x += 1
+ end
+ end # }}}
+
+ ## dump {{{
+ # Print style element
+ ##
+
+ def dump
+ puts "style :#{@name.downcase} do"
+ puts " foreground '#{@buttons[:fg].value}'"
+ puts " background '#{@buttons[:bg].value}'"
+
+ # Compact borders
+ if(@buttons[:top].value == @buttons[:right].value and
+ @buttons[:top].value == @buttons[:bottom].value and
+ @buttons[:top].value == @buttons[:left].value and
+ @buttons[:border][:top].value == @buttons[:border][:left].value and
+ @buttons[:border][:top].value == @buttons[:border][:bottom].value and
+ @buttons[:border][:top].value == @buttons[:border][:right].value)
+ puts " border '#{@buttons[:top].value}', #{@buttons[:border][:top].value}"
+ else
+ puts " border_top '#{@buttons[:top].value}', #{@buttons[:border][:top].value}"
+ puts " border_right '#{@buttons[:right].value}', #{@buttons[:border][:right].value}"
+ puts " border_bottom '#{@buttons[:bottom].value}', #{@buttons[:border][:bottom].value}"
+ puts " border_left '#{@buttons[:left].value}', #{@buttons[:border][:left].value}"
+ end
+
+ puts " padding #{compact(:padding)}"
+ puts " margin #{compact(:margin)}"
+ puts "end"
+ puts
+ end # }}}
+ end
+
+ class StyleClients < Style
+
+ ## initialize {{{
+ # Create a new other element
+ # @param [String] name Element name
+ # @param [Array] colors Color array
+ ##
+
+ def initialize(name, colors)
+ @name = name
+ @table = Gtk::Table.new(5, 7)
+ @buttons = {}
+
+ # Labels: Vertical
+ attach_label(0, 1, 'Client active')
+ attach_label(0, 2, 'Client inactive')
+ attach_label(0, 3, 'Top')
+ attach_label(0, 4, 'Right')
+ attach_label(0, 5, 'Bottom')
+ attach_label(0, 6, 'Left')
+
+ # Labels: Horizontal
+ attach_label(1, 0, 'Color')
+ attach_label(2, 0, 'Border')
+ attach_label(3, 0, 'Padding')
+ attach_label(4, 0, 'Margin')
+
+ # Buttons
+ y = 1
+
+ [ :active, :inactive ].each do |name|
+ # Color button
+ @buttons[name] = ColorButton.new(
+ colors[('client_%s' % name).to_sym]
+ )
+ @buttons[name].attach(@table, 1, y)
+
+ # Border spinner
+ sym = ('%s_border' % [ name ]).to_sym
+ @buttons[sym] = SpinButton.new(2)
+ @buttons[sym].attach(@table, 2, y)
+
+ y += 1
+ end
+
+ # Margin
+ @buttons[:margin] = {}
+
+ [ :top, :right, :bottom, :left ].each do |side|
+ @buttons[:margin][side] = SpinButton.new(0)
+ @buttons[:margin][side].attach(@table, 4, y)
+
+ y += 1
+ end
+
+ # Fill remaining fields
+ @buttons[:fg] = @buttons[:active]
+ @buttons[:bg] = @buttons[:inactive]
+ @buttons[:border ] = {
+ top: @buttons[:active_border],
+ right: @buttons[:active_border],
+ bottom: @buttons[:active_border],
+ left: @buttons[:active_border]
+ }
+
+ # Just for text placement
+ @buttons[:padding] = { top: 5, right: 0, bottom: 0, left: 5 }
+ end # }}}
+
+ ## dump {{{
+ # Print style element
+ ##
+
+ def dump
+ puts <<STYLE
+style :#{@name.downcase} do
+ active '#{@buttons[:active].value}'
+ inactive '#{@buttons[:inactive].value}'
+ margin #{compact(:margin)}
+end\n
+STYLE
+ end # }}}
+ end
+
+ class StyleSubtle < Style
+
+ ## initialize {{{
+ # Create a new other element
+ # @param [String] name Element name
+ # @param [Array] colors Color array
+ ##
+
+ def initialize(name, colors)
+ @name = name
+ @table = Gtk::Table.new(5, 7)
+ @buttons = {}
+
+ # Labels: Vertical
+ attach_label(0, 1, 'Panel top')
+ attach_label(0, 2, 'Panel bottom')
+ attach_label(0, 3, 'Background')
+ attach_label(0, 4, 'Stipple')
+ attach_label(0, 5, 'Top')
+ attach_label(0, 6, 'Right')
+ attach_label(0, 7, 'Bottom')
+ attach_label(0, 8, 'Left')
+
+ # Labels: Horizontal
+ attach_label(1, 0, 'Color')
+ attach_label(2, 0, 'Border')
+ attach_label(3, 0, 'Padding')
+ attach_label(4, 0, 'Margin')
+
+ # Buttons
+ y = 1
+
+ [ :panel_top, :panel_bottom, :background, :stipple ].each do |name|
+ # Color button
+ @buttons[name] = ColorButton.new(colors[name])
+ @buttons[name].attach(@table, 1, y)
+
+ y += 1
+ end
+
+ # Padding
+ @buttons[:padding] = {}
+
+ [ :top, :right, :bottom, :left ].each do |side|
+ @buttons[:padding][side] = SpinButton.new(0)
+ @buttons[:padding][side].attach(@table, 3, y)
+
+ y += 1
+ end
+ end # }}}
+
+ ## dump {{{
+ # Print style element
+ ##
+
+ def dump
+ puts "style :#{@name.downcase} do"
+
+ # Compact panel colors
+ if(@buttons[:panel_top].value == @buttons[:panel_bottom].value)
+ puts " panel '#{@buttons[:panel_top].value}'"
+ else
+ puts " panel_top '#{@buttons[:panel_top].value}'"
+ puts " panel_bottom '#{@buttons[:panel_bottom].value}'"
+ end
+
+ puts " background '#{@buttons[:background].value}'"
+ puts " stipple '#{@buttons[:stipple].value}'"
+ puts " padding #{compact(:padding)}"
+ puts "end"
+ puts
+ end # }}}
+ end
+
+ class Styler < Gtk::Window
+ include Singleton
+
+ BORDER_WIDTH = 2
+ WINDOW_WIDTH = 790
+ AREA_WIDTH = WINDOW_WIDTH - 16 #< Padding + spacing
+ CLIENT_WIDTH = (AREA_WIDTH - 2 * BORDER_WIDTH) / 2
+
+ WINDOW_HEIGHT = 540
+ AREA_HEIGHT = 150
+
+ ## initialize {{{
+ # Init window
+ ##
+
+ def initialize
+ super
+
+ # Options
+ set_title('Styler for subtle #{Subtlext::VERSION}')
+ set_wmclass('styler', 'subtle')
+ set_resizable(false)
+ set_keep_above(true)
+ set_size_request(WINDOW_WIDTH, WINDOW_HEIGHT)
+ set_window_position(Gtk::Window::POS_CENTER)
+ stick
+
+ # Signals {{{
+ signal_connect('delete_event') do
+ false
+ end
+
+ signal_connect('destroy') do
+ Gtk.main_quit
+ end # }}}
+
+ # Alignment
+ align = Gtk::Alignment.new(0.5, 0.5, 0.5, 0.5)
+ align.set_padding(7, 7, 7, 7)
+ add(align)
+
+ # Vbox
+ vbox = Gtk::VBox.new()
+ align.add(vbox)
+
+ # Frame {{{
+ frame = Gtk::Frame.new
+ frame.set_border_width(0)
+ frame.set_shadow_type(Gtk::SHADOW_NONE)
+ vbox.pack_start(frame) # }}}
+
+ # Area {{{
+ @area = Gtk::DrawingArea.new
+ @area.set_size_request(AREA_WIDTH, AREA_HEIGHT)
+ @area.signal_connect('expose_event') do
+ expose(@area.window.create_cairo_context)
+ end
+ frame.add(@area) # }}}
+
+ # Notebook {{{
+ notebook = Gtk::Notebook.new
+ notebook.set_tab_pos(Gtk::PositionType::LEFT)
+ vbox.pack_start(notebook, true, false, 5) # }}}
+
+ # Styles {{{
+ colors = {}
+ Subtlext::Subtle.colors.map { |k, v| colors[k] = v.to_hex }
+ @styles = {}
+
+ # Styles and pages
+ [
+ 'all', 'title', 'views', 'focus', 'urgent',
+ 'occupied', 'sublets', 'separator'
+ ].each do |caption|
+ sym = caption.to_sym
+ @styles[sym] = StyleNormal.new(caption.capitalize, colors)
+ @styles[sym].append(notebook)
+ end
+
+ # Clients
+ @styles[:clients] = StyleClients.new('Clients', colors)
+ @styles[:clients].append(notebook)
+
+ # Subtle
+ @styles[:subtle] = StyleSubtle.new('Subtle', colors)
+ @styles[:subtle].append(notebook)
+
+ # All: Update border, padding and margin
+ [ :border, :padding, :margin ].each do |name|
+ [ :top, :right, :bottom, :left ].each do |side|
+ @styles[:all].buttons[name][side].callback do |value|
+ [
+ :title, :views, :focus, :urgent,
+ :occupied, :sublets, :separator
+ ].each do |style|
+ @styles[style].buttons[name][side].value = value
+ end
+ end
+ end
+ end
+
+ # All: Update colors
+ [ :fg, :bg, :top, :right, :bottom, :left ].each do |name|
+ @styles[:all].buttons[name].callback do |value|
+ [
+ :title, :views, :focus, :urgent,
+ :occupied, :sublets, :separator
+ ].each do |style|
+ @styles[style].buttons[name].value = value
+ end
+ end
+ end # }}}
+
+ # Hbox
+ hbox = Gtk::HButtonBox.new
+ vbox.pack_start(hbox, false, false, 5)
+
+ # Print button {{{
+ button = Gtk::Button.new('Print')
+ hbox.pack_start(button, false, false, 2)
+ button.signal_connect('clicked') do
+ @styles.select { |k, v| :all != k }.each do |k, v|
+ v.dump
+ end
+ end # }}}
+
+ # Reset button {{{
+ button = Gtk::Button.new('Reset')
+ hbox.pack_start(button, false, false, 2)
+ button.signal_connect('clicked') do
+ @styles.each do |k, v|
+ if v.respond_to? :reset
+ v.reset
+ end
+ end
+
+ @area.signal_emit('expose-event', nil)
+ end # }}}
+
+ # Exit button {{{
+ button = Gtk::Button.new('Exit')
+ hbox.pack_start(button, false, false, 2)
+ button.signal_connect('clicked') do
+ Gtk.main_quit
+ end # }}}
+
+ show_all
+ end # }}}
+
+ ## render {{{
+ # Render preview
+ ##
+
+ def render
+ @area.signal_emit('expose-event', nil)
+ end # }}}
+
+ private
+
+ def expose(cr) # {{{
+ # Font face, size and height
+ cr.select_font_face('Arial', 'normal')
+ cr.set_font_size(12)
+
+ extents = cr.font_extents
+ @font_height = extents.ascent + extents.descent + 2;
+ @font_y = (extents.height - 2 + extents.ascent) / 2
+
+ # Calculate item heights
+ panel_height = @font_height
+
+ [
+ :title, :views, :focus, :urgent,
+ :occupied, :sublets, :separator
+ ].each do |panel|
+ value = @font_height + @styles[panel].sum([ :top, :bottom ])
+
+ panel_height = value if(value > panel_height)
+ end
+
+ client_height = AREA_HEIGHT - 2 * panel_height - 2 * BORDER_WIDTH
+
+ # Border
+ cr.set_source_rgb(1.0, 0.0, 0.0)
+ cr.rectangle(0, 0, AREA_WIDTH, AREA_HEIGHT)
+ cr.fill
+
+ # Background
+ draw_rect(cr, BORDER_WIDTH, BORDER_WIDTH,
+ AREA_WIDTH - 2 * BORDER_WIDTH, AREA_HEIGHT - 2 * BORDER_WIDTH,
+ @styles[:subtle][:background])
+
+ # Panels
+ draw_rect(cr, BORDER_WIDTH, BORDER_WIDTH,
+ AREA_WIDTH - 2 * BORDER_WIDTH, panel_height,
+ @styles[:subtle][:panel_top]
+ )
+ draw_rect(cr, BORDER_WIDTH, BORDER_WIDTH + panel_height + client_height,
+ AREA_WIDTH - 2 * BORDER_WIDTH, panel_height,
+ @styles[:subtle][:panel_bottom]
+ )
+
+ # Clients
+ draw_box(cr, BORDER_WIDTH, BORDER_WIDTH + panel_height, CLIENT_WIDTH,
+ client_height, 'active', @styles[:clients])
+
+ draw_box(cr, BORDER_WIDTH + CLIENT_WIDTH, BORDER_WIDTH + panel_height,
+ CLIENT_WIDTH, client_height, 'inactive', @styles[:clients])
+
+ # Panel items
+ x = BORDER_WIDTH
+
+ [
+ 'focus', 'views', 'urgent', 'occupied', 'title'
+ ].each do |name|
+ sym = name.to_sym
+ extents = cr.text_extents(name)
+ width = @styles[sym].sum([ :left, :right ]) + extents.x_advance
+
+ draw_box(cr, x, BORDER_WIDTH, width,
+ panel_height, name, @styles[sym])
+
+ x += width
+ end
+
+ # Sublets
+ x = BORDER_WIDTH
+ {
+ 'sublet1' => :sublets, '|' => :separator, 'sublet2' => :sublets
+ }.each do |name, style|
+ extents = cr.text_extents(name)
+ width = @styles[style].sum([ :left, :right ]) + extents.x_advance
+
+ draw_box(cr, x, BORDER_WIDTH + panel_height + client_height,
+ width, panel_height, name, @styles[style])
+
+ x += width
+ end
+ end # }}}
+
+ def draw_text(cr, x, y, text, color) # {{{
+ cr.set_source_color(Gdk::Color.parse(color))
+ cr.move_to(x, y + @font_y)
+ cr.show_text(text)
+ cr.stroke
+ end # }}}
+
+ def draw_rect(cr, x, y, width, height, color) # {{{
+ cr.set_source_color(Gdk::Color.parse(color))
+ cr.rectangle(x, y, width, height)
+ cr.fill
+ end # }}}
+
+ def draw_box(cr, x, y, width, height, text, style) # {{{
+ mw = style[:margin][:left].value + style[:margin][:right].value
+ mh = style[:margin][:top].value + style[:margin][:bottom].value
+
+ # Filling
+ draw_rect(cr, x + style[:margin][:left].value,
+ y + style[:margin][:top].value, width - mw, height - mh, style[:bg])
+
+ # Borders
+ draw_rect(cr, x + style[:margin][:left].value,
+ y + style[:margin][:top].value, width - mw,
+ style[:border][:top].value,
+ text.end_with?('active') ? style[text.to_sym] : style[:top])
+
+ draw_rect(cr, x + width - style[:border][:right].value -
+ style[:margin][:right].value, y + style[:margin][:top].value,
+ style[:border][:right].value, height - mh,
+ text.end_with?('active') ? style[text.to_sym] : style[:right])
+
+ draw_rect(cr, x + style[:margin][:left].value, y + height -
+ style[:border][:bottom].value - style[:margin][:bottom].value,
+ width - mw, style[:border][:bottom].value,
+ text.end_with?('active') ? style[text.to_sym] : style[:bottom])
+
+ draw_rect(cr, x + style[:margin][:left].value,
+ y + style[:margin][:top].value, style[:border][:left].value,
+ height - mh,
+ text.end_with?('active') ? style[text.to_sym] : style[:left])
+
+ # Text
+ draw_text(cr, x + style.sum([ :left ]),
+ y + style.sum([ :top ]), text, style[:fg]
+ )
+ end # }}}
+ end
+ end # }}}
+end # }}}
+
+# Implicitly run<
+if __FILE__ == $0
+ Gtk.init
+ Subtle::Contrib::Styler.instance
+ Gtk.main
+end
+
+# vim:ts=2:bs=2:sw=2:et:fdm=marker
diff --git a/bin/subtle-contrib/ruby/termstyler.rb b/bin/subtle-contrib/ruby/termstyler.rb
new file mode 100644
index 0000000..3ea497a
--- /dev/null
+++ b/bin/subtle-contrib/ruby/termstyler.rb
@@ -0,0 +1,289 @@
+#!/usr/bin/ruby
+#
+# @file TermStyler
+#
+# @copyright (c) 2011, Christoph Kappel <unexist@dorfelite.net>
+# @version $Id$
+#
+# TermStyler is a helper to create or change terminal color themes.
+#
+# http://subforge.org/projects/subtle-contrib/wiki/TermStyler
+#
+
+begin
+ require "gtk2"
+rescue LoadError
+ puts <<EOF
+>>> ERROR: Couldn't find the gem `gtk2'
+>>> Please install it with following command:
+>>> gem install gtk2
+EOF
+ exit
+end
+
+# TermStyler class
+module Subtle # {{{
+ module Contrib # {{{
+ class TermStyler < Gtk::Window # {{{
+ WINDOW_WIDTH = 720
+ WINDOW_HEIGHT = 420
+ AREA_WIDTH = 404
+ AREA_HEIGHT = 365
+ REGEXP = Regexp.new("^[^!#]*(color[0-9]+|foreground|background)\s*:\s*(#[0-9a-zA-Z]*)")
+
+ ## initialize {{{
+ # Init window
+ ##
+
+ def initialize
+ super
+
+ # Options
+ set_title("Styler for terminals")
+ set_wmclass("termstyler", "subtle")
+ set_resizable(false)
+ set_keep_above(true)
+ set_size_request(WINDOW_WIDTH, WINDOW_HEIGHT)
+ set_window_position(Gtk::Window::POS_CENTER)
+ stick
+
+ # Signals
+ signal_connect("delete_event") do
+ false
+ end
+
+ signal_connect("destroy") do
+ Gtk.main_quit
+ end
+
+ # Alignment
+ align = Gtk::Alignment.new(0.5, 0.5, 0.5, 0.5)
+ align.set_padding(7, 7, 7, 7)
+
+ add(align)
+
+ # Vbox
+ vbox = Gtk::VBox.new
+ align.add(vbox)
+
+ # HBox
+ hbox = Gtk::HBox.new
+ vbox.pack_start(hbox)
+
+ # Table
+ table = Gtk::Table.new(4, 9)
+ hbox.pack_start(table, true, false, 5)
+
+ # Frame
+ frame = Gtk::Frame.new
+ frame.set_border_width(0)
+ frame.set_shadow_type(Gtk::SHADOW_NONE)
+ hbox.pack_start(frame)
+
+ # Area
+ @area = Gtk::DrawingArea.new
+ @area.set_size_request(AREA_WIDTH, AREA_HEIGHT)
+ @area.signal_connect("expose_event") do
+ expose(@area.window.create_cairo_context)
+ end
+ frame.add(@area)
+
+ # Get colors
+ load_colors
+
+ # Color buttons
+ @buttons = {}
+ row = 0
+ idx1 = 0
+ idx2 = 8
+
+ @buttons["foreground"] = color_button(table, row, 0, "foreground")
+ @buttons["background"] = color_button(table, row, 2, "background")
+
+ 8.times do |i|
+ row += 1
+ name1 = "color#{idx1 + i}"
+ name2 = "color#{idx2 + i}"
+
+ @buttons[name1] = color_button(table, row, 0, name1)
+ @buttons[name2] = color_button(table, row, 2, name2)
+ end
+
+ # Hbox
+ hbox = Gtk::HButtonBox.new
+ vbox.pack_start(hbox, false, false, 5)
+
+ # Print button
+ button = Gtk::Button.new("Print")
+ hbox.pack_start(button, false, false, 2)
+ button.signal_connect("clicked") do
+ print_pair("", "foreground", "background")
+ print_pair("black", "color0", "color8")
+ print_pair("red", "color1", "color9")
+ print_pair("green", "color2", "color10")
+ print_pair("yellow", "color3", "color11")
+ print_pair("blue", "color4", "color12")
+ print_pair("magenta", "color5", "color13")
+ print_pair("cyan", "color6", "color14")
+ print_pair("white", "color7", "color15")
+ end
+
+ # Reset button
+ button = Gtk::Button.new("Reset")
+ hbox.pack_start(button, false, false, 2)
+ button.signal_connect("clicked") do
+ load_colors
+
+ # Reset color buttons
+ @colors.each do |k, v|
+ @buttons[k].set_color(Gdk::Color.parse(v))
+ end
+
+ @area.signal_emit("expose-event", nil)
+ end
+
+ # Exit button
+ button = Gtk::Button.new("Exit")
+ hbox.pack_start(button, false, false, 2)
+ button.signal_connect("clicked") do
+ Gtk.main_quit
+ end
+
+ show_all
+ end # }}}
+
+ private
+
+ def print_pair(name, col1, col2) # {{{
+ puts "!#{name}" unless(name.empty?)
+ puts "*%-11s %s" % [ col1 + ":", @colors[col1] ]
+ puts "*%-11s %s" % [ col2 + ":", @colors[col2] ]
+ end # }}}
+
+ def load_colors # {{{
+ @colors = Hash[*(16.times.to_a.map { |i|
+ [ "color#{i}", "#000000" ]
+ } << ["forground", "#ffffff", "background", "#000000"]).flatten]
+
+ # Load and parse Xdefaults
+ File.open("#{ENV["HOME"]}/.Xdefaults") do |f|
+ while(line = f.gets)
+ if line.match(REGEXP)
+ @colors[$~[1]] = $~[2]
+ end
+ end
+ end
+ rescue
+ end # }}}
+
+ def scale_round(val, factor) # {{{
+ val = (val * factor + 0.5).floor
+ val = val > 0 ? val : 0 #< Min
+ val = val > factor ? factor : val #< Max
+
+ val
+ end # }}}
+
+ def color_button(table, row, col, name) # {{{
+ caption = name.to_s.split(/[^a-z0-9]/i).map { |w|
+ w.capitalize
+ }.join(" ")
+ label = Gtk::Label.new(caption)
+ button = Gtk::ColorButton.new(
+ Gdk::Color.parse(@colors[name])
+ )
+
+ # Align
+ align = Gtk::Alignment.new(1.0, 0.0, 0, 0)
+ align.add(label)
+
+ # Signal handler
+ button.signal_connect("color-set") do |button|
+ begin
+ # Assemble and update hex color
+ @colors[name] = "#%02X%02X%02X" % [
+ scale_round((button.color.red.to_f / 65535), 255),
+ scale_round((button.color.green.to_f / 65535), 255),
+ scale_round((button.color.blue.to_f / 65535), 255)
+ ]
+
+ @area.signal_emit("expose-event", nil)
+ rescue => error
+ puts error, error.backtrace
+ end
+ end
+
+ # Attach to table
+ table.attach(align, 0 + col, 1 + col, 0 + row, 1 + row,
+ Gtk::SHRINK, Gtk::SHRINK, 5, 5)
+ table.attach(button, 1 + col, 2 + col, 0 + row, 1 + row,
+ Gtk::SHRINK, Gtk::SHRINK, 5, 5)
+
+ return button
+ end # }}}
+
+ def expose(cr) # {{{
+ # Border
+ cr.set_source_rgb(1.0, 0.0, 0.0)
+ cr.rectangle(0, 0, AREA_WIDTH, AREA_HEIGHT)
+ cr.fill
+
+ # Rects
+ width = (AREA_WIDTH - 4) / 8
+
+ 8.times do |i|
+ draw_rect(cr, 2 + width * i, 2, width, AREA_HEIGHT - 4, @colors["color#{i}"])
+ end
+
+ # Fore-/background
+ draw_rect(cr, 2, 2, AREA_WIDTH - 4, 38, @colors["background"])
+
+ # Labels
+ xary = 8.times.map { |i| 10 + width * i }
+ y = 0
+
+ 8.times do |i|
+ draw_text(cr, "Norm", xary, 60 + y, @colors["color#{i}"])
+ draw_text(cr, "Bold", xary, 75 + y, @colors["color#{8 + i}"], true)
+
+ y += 40
+ end
+
+ draw_text(cr, "Col", xary, 24, @colors["foreground"])
+ end # }}}
+
+ def draw_rect(cr, x, y, width, height, color) # {{{
+ cr.set_source_color(Gdk::Color.parse(color))
+ cr.rectangle(x, y, width, height)
+ cr.fill
+ end # }}}
+
+ def draw_text(cr, text, xary, y, color, bold = false) # {{{
+ cr.set_source_color(Gdk::Color.parse(color))
+ cr.select_font_face("Arial", Cairo::FONT_SLANT_NORMAL,
+ bold ? Cairo::FONT_WEIGHT_BOLD : Cairo::FONT_WEIGHT_NORMAL
+ )
+ cr.set_font_size(14)
+
+ xary = [ xary ] if(xary.is_a?(Fixnum))
+
+ # Draw text on each xary pos
+ xary.each do |x|
+ cr.move_to(x, y)
+ cr.show_text(text)
+ end
+
+ cr.stroke
+ end # }}}
+ end # }}}
+ end # }}}
+end # }}}
+
+# Implicitly run<
+if __FILE__ == $0
+ Gtk.init
+ Subtle::Contrib::TermStyler.new
+ Gtk.main
+end
+
+# vim:ts=2:bs=2:sw=2:et:fdm=marker
diff --git a/bin/subtle-contrib/ruby/vitag.rb b/bin/subtle-contrib/ruby/vitag.rb
new file mode 100644
index 0000000..4a1e6c8
--- /dev/null
+++ b/bin/subtle-contrib/ruby/vitag.rb
@@ -0,0 +1,127 @@
+#!/usr/bin/ruby
+#
+# @file Vitag
+#
+# @copyright (c) 2010-2011, 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.
+#
+# Vitag is a helper to edit window/view tagging with any $EDITOR
+#
+# http://subforge.org/projects/subtle-contrib/wiki/Vitag
+#
+
+require 'tempfile'
+require 'digest/md5'
+
+begin
+ require 'subtle/subtlext'
+rescue LoadError
+ puts ">>> ERROR: Couldn't find subtlext"
+ exit
+end
+
+# Check if $EDITOR is set
+if ENV['EDITOR'].nil?
+ puts <<-EOF
+>>> ERROR: Couldn't find $EDITOR envorinment variable
+>>> Please set it like this: export EDITOR=vim
+ EOF
+ exit
+end
+
+# Check whether subtle is running
+unless(Subtlext::Subtle.running?)
+ puts ">>> ERROR: Couldn't find running subtle"
+ exit
+end
+
+# Check for subtlext version
+major, minor, teeny = Subtlext::VERSION.split('.').map(&:to_i)
+if major == 0 and minor == 10 and 3104 > teeny
+ puts ">>> ERROR: vitag needs at least subtle `0.10.3104' (found: %s)" % [
+ Subtlext::VERSION
+ ]
+ exit
+end
+
+# Collect views and clients
+views = Subtlext::View.all
+clients = Subtlext::Client.all
+
+# Create temp file
+temp = Tempfile.new('vitag-')
+
+# Fill in tags
+temp.puts('# Views')
+
+views.each do |v|
+ temp.puts('@%s %s' % [
+ v.name,
+ v.tags.map { |t| '#%s' % [ t ] }.join(' ')
+ ])
+end
+
+# Fill in tags
+temp.puts('')
+temp.puts('# Clients')
+
+clients.each do |c|
+ # Remove hashes from string
+ name = c.to_str.split('#').first
+
+ temp.puts('%s (%s) %s' % [
+ name, c.instance, c.tags.map { |t| '#%s' % [ t ] }.join(' ')
+ ])
+end
+
+temp.flush
+
+# Store checksum for check
+md5 = Digest::MD5.file(temp.path)
+
+# Start editor
+system('$EDITOR %s' % [ temp.path ])
+
+temp.rewind
+
+# Check for changes
+if md5 != Digest::MD5.file(temp.path)
+
+ # Read temp file
+ temp.readlines.each do |line|
+
+ # Handle lines
+ case line[0]
+ when '@' then cur = views.shift
+ when '#', ' ', "\n" then next
+ else cur = clients.shift
+ end
+
+ # Select tags and sanitize
+ tags = line.split('#')[1..-1].map(&:rstrip)
+
+ # Check for valid object
+ if cur and tags
+
+ # Find or create tags
+ tags.map! do |name|
+ tag = Subtlext::Tag.first(name) || Subtlext::Tag.new(name)
+ tag.save
+
+ tag
+ end
+
+ # Finally assign tags
+ cur.tags = tags
+
+ cur = nil
+ end
+ end
+end
+
+temp.close
+
+# vim:ts=2:bs=2:sw=2:et:fdm=marker