From 76d5e549fcb9c0b316f81eb4a2a496d84f0313c7 Mon Sep 17 00:00:00 2001 From: Tilman Sauerbeck Date: Sun, 9 Apr 2006 20:45:37 +0200 Subject: [PATCH] initial import --- AUTHORS | 1 + COPYING | 20 ++ Rakefile | 40 ++++ bin/embrace | 458 +++++++++++++++++++++++++++++++++++++ data/l33t_MAI_envelope.png | Bin 0 -> 24575 bytes lib/embrace/imap.rb | 275 ++++++++++++++++++++++ 6 files changed, 794 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 Rakefile create mode 100755 bin/embrace create mode 100644 data/l33t_MAI_envelope.png create mode 100644 lib/embrace/imap.rb diff --git a/AUTHORS b/AUTHORS new file mode 100644 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 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 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 index 0000000..f9d00c0 --- /dev/null +++ b/bin/embrace @@ -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 index 0000000000000000000000000000000000000000..1e2b2339db3cb07071fba81f5dc3973ed8bd89af GIT binary patch literal 24575 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 AApigX literal 0 HcmV?d00001 diff --git a/lib/embrace/imap.rb b/lib/embrace/imap.rb new file mode 100644 index 0000000..2fc3af4 --- /dev/null +++ b/lib/embrace/imap.rb @@ -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 -- 2.30.2