From: Tilman Sauerbeck Date: Sun, 9 Apr 2006 18:45:37 +0000 (+0200) Subject: initial import X-Git-Tag: embrace-0.1.0~32 X-Git-Url: http://git.code-monkey.de/?p=embrace.git;a=commitdiff_plain;h=76d5e549fcb9c0b316f81eb4a2a496d84f0313c7 initial import --- 76d5e549fcb9c0b316f81eb4a2a496d84f0313c7 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 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 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