initial import
authorTilman Sauerbeck <tilman@code-monkey.de>
Sun, 9 Apr 2006 18:45:37 +0000 (20:45 +0200)
committerTilman Sauerbeck <tilman@code-monkey.de>
Sun, 9 Apr 2006 18:45:37 +0000 (20:45 +0200)
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
Rakefile [new file with mode: 0644]
bin/embrace [new file with mode: 0755]
data/l33t_MAI_envelope.png [new file with mode: 0644]
lib/embrace/imap.rb [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..5864b3d
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Tilman Sauerbeck (tilman at code-monkey de)
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..77ff64a
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,20 @@
+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.
diff --git a/Rakefile b/Rakefile
new file mode 100644 (file)
index 0000000..5188ba5
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,40 @@
+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
diff --git a/bin/embrace b/bin/embrace
new file mode 100755 (executable)
index 0000000..f9d00c0
--- /dev/null
@@ -0,0 +1,458 @@
+#!/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
diff --git a/data/l33t_MAI_envelope.png b/data/l33t_MAI_envelope.png
new file mode 100644 (file)
index 0000000..1e2b233
Binary files /dev/null and b/data/l33t_MAI_envelope.png differ
diff --git a/lib/embrace/imap.rb b/lib/embrace/imap.rb
new file mode 100644 (file)
index 0000000..2fc3af4
--- /dev/null
@@ -0,0 +1,275 @@
+# 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