--- /dev/null
+Tilman Sauerbeck (tilman at code-monkey de)
--- /dev/null
+Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+require "rake/packagetask"
+require "rake/contrib/sshpublisher"
+
+PKG_NAME = "embrace"
+PKG_VERSION = File.read("bin/embrace").
+ match(/^\s*VERSION = \"(.*)\"$/).captures.first
+
+task :install do
+ sitelibdir = Config::CONFIG["sitelibdir"]
+ destdir = ENV["DESTDIR"] || ""
+ prefix = ENV["PREFIX"] || "/usr/local"
+
+ ddir = destdir + prefix + "/bin"
+
+ FileUtils::Verbose.mkdir_p(ddir) unless File.directory?(ddir)
+ FileUtils::Verbose.install("bin/embrace", ddir, :mode => 0755)
+
+ ddir = destdir + sitelibdir + "/#{PKG_NAME}"
+
+ FileUtils::Verbose.mkdir_p(ddir) unless File.directory?(ddir)
+ FileUtils::Verbose.install(Dir["lib/embrace/*.rb"], ddir,
+ :mode => 0644)
+
+ ddir = destdir + prefix + "/share/#{PKG_NAME}"
+
+ FileUtils::Verbose.mkdir_p(ddir) unless File.directory?(ddir)
+ FileUtils::Verbose.install(Dir["data/*"], ddir, :mode => 0644)
+end
+
+Rake::PackageTask.new(PKG_NAME, PKG_VERSION) do |t|
+ t.need_tar_bz2 = true
+ t.package_files.include("[A-Z]*", "bin/embrace", "lib/embrace/*.rb",
+ "data/*")
+end
+
+task :publish => [:package] do
+ Rake::SshFilePublisher.new("code-monkey.de", ".", "pkg",
+ "#{PKG_NAME}-#{PKG_VERSION}.tar.bz2").
+ upload
+end
--- /dev/null
+#!/usr/bin/env ruby
+
+# vim:syn=ruby
+#
+# Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of version 2 of the GNU General Public License as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+require "ecore"
+require "ecore_x"
+require "ecore_evas"
+require "singleton"
+require "yaml"
+require "embrace/imap"
+
+PKG_NAME = "embrace"
+DATADIR = "/usr/local/share/#{PKG_NAME}/"
+
+class Evas::EvasObject
+ def move_relative(obj, x, y)
+ # FIXME investigate whether there's an easier way
+ move(*obj.geometry[0..1].zip([x, y]).map { |(a, b)| a + b })
+ end
+
+ def center(obj)
+ a = geometry
+ b = obj.geometry
+
+ move_relative(obj, (b[2] / 2) - (a[2] / 2),
+ (b[3] / 2) - (a[3] / 2))
+ end
+
+ def alpha=(alpha)
+ set_color(*(get_color[0..-2] << alpha))
+ end
+end
+
+module Embrace
+ VERSION = "0.0.1"
+ ICON_FILE = DATADIR + "l33t_MAI_envelope.png"
+ MAX_ICONS = 11
+
+ class ZeroToOneAnimator < Ecore::Animator
+ def initialize(duration)
+ @finished = nil
+
+ started = Time.now
+
+ super() do
+ v = [(Time.now - started) / duration, 1.0].min
+
+ yield v
+
+ @finished.call(self) unless (v < 1.0) || @finished.nil?
+ v < 1.0
+ end
+ end
+
+ def delete
+ super
+
+ @finished = nil
+ end
+
+ def on_finished(&block)
+ @finished = block
+ end
+ end
+
+ # an animator that runs for the specified number of seconds,
+ # and yields values between 0 and 255
+ class AlphaAnimator < ZeroToOneAnimator
+ def initialize(duration, *objects)
+ super(duration) do |v|
+ objects.each { |o| o.alpha = (255 * v).to_i }
+ end
+ end
+ end
+
+ class MoveAnimator < ZeroToOneAnimator
+ def initialize(duration, movement, *objects)
+ zipped = objects.zip(objects.map { |o| o.geometry[0..1] })
+
+ super(duration) do |v|
+ # decelerate
+ v = Math.sin(v * Math::PI / 2.0)
+
+ zipped.each do |(o, orig_pos)|
+ o.move(orig_pos.first,
+ orig_pos.last + (movement * v).to_i)
+ end
+ end
+ end
+ end
+
+ class MailboxIcon < Evas::Smart
+ attr_accessor :slot
+
+ def initialize(evas, label)
+ super(evas)
+
+ @slot = nil
+ @alpha_anim = nil
+
+ @img = Evas::Image.new(evas)
+ @label = Evas::Text.new(evas)
+
+ @objects = [@img, @label]
+
+ @img.set_color(255, 255, 255, 0)
+ @label.set_color(255, 255, 255, 0)
+ @label.set_color(255, 0, 0, 0)
+
+ @img.set_file(ICON_FILE)
+ @img.set_fill(0, 0, *@img.get_size)
+
+ @label.text = label
+ @label.set_font("VeraBd", 10)
+
+ resize(*@img.get_size)
+ end
+
+ def label
+ @label.text
+ end
+
+ # smart callbacks
+ def on_show
+ @objects.each { |o| o.show }
+
+ @alpha_anim ||= AlphaAnimator.new(2, @img, @label)
+ @alpha_anim.on_finished { @alpha_anim = nil }
+ end
+
+ def on_hide
+ @objects.each { |o| o.hide }
+
+ @alpha_anim && @alpha_anim.delete
+ @alpha_anim = nil
+ end
+
+ def on_delete
+ @objects.each { |o| o.delete }
+ @objects.clear
+
+ @alpha_anim && @alpha_anim.delete
+
+ @img = @label = @alpha_anim = nil
+ end
+
+ def on_layer_set(layer)
+ @objects.each { |o| o.layer = layer }
+ end
+
+ def on_stack_above(other)
+ @objects.each { |o| o.stack_above = other }
+ end
+
+ def on_stack_below(other)
+ @objects.each { |o| o.stack_below = other }
+ end
+
+ def on_move(x, y)
+ @objects.each { |o| o.move(x, y) }
+
+ @label.center(self)
+ end
+
+ def on_resize(w, h)
+ @img.resize(w, h)
+ end
+ end
+
+ class FixedSizeArray < Array
+ def initialize(siz)
+ super
+ end
+
+ def each
+ super { |item| yield item unless item.nil? }
+ end
+
+ def delete_at(i)
+ self[i] = nil
+ end
+
+ undef :push
+ undef :<<
+ undef :unshift
+
+ undef :pop
+ undef :shift
+ undef :delete
+ end
+
+ class Container < Evas::Smart
+ class ContainerError < StandardError; end
+ class ContainerFullError < ContainerError; end
+ class ContainerLockedError < ContainerError; end
+
+ include Enumerable
+
+ def initialize(evas)
+ super
+
+ @bg = Evas::Rectangle.new(evas)
+ @bg.set_color(0, 0, 0, 255)
+
+ @icons = FixedSizeArray.new(MAX_ICONS)
+ @about_to_add = []
+ @animators = []
+
+ @lock_count = 0
+ end
+
+ def each
+ @icons.each { |i| yield i unless i.nil? }
+ end
+
+ def <<(i)
+ Kernel.raise(ContainerFullError) if slots_left.zero?
+ Kernel.raise(ContainerLockedError) if @lock_count > 0
+
+ i.move_relative(self, 0, 0)
+ i.slot = next_slot
+ i.clip = self
+ i.show
+
+ # check whether we need to need to move this icon
+ if slots_left == 1
+ @icons[i.slot] = i
+ return
+ end
+
+ movement = Main.instance.icon_height * (slots_left - 1)
+
+ @about_to_add << i
+
+ move_time = 0.25 * slots_left
+ @animators << MoveAnimator.new(move_time, movement, i)
+
+ @animators.last.on_finished do |ani|
+ @animators.delete(ani)
+ @icons[i.slot] = i
+
+ # FIXME check whether we can always shift the array instead
+ #puts "really added #{i.label} now (slot #{i.slot})"
+ @about_to_add.delete(i)
+ end
+ end
+
+ def delete(icon)
+ # icons that are placed above the one that's deleted need
+ # to be moved
+ i = @icons.index(icon)
+ return (block_given? ? yield : nil) if i.nil?
+
+ delete_at(i)
+ end
+
+ def delete_at(i)
+ @icons[i].delete
+ @icons.delete_at(i)
+
+ ar = @icons[i..-1].reject { |i| i.nil? }
+ return if ar.nil?
+
+ @lock_count += 1
+
+ @animators << MoveAnimator.new(2, Main.instance.icon_height, *ar)
+ @animators.last.on_finished do |ani|
+ @animators.delete(ani)
+ @lock_count -= 1
+ end
+ end
+
+ def length
+ @icons.nitems
+ end
+
+ # smart callbacks
+ def on_show
+ @bg.show
+ end
+
+ def on_hide
+ @bg.hide
+ end
+
+ def on_delete
+ @bg.delete
+ @bg = nil
+ end
+
+ def on_layer_set(layer)
+ @bg.layer = layer
+ end
+
+ def on_stack_above(other)
+ @bg.stack_above = other
+ end
+
+ def on_stack_below(other)
+ @bg.stack_below = other
+ end
+
+ def on_move(x, y)
+ @bg.move(x, y)
+ end
+
+ def on_resize(w, h)
+ @bg.resize(w, h)
+ end
+
+ private
+ def slots_left
+ MAX_ICONS - @icons.nitems - @about_to_add.length
+ end
+
+ def next_slot
+ @icons.nitems + @about_to_add.length
+ end
+ end
+
+ class Main < Ecore::Evas::SoftwareX11
+ include Singleton
+
+ attr_reader :container
+
+ def initialize
+ super
+
+ self.title = "blah"
+ self.borderless = true
+
+ @icon_dim = IO.read(ICON_FILE, 8, 16).unpack("NN")
+
+ self.on_resize { @container.resize(*geometry[2, 3]) }
+
+ @container = Container.new(evas)
+ @container.move(0, 0)
+ @container.layer = -1
+ @container.show
+
+ size = [@icon_dim.first, icon_height * MAX_ICONS]
+ resize(*size)
+ set_size_min(*size)
+ set_size_max(*size)
+
+ evas.font_path_append("/usr/lib/X11/fonts/TTF")
+
+ @handlers = [
+ Ecore::EventHandler.new(IMAP::MailboxStatusEvent,
+ &method(:on_mailbox_status)),
+ Ecore::EventHandler.new(IMAP::FinishedEvent,
+ &method(:on_finished))
+ ]
+
+ @server = nil
+ @timer = Ecore::Timer.new(30, &method(:on_timer))
+ on_timer
+ end
+
+ def icon_height
+ @icon_dim.last
+ end
+
+ def run
+ Ecore.main_loop_begin
+ end
+
+ private
+ def add_icon(name)
+ @container << MailboxIcon.new(evas, name)
+ end
+
+ def on_timer
+ return unless @server.nil?
+
+ mboxes = %w{
+ INBOX
+ Lists.ba-2005
+ Lists.blackbox-devel
+ Lists.clc-devel
+ Lists.crux
+ Lists.cruxcon
+ Lists.dri-devel
+ Lists.dri-users
+ Lists.enlightenment-cvs
+ Lists.enlightenment-devel
+ Lists.hobix
+ Lists.mesa3d-dev
+ Lists.ruby-core
+ Lists.rubygems-devel
+ Lists.vim-ruby-devel
+ Lists.xmms2-devel
+ Lists.xorg
+ }
+
+ s = File.expand_path("~/.e/apps/embrace/config.yaml")
+
+ File.open(s) do |f|
+ @server = IMAP::Session.new(YAML.load(f), mboxes)
+ end
+
+ true
+ end
+
+ def on_mailbox_status(ev)
+ md = ev.name.match(/^Lists.(.+)$/)
+ if md.nil?
+ lbl = ev.name
+ else
+ lbl = md.captures.first
+ end
+
+ found = @container.find { |i| i.label == lbl }
+
+ begin
+ if ev.count.zero?
+ unless found.nil?
+ #puts "removing icon #{lbl}"
+ @container.delete(found)
+ else
+ #puts "count == 0, but icon not found (#{lbl})"
+ end
+ elsif found.nil?
+ #puts "adding icon #{lbl}"
+ add_icon(lbl)
+ else
+ #puts "count > 0, but already there (#{lbl})"
+ end
+ rescue Exception => e
+ puts e.message
+ end
+
+ false
+ end
+
+ def on_finished(ev)
+ @server = nil
+ end
+ end
+end
+
+Embrace::Main.instance.show
+Embrace::Main.instance.run
--- /dev/null
+# Copyright (c) 2006 Tilman Sauerbeck (tilman at code-monkey de)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of version 2 of the GNU General Public License as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+require "ecore"
+require "ecore_con"
+
+module Embrace; end
+
+module Embrace::IMAP
+ class IMAPError < StandardError; end
+ class LoginError < IMAPError; end
+
+ class MailboxStatusEvent < Ecore::Event
+ attr_reader :name, :count
+
+ def initialize(name, count)
+ super()
+
+ @name, @count = name, count
+ end
+ end
+
+ class FinishedEvent < Ecore::Event; end
+
+ class Session
+ attr_reader :connection
+
+ def initialize(server_info, mboxes)
+ @server_info = server_info
+
+ @connection = Ecore::Con::Server.new(2 | 16,
+ @server_info[:host],
+ @server_info[:port])
+ @buffer = ""
+
+ @tag_id = 0
+ @requests = []
+
+ @mboxes = mboxes.dup
+ @state = :disconnected
+
+ @handlers = [
+ Ecore::EventHandler.new(Ecore::Con::ServerAddEvent,
+ &method(:on_add)),
+ Ecore::EventHandler.new(Ecore::Con::ServerDataEvent,
+ &method(:on_data)),
+ Ecore::EventHandler.new(Ecore::Con::ServerDelEvent,
+ &method(:on_del))
+ ]
+ end
+
+ def next_tag
+ @tag_id = 0 if @tag_id == 0xffff
+
+ "0x%x" % [@tag_id += 1]
+ end
+
+ private
+ def login(login, password)
+ @requests << LoginRequest.new(self, login, password).send
+ end
+
+ def logout
+ @requests << LogoutRequest.new(self).send
+ end
+
+ def query_status(mailbox)
+ @requests << StatusRequest.new(self, mailbox).send
+ end
+
+ def on_add(ev)
+ return true unless ev.server == @connection
+
+ @state = :connected
+
+ false
+ end
+
+ def on_data(ev)
+ return true unless ev.server == @connection
+
+ lines = (@buffer + ev.data).split(/\r\n/, -1)
+ @buffer = lines.pop
+
+ lines.each { |line| handle_line(line.strip) }
+
+ false
+ end
+
+ def on_del(ev)
+ return true unless ev.server == @connection
+
+ @handlers.each { |h| h.delete }
+
+ @state = :disconnected
+ @connection.delete
+
+ FinishedEvent.raise
+
+ false
+ end
+
+ def handle_line(line)
+if $DEBUG
+ puts("<- " + line)
+end
+ handle_response(Response.deserialize(line))
+
+ if @state == :connected
+ login(@server_info[:login], @server_info[:password])
+ end
+ end
+
+ def handle_response(resp)
+ unless resp.tagged?
+ handle_untagged_response(resp)
+ else
+ req = @requests.find { |r| r.tag == resp.tag }
+ handle_tagged_response(resp, req)
+ @requests.delete(req)
+ end
+ end
+
+ def handle_tagged_response(resp, req)
+ return unless req.is_a?(LoginRequest)
+
+ case resp.status
+ when :ok
+ @state = :logged_in
+
+ @mboxes.each { |mb| query_status(mb) }
+ else
+ raise(LoginError, "cannot login - #{resp.data}")
+ end
+ end
+
+ def handle_untagged_response(resp)
+ return if resp.key != "STATUS"
+
+ md = resp.data.match(/\"(.+)\" \(UNSEEN (\d+)\)/)
+ if md.nil?
+ raise(IMAPError, "invalid status response - #{resp.data}")
+ end
+
+ name, count = md.captures.first, md.captures.last.to_i
+
+ MailboxStatusEvent.raise(name, count)
+ @mboxes.delete(name)
+
+ if @mboxes.empty?
+ @status = :done
+ logout
+ end
+ end
+ end
+
+ class Request
+ attr_reader :tag
+
+ def initialize(session)
+ @session = session
+ @tag = session.next_tag
+ end
+
+ def send
+if $DEBUG
+ puts("-> #{@tag} #{serialize}")
+end
+ @session.connection << "#{@tag} #{serialize}\r\n"
+
+ self
+ end
+ end
+
+ class LoginRequest < Request
+ def initialize(session, login, password)
+ super(session)
+
+ @login = login
+ @password = password
+ end
+
+ def serialize
+ "LOGIN #{@login} #{@password}"
+ end
+ end
+
+ class LogoutRequest < Request
+ def initialize(session)
+ super
+ end
+
+ def serialize
+ "LOGOUT"
+ end
+ end
+
+ class StatusRequest < Request
+ def initialize(session, mailbox)
+ super(session)
+
+ @mailbox = mailbox
+ end
+
+ def serialize
+ "STATUS #{@mailbox} (UNSEEN)"
+ end
+ end
+
+ class Response
+ class ResponseError < IMAPError; end
+
+ attr_reader :data
+
+ def Response.deserialize(line)
+ case line
+ when /^\* (\w+) (.*)$/
+ UntaggedResponse.new($1, $2)
+ when /^(0x[[:xdigit:]]+) (OK|NO|BAD) /
+ tag = $1
+ rest = line[(tag.length + 1)..-1]
+ status = $2.downcase.to_sym
+
+ TaggedResponse.new(tag, status, rest)
+ else
+ raise(ResponseError, "cannot parse - #{line}")
+ end
+ end
+
+ def initialize(data)
+ @data = data.to_str.freeze
+ end
+
+ def tagged?
+ false
+ end
+ end
+
+ class TaggedResponse < Response
+ attr_reader :tag, :status
+
+ def initialize(tag, status, data)
+ super(data)
+
+ @tag = tag
+ @status = status
+ end
+
+ def tagged?
+ true
+ end
+ end
+
+ class UntaggedResponse < Response
+ attr_reader :key
+
+ def initialize(key, data)
+ super(data)
+
+ @key = key
+ end
+ end
+end