class Object

Constants

BIRD_CACHE_PATH
BIRD_URL
BOOT_TIME
CACHE_DIR
DEFAULT_SOUND_DIRECTORY
Deferred
Event
EventFilter
EventListener
FOLLOW_DIR
HYDE

基本的な単位であり、数学的にも重要なマジックナンバーで、至るところで使われる。 これが言語仕様に含まれていないRubyは正直気が狂っていると思う。 ja.uncyclopedia.info/wiki/Hyde

Message

begin

Message

投稿1つを表すクラス。

end

Plugin
Post

begin rdoc

Twitter APIとmikutterプラグインのインターフェイス

end

RUBY_VERSION_ARRAY

Rubyのバージョンを配列で。あると便利。

Retriever
SerialThread

SerialThreadGroup のインスタンス。 同時実行数は1固定

Sound
TABPOS
User
UserList

Public Class Methods

boot() click to toggle source
# File core/plugin/gtk/delayer.rb, line 7
def self.boot
  Gtk.idle_add_priority(GLib::PRIORITY_LOW) {
    Delayer.run
    false }
end
define_periodical_executer(api, interval, count, &success) click to toggle source
# File core/plugin/rest/rest.rb, line 5
def self.define_periodical_executer(api, interval, count, &success)
  counter = UserConfig[interval]
  lambda{ |service|
    counter += 1
    if counter >= UserConfig[interval]
      counter = 0
      service.call_api(api, count: UserConfig[count]){ |messages|
        success.call(service, messages) if messages and not messages.empty? } end } end
defnotify(label, kind) click to toggle source
# File core/plugin/notify/notify.rb, line 7
def self.defnotify(label, kind)
  settings (label) do
    boolean _('ポップアップ'), "notify_#{kind}".to_sym
    fileselect(_('サウンド'), "notify_sound_#{kind}".to_sym,
               dir: DEFAULT_SOUND_DIRECTORY,
               shortcuts: [DEFAULT_SOUND_DIRECTORY],
               filters: {_('非圧縮音声ファイル (*.wav, *.aiff)') => ['wav', 'WAV', 'aiff', 'AIFF'],
                         _('FLAC (*.flac, *.fla)') => ['flac', 'FLAC', 'fla', 'FLA'],
                         _('MPEG-1/2 Audio Layer-3 (*.mp3)') => ['mp3', 'MP3'],
                         _('Ogg (*.ogg)') => ['ogg', 'OGG'],
                         _('全てのファイル') => ['*']
                        }) end end
notify(user, text) click to toggle source
# File core/plugin/notify/notify.rb, line 89
def self.notify(user, text)
  Plugin.call(:popup_notify, user, text) end
notify_sound(sndfile) click to toggle source
# File core/plugin/notify/notify.rb, line 92
def self.notify_sound(sndfile)
  if sndfile.respond_to?(:to_s) and FileTest.exist?(sndfile.to_s)
    Plugin.call(:play_sound, sndfile) end end

Public Instance Methods

__write_stderr(msg) click to toggle source
# File core/utils.rb, line 246
def __write_stderr (msg)
  $stderr.write(msg.gsub(FOLLOW_DIR, '{MIKUTTER_DIR}')+"\n")
end
_loop() click to toggle source
# File core/plugin/image_file_cache/image_file_cache.rb, line 78
def _loop
  Reserver.new(60, thread: SerialThread) do
    if @queue
      @queue.run
      _loop  end end end
absent_tab() click to toggle source
# File core/plugin/home_timeline/home_timeline.rb, line 34
def absent_tab
  if @tag
    tab(:home_timeline).destroy
    detach(@tag)
    @tag = nil
  end
end
active_datasources() click to toggle source

使用されているデータソースのSetを返す

# File core/plugin/extract/extract.rb, line 280
def active_datasources
  @active_datasources ||=
    extract_tabs.values.map{|tab|
      tab.sources
    }.inject(Set.new, &:merge).freeze end
add_intent_rule(intent:, str:, rule:, model_slug:) click to toggle source
# File core/plugin/intent_selector/intent_selector.rb, line 106
def add_intent_rule(intent:, str:, rule:, model_slug:)
  unless UserConfig[:intent_selector_rules].any?{|r| r[:intent].to_sym == intent.slug && r[:str] == str && r[:rule] == rule }
    UserConfig[:intent_selector_rules] += [{uuid: SecureRandom.uuid, intent: intent.slug, model: model_slug, str: str, rule: rule}]
  end
end
add_muted_user(user) click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 161
def add_muted_user(user)
  type_strict user => User
  atomic{
    muted = (UserConfig[:muted_users] ||= []).melt
    muted << user.idname
    UserConfig[:muted_users] = muted } end

def remove_muted_user(user)
  type_strict user => User
  atomic{
    muted = (UserConfig[:muted_users] ||= []).melt
    muted.delete(user.idname)
    UserConfig[:muted_users] = muted } end

# フォロー関係を表示する
# ==== Args
# [user] 対象となるユーザ
# ==== Return
# リレーションバーのウィジェット(Gtk::VBox)
def relation_bar(user)
  icon_size = Gdk::Rectangle.new(0, 0, 32, 32)
  arrow_size = Gdk::Rectangle.new(0, 0, 16, 16)
  container = ::Gtk::VBox.new(false, 4)
  Enumerator.new{|y|
    Plugin.filtering(:worlds, y)
  }.select{|world|
    world.class.slug == :twitter
  }.each{ |me|
    following = followed = nil
    w_following_label = ::Gtk::Label.new(_("関係を取得中"))
    w_followed_label = ::Gtk::Label.new("")
    w_eventbox_image_following = ::Gtk::EventBox.new
    w_eventbox_image_followed = ::Gtk::EventBox.new
    relation = if me.user_obj == user
                 ::Gtk::Label.new(_("それはあなたです!"))
               else
                 ::Gtk::HBox.new.
                   closeup(w_eventbox_image_following).
                   closeup(w_following_label) end
    relation_container = ::Gtk::HBox.new(false, icon_size.width/2)
    relation_container.closeup(::Gtk::WebIcon.new(me.user_obj.icon, icon_size).tooltip("#{me.user_obj.idname}(#{me.user_obj[:name]})"))
    relation_container.closeup(::Gtk::VBox.new.
                               closeup(relation).
                               closeup(::Gtk::HBox.new.
                                       closeup(w_eventbox_image_followed).
                                       closeup(w_followed_label)))
    relation_container.closeup(::Gtk::WebIcon.new(user.icon, icon_size).tooltip("#{user.idname}(#{user[:name]})"))
    if me.user_obj != user
      followbutton = ::Gtk::Button.new
      followbutton.sensitive = false
      # フォローしている状態の更新
      m_following_refresh = lambda { |new|
        if not w_eventbox_image_following.destroyed?
          following = new
          if not w_eventbox_image_following.children.empty?
            w_eventbox_image_following.remove(w_eventbox_image_following.children.first) end

          w_eventbox_image_following.style = w_eventbox_image_following.parent.style
          w_eventbox_image_following.add(::Gtk::WebIcon.new(Skin[new ? 'arrow_following.png' : 'arrow_notfollowing.png'], arrow_size).show_all)
          w_following_label.text = new ? _("フョローしている") : _("フョローしていない")
          followbutton.label = new ? _("解除") : _("フョロー") end }
      # フォローされている状態の更新
      m_followed_refresh = lambda { |new|
        if not w_eventbox_image_followed.destroyed?
          followed = new
          if not w_eventbox_image_followed.children.empty?
            w_eventbox_image_followed.remove(w_eventbox_image_followed.children.first) end
          w_eventbox_image_followed.style = w_eventbox_image_followed.parent.style
          w_eventbox_image_followed.add(::Gtk::WebIcon.new(Skin.get_path(new ? "arrow_followed.png" : "arrow_notfollowed.png"), arrow_size).show_all)
          w_followed_label.text = new ? _("フョローされている") : _("フョローされていない") end }
      me.friendship(target_id: user[:id], source_id: me.user_obj[:id]).next{ |rel|
        if rel and not(w_eventbox_image_following.destroyed?)
          m_following_refresh.call(rel[:following])
          m_followed_refresh.call(rel[:followed_by])
          handler_followings_created = on_followings_created do |service, dst_users|
            if service == me and dst_users.include?(user)
              m_following_refresh.call(true) end end
          handler_followings_destroy = on_followings_destroy do |service, dst_users|
            if service == me and dst_users.include?(user)
              m_following_refresh.call(false) end end
          followbutton.ssc(:clicked){
            followbutton.sensitive = false
            event = following ? :followings_destroy : :followings_created
            me.__send__(following ? :unfollow : :follow, user_id: user.id).next{ |msg|
              Plugin.call(event, me, [user])
              followbutton.sensitive = true unless followbutton.destroyed? }.
            terminate.trap{
              followbutton.sensitive = true unless followbutton.destroyed? }
            true }
          followbutton.signal_connect(:destroy){
            detach(:followings_created, handler_followings_created)
            detach(:followings_destroy, handler_followings_destroy)
            false }
          followbutton.sensitive = true
          relation_container.closeup(followbutton) end
      }.terminate.trap{
        w_following_label.text = _("取得できませんでした") } end
    container.closeup(relation_container) }
  container end

# ユーザのプロフィールのヘッダ部を返す
# ==== Args
# [user] 表示するUser
# [intent_token] ユーザを開くときに利用するIntent
# ==== Return
# ヘッダ部を表すGtkコンテナ
def profile_head(user, intent_token)
  eventbox = ::Gtk::EventBox.new
  eventbox.ssc('visibility-notify-event'){
    eventbox.style = background_color
    false }

  icon = ::Gtk::EventBox.new.add(::Gtk::WebIcon.new(user.icon_large, UserConfig[:profile_icon_size], UserConfig[:profile_icon_size]).tooltip(_('アイコンを開く')))
  icon.ssc(:button_press_event) do |this, event|
    Plugin.call(:open, user.icon_large)
    true end
  icon.ssc(:realize) do |this|
    this.window.set_cursor(Gdk::Cursor.new(Gdk::Cursor::HAND2))
    false end

  icon_alignment = Gtk::Alignment.new(0.5, 0, 0, 0)
                   .set_padding(*[UserConfig[:profile_icon_margin]]*4)

  eventbox.add(::Gtk::VBox.new(false, 0).
                add(::Gtk::HBox.new.
                     closeup(icon_alignment.add(icon)).
                     add(::Gtk::VBox.new.closeup(user_name(user, intent_token)).closeup(profile_table(user)))))
end

# ユーザ名を表示する
# ==== Args
# [user] 表示するUser
# [intent_token] ユーザを開くときに利用するIntent
# ==== Return
# ユーザの名前の部分のGtkコンテナ
def user_name(user, intent_token)
  w_name = ::Gtk::TextView.new
  w_name.editable = false
  w_name.cursor_visible = false
  w_name.wrap_mode = Gtk::TextTag::WRAP_CHAR
  w_name.ssc(:event) do |this, event|
    if event.is_a? ::Gdk::EventMotion
      this.get_window(::Gtk::TextView::WINDOW_TEXT)
        .set_cursor(::Gdk::Cursor.new(::Gdk::Cursor::XTERM)) end
    false end
  if Gtk::BINDING_VERSION >= [3,1,2]
    tag_sn = w_name.buffer.create_tag('sn', {foreground: '#0000ff',
                                             weight: Pango::Weight::BOLD,
                                             underline: Pango::Underline::SINGLE})
  else
    tag_sn = w_name.buffer.create_tag('sn', {foreground: '#0000ff',
                                             weight: Pango::FontDescription::WEIGHT_BOLD,
                                             underline: Pango::AttrUnderline::SINGLE})
  end
  tag_sn.ssc(:event, &user_screen_name_event_callback(user, intent_token))

  w_name.buffer.insert(w_name.buffer.start_iter, user[:idname], tag_sn)
  w_name.buffer.insert(w_name.buffer.end_iter, "\n#{user[:name]}")
  Gtk::VBox.new.add(w_name) end

# プロフィールの上のところの格子になってる奴をかえす
# ==== Args
# [user] 表示するUser
# ==== Return
# プロフィールのステータス部を表すGtkコンテナ
def profile_table(user)
  _, profile_columns = Plugin.filtering(:user_detail_view_header_columns, user, [
                                          ['tweets',     user[:statuses_count].to_s],
                                          ['favs',       user[:favourites_count].to_s],
                                          ['followings', user[:friends_count]],
                                          ['followers',  user[:followers_count]],
                                        ])
  ::Gtk::Table.new(2, profile_columns.size).tap{|table|
    profile_columns.each_with_index do |column, index|
      key, value = column
      table.
        attach(::Gtk::Label.new(value.to_s).right, 0, 1, index, index+1).
        attach(::Gtk::Label.new(key.to_s)  .left , 1, 2, index, index+1)
    end
  }.set_row_spacing(0, 4).
    set_row_spacing(1, 4).
    set_column_spacing(0, 16)
end

def background_color
  style = ::Gtk::Style.new()
  style.set_bg(::Gtk::STATE_NORMAL, 0xFF ** 2, 0xFF ** 2, 0xFF ** 2)
  style end

def user_screen_name_event_callback(user, intent_token)
  lambda do |tag, textview, event, iter|
    case event
    when ::Gdk::EventButton
      if event.event_type == ::Gdk::Event::BUTTON_RELEASE and event.button == 1
        if intent_token.respond_to?(:forward)
          intent_token.forward
        else
          Plugin.call(:open, user.uri)
        end
        next true
      end
    when ::Gdk::EventMotion
      textview
        .get_window(::Gtk::TextView::WINDOW_TEXT)
        .set_cursor(::Gdk::Cursor.new(::Gdk::Cursor::HAND2))
    end
    false
  end
end
add_tab(saved_search) click to toggle source

タブを保存する

Args

saved_search

saved search

# File core/plugin/saved_search/saved_search.rb, line 49
def add_tab(saved_search)
  type_strict saved_search => Plugin::SavedSearch::SavedSearch
  tab(saved_search.slug, saved_search.name) do
    set_icon Skin['savedsearch.png']
    timeline saved_search.slug end
  register_cache(saved_search)
  timelines[saved_search.id] = saved_search end
addsupport(cond, element_rule = {}, &block) click to toggle source
# File core/plugin/openimg/openimg.rb, line 80
def addsupport(cond, element_rule = {}, &block); end
append_message(source, messages) click to toggle source
# File core/plugin/extract/extract.rb, line 354
def append_message(source, messages)
  type_strict source => Symbol, messages => Enumerable
  tabs = extract_tabs.values.select{ |r| r.sources && r.using?(source) }
  return if tabs.empty?
  converted_messages = messages.map{ |message| message.retweet_source ? message.retweet_source : message }
  tabs.each{ |record|
      filtered_messages = timeline(record.slug).not_in_message(converted_messages.select(&compile(record.id, record.sexp)))
      timeline(record.slug) << filtered_messages
      notificate_messages = filtered_messages.lazy.select{|message| message[:created] > defined_time}
      if record.popup?
        notificate_messages.deach do |message|
          Plugin.call(:popup_notify, message.user, message.description) end end
      if record.sound.is_a?(String) and notificate_messages.first and FileTest.exist?(record.sound)
        Plugin.call(:play_sound, record.sound) end
  } end
atomic() { || ... } click to toggle source

共通のMutexで処理を保護して実行する。 atomicブロックで囲まれたコードは、別々のスレッドで同時に実行されない。

# File core/utils.rb, line 276
def atomic
  $atomic.synchronize{ yield }
end
available_lists(service = nil) click to toggle source

自分がフォローしているリストを返す。 service を指定すると、そのアカウントでフォローしているリストに結果を限定する。 結果は重複する可能性がある

Args

service

Service|nil リストのフォロイーで絞り込む場合、そのService

Return

Enumerable 自分がフォローしているリスト(Plugin::Twitter::UserList)を列挙する

# File core/plugin/list/list.rb, line 149
def available_lists(service = nil)
  @available_lists ||= Hash.new
  if service
    @available_lists[service.user_obj] ||= [].freeze
  else
    @all_available_lists ||= @available_lists.flat_map{|k,v| v}.uniq.compact.freeze end end
background_color() click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 345
def background_color
  style = ::Gtk::Style.new()
  style.set_bg(::Gtk::STATE_NORMAL, 0xFF ** 2, 0xFF ** 2, 0xFF ** 2)
  style end
backtrace() click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 103
def backtrace
  "#{crashed_exception.class} #{crashed_exception.to_s}\n" +
    crashed_exception.backtrace.map{ |msg| msg.gsub(FOLLOW_DIR, '{MIKUTTER_DIR}') }.join("\n")
end
bg_system(*args) click to toggle source

コマンドをバックグラウンドで起動することを覗いては system() と同じ

# File core/utils.rb, line 281
def bg_system(*args)
  Process.detach(spawn(*args))
end
bool(val) click to toggle source

値が真であるならtrueを返す。遅延評価オブジェクトでも正確に判断することができる。

# File core/lib/lazy.rb, line 101
def bool(val)
  not(val.nil? or val.is_a?(FalseClass)) end
boot() click to toggle source
# File core/plugin/followingcontrol/followingcontrol.rb, line 132
def boot
  @activating_services = Set.new
  @relation = Struct.new(:followings, :followers).new(TimeLimitedStorage.new, TimeLimitedStorage.new)

  Enumerator.new{|y|
    Plugin.filtering(:worlds, y)
  }.select{|world|
    world.class.slug == :twitter
  }.each(&method(:service_register))
end
boot!(profile) click to toggle source

イベントの待受を開始する。 profile がtrueなら、プロファイリングした結果を一時ディレクトリに保存する

# File mikutter.rb, line 48
def boot!(profile)
  begin
    Mainloop.before_mainloop
    if profile
      require 'ruby-prof'
      begin
        notice 'start profiling'
        RubyProf.start
        Mainloop.mainloop
      ensure
        result = RubyProf.stop
        printer = RubyProf::CallTreePrinter.new(result)
        path = File.join(Environment::TMPDIR, 'profile', Time.new.strftime('%Y-%m-%d-%H%M%S'))
        FileUtils.mkdir_p(path)
        notice "profile: writing to #{path}"
        printer.print(path: path)
        notice "profile: done."
      end
    else
      Mainloop.mainloop end
  rescue => exception
    into_debug_mode(exception)
    notice "catch exception `#{exception.class}'"
    raise
  rescue Exception => exception
    notice "catch exception `#{exception.class}'"
    exception = Mainloop.exception_filter(exception)
    notice "=> `#{exception.class}'"
    raise end
  exception = Mainloop.exception_filter(nil)
  if exception
    notice "raise mainloop exception `#{exception.class}'"
    raise exception end
  notice "boot! exited normally." end
cache_expire() click to toggle source

キャッシュの有効期限を秒単位で返す

# File core/plugin/image_file_cache/image_file_cache.rb, line 38
def cache_expire
  (UserConfig[:image_file_cache_expire] || 32) * 24 * 60 * 60 end
cache_it(photo) click to toggle source
# File core/plugin/image_file_cache/image_file_cache.rb, line 41
def cache_it(photo)
  unless @db.key?(photo.uri.to_s)
    notice "cache added #{photo.uri}"
    photo.download.next{|downloaded|
      @db[downloaded.uri.to_s] = downloaded.blob
    }
  end
end
caller_util() click to toggle source

utils.rbのメソッドを呼び出した最初のバックトレースを返す

# File core/utils.rb, line 218
def caller_util
  caller.each{ |result|
    return result unless /utils\.rb/ === result } end
check_dirs() click to toggle source
# File core/plugin/image_file_cache/image_file_cache.rb, line 65
def check_dirs
  @queue.new(:check_dirs) do
    Dir.foreach(@cache_directory)
      .select{|x| x =~ %r<\A[a-fA-F0-9]{2}\Z> }
      .shuffle
      .each{|subdir|
      check_subdirs(File.join(@cache_directory, subdir))
    }
    Reserver.new(cache_expire, thread: SerialThread) do
      check_dirs end
  end
end
check_subdirs(dir) click to toggle source
# File core/plugin/image_file_cache/image_file_cache.rb, line 50
  def check_subdirs(dir)
    @queue.new(:check_subdirs) do
      Dir.foreach(dir)
        .map{|x| File.join(dir, x) }
        .select{|x| FileTest.file?(x) }
        .each{|x|
        Reserver.new((File.atime(x) rescue File.mtime(x)) + cache_expire, thread: SerialThread) do
          notice "cache deleted #{x}"
          File.delete(x) if FileTest.file?(x)
          if Dir.foreach(dir).select{|y| File.file? File.join(dir, y) }.empty?
            Dir.delete(dir) rescue nil end end
      }
    end
  end

  def check_dirs
    @queue.new(:check_dirs) do
      Dir.foreach(@cache_directory)
        .select{|x| x =~ %r<\A[a-fA-F0-9]{2}\Z> }
        .shuffle
        .each{|subdir|
        check_subdirs(File.join(@cache_directory, subdir))
      }
      Reserver.new(cache_expire, thread: SerialThread) do
        check_dirs end
    end
  end

  def _loop
    Reserver.new(60, thread: SerialThread) do
      if @queue
        @queue.run
        _loop  end end end

  on_unload do
    @db.close
    @db = @queue = nil
  end

  check_dirs
  _loop

end
chi_fatal_alert(msg) click to toggle source

環境や設定の不備で終了する。msgには、何が原因かを文字列で渡す。このメソッドは 処理を返さずにアボートする。

# File core/utils.rb, line 252
def chi_fatal_alert(msg)
  require_if_exist 'gtk2'
  if defined?(Gtk::MessageDialog)
    dialog = Gtk::MessageDialog.new(nil,
                                    Gtk::Dialog::DESTROY_WITH_PARENT,
                                    Gtk::MessageDialog::ERROR,
                                    Gtk::MessageDialog::BUTTONS_CLOSE,
                                    "#{Environment::NAME} エラー")
    dialog.secondary_text = msg.to_s
    dialog.run
    dialog.destroy end
  puts msg.to_s
  abort end
command_exist?(cmd) click to toggle source

UNIXコマンド cmd が存在するか否かを返す。

# File core/utils.rb, line 82
def command_exist?(cmd)
  system("which #{cmd} > /dev/null")
end
compile(tab_id, code) click to toggle source
# File core/plugin/extract/extract.rb, line 286
def compile(tab_id, code)
  atomic do
    @compiled ||= {}
    @compiled[tab_id] ||=
      if code.empty?
        ret_nth
      else
        begin
          before = Set.new
          extract_condition ||= Hash[Plugin.filtering(:extract_condition, []).first.map{ |condition| [condition.slug, condition] }]
          evaluated = MIKU::Primitive.new(:to_ruby_ne).call(MIKU::SymbolTable.new, metamorphose(code: code, assign: before, extract_condition: extract_condition))
          code_string = "lambda{ |message|\n" + before.to_a.join("\n") + "\n  " + evaluated + "\n}"
          instance_eval(code_string)
        rescue Plugin::Extract::ConditionNotFoundError => exception
          Plugin.call(:modify_activity,
                      plugin: self,
                      kind: 'error'.freeze,
                      title: _("抽出タブ条件エラー"),
                      date: Time.new,
                      description: _("抽出タブ「%{tab_name}」で使われている条件が見つかりませんでした:\n%{error_string}") % {tab_name: extract_tabs[tab_id].name, error_string: exception.to_s})
          warn exception
          ret_nth end end end end
confroot(*path) click to toggle source

Environment::CONFROOT内のファイル名を得る。

confroot(*path)

File::expand_path(File.join(Environment::CONFROOT, *path))

と等価。

# File core/utils.rb, line 38
def confroot(*path)
  File::expand_path(File.join(Environment::CONFROOT, *path))
end
crashed_exception() click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 108
def crashed_exception
  @crashed_exception ||= object_get_contents(File.expand_path(File.join(Environment::TMPDIR, 'crashed_exception'))) rescue nil
end
create_pane(i_pane) click to toggle source

ペインを作成

Args

i_pane

ペイン

Return

ペイン(Gtk::Notebook)

# File core/plugin/gtk/gtk.rb, line 553
def create_pane(i_pane)
  pane = ::Gtk::Notebook.new
  @slug_dictionary.add(i_pane, pane)
  pane.ssc('key_press_event'){ |widget, event|
    Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_pane) }
  pane.ssc(:destroy){
    i_pane.destroy if i_pane.destroyed?
    false }
  pane.show_all end
create_tab(i_tab) click to toggle source

タブを作成する

Args

i_tab

タブ

Return

Tab(Gtk::EventBox)

# File core/plugin/gtk/gtk.rb, line 142
def create_tab(i_tab)
  tab = ::Gtk::EventBox.new.tooltip(i_tab.name)
  @slug_dictionary.add(i_tab, tab)
  tab_update_icon(i_tab)
  tab.ssc(:focus_in_event) {
    i_tab.active!(true, true)
    false
  }
  tab.ssc(:key_press_event){ |widget, event|
    Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_tab) }
  tab.ssc(:button_press_event) { |this, event|
    if event.button == 3
      Plugin::GUI::Command.menu_pop(i_tab)
    else
      Plugin::GUI.keypress(::Gtk::buttonname([event.event_type, event.button, event.state]), i_tab)
    end
    false }
  tab.ssc(:destroy) {
    i_tab.destroy
    false }
  tab.show_all end
current_world() click to toggle source

現在選択されているアカウントを返す

Return

Diva::Model

カレントアカウント

# File core/plugin/current_world/current_world.rb, line 27
def current_world
  @current ||= Enumerator.new{|y| Plugin.filtering(:worlds, y) }.first
end
current_world=(new) click to toggle source

カレントアカウントを new に変更する。

Args

new

新たなカレントアカウント(Diva::Model)。 worlds が返す内容のうちのいずれかでなければならない。

Return

Diva::Model

新たなカレントアカウント

Raise

Plugin::World::InvalidWorldError

worlds にないアカウントが渡された場合

# File core/plugin/current_world/current_world.rb, line 40
def current_world=(new)
  raise RuntimeError unless Enumerator.new{|y| Plugin.filtering(:worlds, y) }.include?(new)
  @current = new
end
datasource_slug(list) click to toggle source
# File core/plugin/list/list.rb, line 20
def datasource_slug(list)
  :"#{list.user.idname}_list_#{list[:id]}" end
datasources() click to toggle source

このプラグインが提供するデータソースを返す

Return

Hash データソース

# File core/plugin/quoted_message/quoted_message.rb, line 6
def datasources
  ds = {nested_quoted_myself: _("ナウい引用(全てのアカウント)".freeze)}
  Enumerator.new{|yielder|
    Plugin.filtering(:worlds, yielder)
  }.lazy.select{|world|
    world.class.slug == :twitter
  }.each do |twitter|
    ds["nested_quote_quotedby_#{twitter.user_obj.id}".to_sym] = "@#{twitter.user_obj.idname}/" + _('ナウい引用'.freeze)
  end
  ds end
debugging_wait() click to toggle source

他のスレッドでinto_debug_modeが呼ばれているなら、それが終わるまでカレントスレッドをスリープさせる

# File core/utils.rb, line 148
def debugging_wait
  if $into_debug_mode
    $into_debug_mode << Thread.current
    Thread.stop end end
defsequence(name, &content) click to toggle source
# File core/plugin/guide/guide.rb, line 11
def defsequence(name, &content)
  @sequence[name] = content end
delete_cache(id) click to toggle source

保存した検索の情報をキャッシュから削除

Args

id

削除するID

# File core/plugin/saved_search/saved_search.rb, line 124
def delete_cache(id)
  cache = at(:last_saved_search_state, {}).melt
  cache.delete(id)
  store(:last_saved_search_state, cache) end
delete_tab(id) click to toggle source

idに対応するタブを削除

Args

id

saved search の ID

# File core/plugin/saved_search/saved_search.rb, line 60
def delete_tab(id)
  type_strict id => Integer
  saved_search = timelines[id]
  timelines.delete(id)
  tab(saved_search.slug).destroy if saved_search.slug end
delete_world_with_confirm(worlds) click to toggle source
# File core/plugin/change_account/change_account.rb, line 92
def delete_world_with_confirm(worlds)
  dialog(_("アカウントの削除")){
    label _("以下のアカウントを本当に削除しますか?\n一度削除するともう戻ってこないよ")
    worlds.each{ |world|
      link world
    }
  }.next{
    worlds.each{ |world|
      Plugin.call(:world_destroy, world)
    }
  }
end
destroy_compile_cache() click to toggle source
# File core/plugin/extract/extract.rb, line 350
def destroy_compile_cache
  atomic do
    @compiled = {} end end
destroy_world(target) click to toggle source
# File core/plugin/world/world.rb, line 82
def destroy_world(target)
  Plugin::World::Keep.account_destroy target.slug
  @worlds = nil
  Plugin.call(:service_destroyed, target) # 互換性のため
end
divide_intents(intents, uri, model_slug) click to toggle source

intent の配列を受け取り、ユーザが過去に入力したルールに基づき、 recommendedとsuggestedに分ける

Args

intents

スキャン対象のintent

uri

リソースのURI

model_slug

絞り込みに使うModelのslug。

Return

条件に対して推奨されるintentの配列と、intentsに指定されたそれ以外の値の配列

# File core/plugin/intent_selector/intent_selector.rb, line 120
def divide_intents(intents, uri, model_slug)
  intent_slugs = UserConfig[:intent_selector_rules].select{|record|
    ((record[:model] == nil) || (model_slug == record[:model].to_s)) && uri.to_s.start_with?(record[:str])
  }.map{|record|
    record[:intent].to_sym
  }
  intents.partition{|intent| intent_slugs.include?(intent.slug.to_sym) }
end
enable_sequence() click to toggle source
# File core/plugin/aspectframe/aspectframe.rb, line 95
def enable_sequence
  Plugin::AspectFrame::SCHEDULE.lazy.select{|s| s.range.cover? now} end
enable_sequence?(seq) click to toggle source
# File core/plugin/aspectframe/aspectframe.rb, line 98
def enable_sequence?(seq)
  enable_sequence.map(&:slug).include? seq end
encode_parameters(params, delimiter = '&', quote = nil) click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 112
def encode_parameters(params, delimiter = '&', quote = nil)
  if params.is_a?(Hash)
    params = params.map do |key, value|
      "#{escape(key)}=#{quote}#{escape(value)}#{quote}"
    end
  else
    params = params.map { |value| escape(value) }
  end
  delimiter ? params.join(delimiter) : params
end
entities_to_notes(entities) { |media| ... } click to toggle source
# File core/plugin/twitter/twitter.rb, line 503
def entities_to_notes(entities)
  entities.map do |media|
    [ Range.new(*media[:indices], false),
      yield(media) ]
  end
end
entitiy_users(tweet, user_entities) click to toggle source
# File core/plugin/twitter/twitter.rb, line 466
def entitiy_users(tweet, user_entities)
  entities_to_notes(user_entities) do |user_entity|
    user = Plugin::Twitter::User.findbyid(user_entity[:id], Diva::DataSource::USE_LOCAL_ONLY)
    if user
      Diva::Model(:score_hyperlink).new(
        description: "@#{user.idname}",
        uri: user.uri,
        reference: user)
    else
      screen_name = user_entity[:screen_name] || tweet.description[Range.new(*user_entity[:indices])]
      Diva::Model(:score_hyperlink).new(
        description: "@#{screen_name}",
        uri: "https://twitter.com/#{screen_name}")
    end
  end
end
entity_hashtag(tweet, hashtag_entities) click to toggle source
# File core/plugin/twitter/twitter.rb, line 497
def entity_hashtag(tweet, hashtag_entities)
  entities_to_notes(hashtag_entities) do |hashtag|
    Plugin::Twitter::HashTag.new(name: hashtag[:text])
  end
end
entity_media(tweet, media_list) click to toggle source
# File core/plugin/twitter/twitter.rb, line 460
def entity_media(tweet, media_list)
  entities_to_notes(media_list) do |media_entity|
    text_note(description: '')
  end
end
entity_urls(tweet, urls) click to toggle source
# File core/plugin/twitter/twitter.rb, line 483
def entity_urls(tweet, urls)
  entities_to_notes(urls) do |url_entity|
    begin
      uri = Diva::URI.new(url_entity[:expanded_url] || url_entity[:url])
      uri.freeze
      Diva::Model(:score_hyperlink).new(
        description: url_entity[:display_url] || url_entity[:expanded_url] || url_entity[:url],
        uri: uri)
    rescue Addressable::URI::InvalidURIError => e
      text_note(description: url_entity[:display_url] || url_entity[:expanded_url] || url_entity[:url])
    end
  end
end
error(msg) click to toggle source

エラーメッセージを表示する。

# File core/utils.rb, line 118
def error(msg)
  log "error", msg if Mopt.error_level >= 1
  abort if Mopt.error_level >= 4
end
error_handling!(exception) click to toggle source
# File mikutter.rb, line 83
def error_handling!(exception)
  notice "catch #{exception.class}"
  if Mopt.debug && exception.respond_to?(:deferred) && exception.deferred
    if command_exist?('dot')
      notice "[[#{exception.deferred.graph_draw}]]"
    else
      notice exception.deferred.graph
    end
  end
  File.open(File.expand_path(File.join(Environment::TMPDIR, 'crashed_exception')), 'w'){ |io| Marshal.dump(exception, io) } rescue nil
  raise exception end
escape(value) click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 123
def escape(value)
  URI.escape(value.to_s, /[^a-zA-Z0-9\-\.\_\~]/)
end
everytime(&proc) click to toggle source

毎回評価オブジェクトを作成する

# File core/lib/lazy.rb, line 93
def everytime(&proc)
  EveryTime.new(&proc) end
extended_entity_media(tweet) click to toggle source
# File core/plugin/twitter/twitter.rb, line 407
def extended_entity_media(tweet)
  extended_entities = (tweet[:extended_entities][:media] rescue nil)
  if extended_entities
    space = text_note(description: ' ')
    result = extended_entities.map{ |media|
      case media[:type]
      when 'photo'
        photo = Diva::Model(:photo).generate(photo_variant_seeds(media), perma_link: media[:media_url_https])
        Diva::Model(:score_hyperlink).new(
          description: photo.uri,
          uri: photo.uri,
          reference: photo)
      when 'video'
        variant = Array(media[:video_info][:variants])
                    .select{|v|v[:content_type] == "video/mp4"}
                    .sort_by{|v|v[:bitrate]}
                    .last
        Diva::Model(:score_hyperlink).new(
          description: "#{media[:display_url]} (%.1fs)" % (media.dig(:video_info, :duration_millis)/1000.0),
          uri: variant[:url])
      when 'animated_gif'
        variant = Array(media[:video_info][:variants])
                    .select{|v|v[:content_type] == "video/mp4"}
                    .sort_by{|v|v[:bitrate]}
                    .last
        Diva::Model(:score_hyperlink).new(
          description: "#{media[:display_url]} (GIF)",
          uri: variant[:url])
      end
    }.flat_map{|media| [media, space] }
    result.pop
    result
  else
    []
  end
end
extract_tabs() click to toggle source

抽出タブオブジェクト。各キーは抽出タブIDで、値は以下のようなオブジェクト

name

タブの名前

sexp

条件式(S式)

source

どこのツイートを見るか(イベント名、配列で複数)

slug

タイムラインとタブのスラッグ

id

抽出タブのID

# File core/plugin/extract/extract.rb, line 67
def extract_tabs
  @extract_tabs ||= {} end
fetch_and_modify_for_using_lists(twitter, cache=:keep) click to toggle source

service が所有するリストを更新し、そのうち実際に抽出タブで使用されているリストについて、 リストのメンバーの更新と、直近のツイートの取得を行う

Args

twitter

Plugin::Twitter::World

# File core/plugin/list/list.rb, line 136
def fetch_and_modify_for_using_lists(twitter, cache=:keep)
  fetch_list_of_service(twitter, cache).next{|service_that_has_list|
    modifier = (Set.new(service_that_has_list) & Set.new(using_lists)).map{|list| list_modify_member(list, twitter: twitter, cache: cache)}
    Deferred.when(*modifier) unless modifier.empty?
  }.terminate(_('%{user_name}がフォローしているリストを取得できませんでした') % {user_name: twitter.user_obj.idname}) end
fetch_list_of_service(twitter, cache=:keep) click to toggle source

service が作成またはフォローしている全てのリストを取得する。 ただし、自分のアカウント(A)で作成したリストを、自分のアカウント(B)でフォローしている場合、 #fetch_list_of_service(B) の結果にそのリストは含まれず、 #fetch_list_of_service(A) の結果からしか見ることができない。

Args

twitter

リストのオーナーを示す Plugin::Twitter::World のインスタンス

cache

キャッシュの利用方法

Return

deferred

# File core/plugin/list/list.rb, line 67
def fetch_list_of_service(twitter, cache=:keep)
  twitter.lists(cache: cache, user: twitter.user_obj).next do |lists|
    set_available_lists(twitter, lists)
    Enumerator.new{|y|
      Plugin.filtering(:worlds, y)
    }.lazy.select{|world|
      world.class.slug == :twitter
    }.reject(&twitter.method(:==)).map(&:user).inject(lists.lazy) do |stream, u|
      stream.reject{|l| l.user == u }
    end
  end
end
file_get_contents(fn) click to toggle source

ファイルの内容を文字列に読み込む

# File core/utils.rb, line 59
def file_get_contents(fn)
  open(fn, 'rb:utf-8'.freeze){ |input|
    input.read
  }
end
file_put_contents(fn, body) click to toggle source

文字列をファイルに書き込む

# File core/utils.rb, line 66
def file_put_contents(fn, body)
  File.open(fn, 'wb'.freeze){ |put|
    put.write body
    body
  }
end
find_implement_widget_by_gtkwidget(widget) click to toggle source

Gtkオブジェクト widget に対応するウィジェットのオブジェクトを返す

Args

widget

Gtkウィジェット

Return

widget に対応するウィジェットオブジェクトまたは偽

# File core/plugin/gtk/gtk.rb, line 610
def find_implement_widget_by_gtkwidget(widget)
  @slug_dictionary.imaginally_by_gtk(widget) end
find_url_open_command() click to toggle source
# File core/plugin/web/web.rb, line 35
      def find_url_open_command
openable_commands = %w{xdg-open open /etc/alternatives/x-www-browser}
wellknown_browsers = %w{firefox chromium opera}
command = nil
catch(:urlopen) do
  openable_commands.each{ |o|
    if command_exist?(o)
      command = o
      throw :urlopen end }
  wellknown_browsers.each{ |o|
    if command_exist?(o)
      activity :system, _('この環境で、URLを開くためのコマンドが判別できなかったので、"%{command}"を使用します。設定の「表示→URLを開く方法」で、URLを開く方法を設定してください。') % {command: command}
      command = o
      throw :urlopen end } end
command end
focus_move_to_latest_widget(postbox) click to toggle source

最後にアクティブだったペインにフォーカスを与える。 親タイムラインがあれば、それにフォーカスを与える。

Args

postbox

基準となるウィジェット

# File core/plugin/command/command.rb, line 323
def focus_move_to_latest_widget(postbox)
  if postbox.parent.is_a? Plugin::GUI::Window
    pane = last_active_pane[postbox.parent.slug]
    pane.active! if pane
  else
    postbox.parent.active! end end
focus_move_to_nearest_postbox(widget) click to toggle source

一番近い postbox にフォーカスを与える

Args

widget

基準となるウィジェット

# File core/plugin/command/command.rb, line 311
def focus_move_to_nearest_postbox(widget)
  if widget.is_a? Plugin::GUI::HierarchyParent
    postbox = widget.children.find{ |w| w.is_a? Plugin::GUI::Postbox }
    if postbox
      return postbox.active! end end
  if widget.is_a? Plugin::GUI::HierarchyChild
    focus_move_to_nearest_postbox(widget.parent) end end
focus_move_widget(widget, distance) click to toggle source

フォーカスを widget から distance に移動する

Args

widget

起点となるウィジェット

distance

移動距離

# File core/plugin/command/command.rb, line 285
def focus_move_widget(widget, distance)
  type_strict widget => Plugin::GUI::HierarchyParent
  type_strict widget => Plugin::GUI::HierarchyChild
  children = widget.parent.children.select{ |w| w.is_a? widget.class }
  index = children.index(widget)
  term = children[(index + distance) % children.size]
  term = term.active_chain.last if term.respond_to? :active_chain
  term.active! if term
  src_tl = widget.active_chain.last
  if widget.is_a?(Plugin::GUI::Pane) and src_tl.is_a?(Plugin::GUI::Timeline) and term.is_a?(Plugin::GUI::Timeline)
    slide_timeline_focus(src_tl, term) end end
freezable?() click to toggle source

freezeできるならtrueを返す

# File core/utils.rb, line 317
def freezable?
  true end
freeze_ifn() click to toggle source

freezeできる場合はfreezeする。selfを返す

# File core/utils.rb, line 321
def freeze_ifn
  freeze if freezable?
  self end
gc() click to toggle source
# File core/plugin/api_request_file_cache.rb, line 5
def gc
  notice "apirequest cache was deleted. "+MikuTwitter::Cache.garbage_collect.inspect
  Reserver.new(3600, thread: SerialThread){
    gc }
end
gen_counter(count=0, increment=1) click to toggle source

スレッドセーフなカウンタを返す。 カウンタの初期値は count で、呼び出すたびに値が increment づつ増える。 なお、カウンタが返す値はインクリメント前の値。

# File core/utils.rb, line 50
def gen_counter(count=0, increment=1)
  mutex = Mutex.new
  lambda{
    mutex.synchronize{
      result = count
      count += increment
      result } } end
gen_listener_for_visible_check(uc, kind) click to toggle source
# File core/plugin/activity/activity.rb, line 89
def gen_listener_for_visible_check(uc, kind)
  UserConfig[uc] ||= []
  Plugin::Settings::Listener.new \
    get: ->(){ UserConfig[uc].include?(kind) rescue false },
    set: ->(value) do
      if value
        UserConfig[uc] += [kind]
      else
        UserConfig[uc] -= [kind] end
gen_message_filter() click to toggle source

Messageの配列を受け取り、一度以上受け取ったことのあるものを除外して返すフィルタを作成して返す

Return

フィルタのプロシージャ(Proc)

# File core/plugin/twitter/twitter.rb, line 63
def gen_message_filter
  appeared = Set.new
  ->(messages) do
    [messages.select{ |message| appeared.add(message.id) unless appeared.include?(message.id) }]
  end
end
gen_message_filter_with_service() click to toggle source

Serviceと、Messageの配列を受け取り、一度以上受け取ったことのあるものを除外して返すフィルタを作成して返す。 ただし、除外したかどうかはService毎に記録する。 また、アカウント登録前等、serviceがnilの時はシステムメッセージ以外を全て削除し、記録しない。

Return

フィルタのプロシージャ(Proc)

# File core/plugin/twitter/twitter.rb, line 44
def gen_message_filter_with_service
  service_filters = Hash.new{|h,k|h[k] = gen_message_filter}
  ->(service, messages, &cancel) do
    if service
      [service] + service_filters[service.user_obj.id].(messages)
    else
      system = messages.select(&:system?)
      if system.empty?
        cancel.call
      else
        [nil, system]
      end
    end
  end
end
gen_scrollbars(widget) click to toggle source

widget のためのスクロールバーを作って返す

Args

widget

Gtk::TextView

Return

縦スクロールバーと横スクロールバー

# File core/plugin/console/console.rb, line 74
def gen_scrollbars(widget)
  scroll_v = ::Gtk::VScrollbar.new
  scroll_h = ::Gtk::HScrollbar.new
  widget.set_scroll_adjustment(scroll_h.adjustment, scroll_v.adjustment)
  return scroll_v, scroll_h
end
gen_tags(buffer) click to toggle source

タグを作る

Args

buffer

Gtk::TextBuffer

# File core/plugin/console/console.rb, line 84
def gen_tags(buffer)
  type_strict buffer => ::Gtk::TextBuffer
  buffer.create_tag("prompt",
                    foreground_gdk: Gdk::Color.new(0, 0x6666, 0))
  buffer.create_tag("echo",
                    weight: Pango::FontDescription::WEIGHT_BOLD)
  buffer.create_tag("result",
                    foreground_gdk: Gdk::Color.new(0, 0, 0x6666))
  buffer.create_tag("errorclass",
                    foreground_gdk: Gdk::Color.new(0x6666, 0, 0))
  buffer.create_tag("error",
                    weight: Pango::FontDescription::WEIGHT_BOLD,
                    foreground_gdk: Gdk::Color.new(0x9999, 0, 0))
  buffer.create_tag("backtrace",
                    foreground_gdk: Gdk::Color.new(0x3333, 0, 0))
end
get_api_property(api, options, apilist) click to toggle source
# File core/plugin/twitter/mikutwitter/query.rb, line 146
def get_api_property(api, options, apilist)
  api = api.split('/') if api.is_a? String
  path = api.empty? ? '.' : api[0]
  method = apilist.has_key?(path) ? apilist[path] : apilist['*']
  if method.is_a? Hash
    get_api_property(api[1, api.size], options, method)
  elsif method.respond_to? :call
    method.call(api, options)
  else
    method end end
get_window_geometry(slug) click to toggle source
# File core/plugin/gtk/gtk.rb, line 538
def get_window_geometry(slug)
  type_strict slug => Symbol
  geo = UserConfig[:windows_geometry]
  if defined? geo[slug]
    geo[slug]
  else
    size = [Gdk.screen_width/3, Gdk.screen_height*4/5]
    { size: size,
      position: [Gdk.screen_width - size[0], Gdk.screen_height/2 - size[1]/2] } end end
guide_start(ach) click to toggle source
# File core/plugin/guide/guide.rb, line 23
def guide_start(ach)
  tab :guide, _('World ガイド') do
    set_icon Skin['icon.png']
    timeline(:guide)
  end

  on_finish_guide do |world|
    ach.take!
  end

  seq = at(:guide_sequence)
  case seq
  when nil, :first
    jump_seq(:first)
  else
    sequence.
      say(_("前回の続きから説明するね")).
      next{ jump_seq(seq) }
  end
end
icon_path(photo) click to toggle source
# File core/plugin/libnotify/notify-send.rb, line 24
def icon_path(photo)
  ext = photo.uri.path.split('.').last || 'png'
  fn = File.join(icon_tmp_dir, Digest::MD5.hexdigest(photo.uri.to_s) + ".#{ext}")
  Delayer::Deferred.new.next{
    case
    when FileTest.exist?(fn)
      fn
    else
      photo.download_pixbuf(width: 48, height: 48).next{|p|
        FileUtils.mkdir_p(icon_tmp_dir)
        photo.pixbuf(width: 48, height: 48).save(fn, 'png')
        fn
      }
    end
  }
end
icon_tmp_dir() click to toggle source
# File core/plugin/libnotify/notify-send.rb, line 41
        def icon_tmp_dir
  File.join(Environment::TMPDIR, 'libnotify', 'icon').freeze
end
imsorry() click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 48
def imsorry
  _("%{mikutter} が突然終了してしまったみたいで ヽ('ω')ノ三ヽ('ω')ノもうしわけねぇもうしわけねぇ")%{mikutter: Environment::NAME}+"\n"+
    _('OKボタンを押したら、自動的に以下のテキストが送られます。これがバグを直すのにとっても役に立つんですよ。よかったら送ってくれません?')
end
intent_choose_dialog(intents, model: nil, uri: model.uri) click to toggle source
# File core/plugin/intent_selector/intent_selector.rb, line 44
def intent_choose_dialog(intents, model: nil, uri: model.uri)
  dialog = Gtk::Dialog.new('開く - %{application_name}' % {application_name: Environment::NAME})
  dialog.window_position = Gtk::Window::POS_CENTER
  dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
  dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
  dialog.vbox.closeup(Gtk::Label.new("%{uri}\nを開こうとしています。どの方法で開きますか?" % {uri: uri}, false))
  intent_token_builder = {
    uri: uri,
    model: model,
    intent: nil,
    parent: nil }
  intents.inject(nil) do |group, intent|
    if group
      radio = Gtk::RadioButton.new(group, intent.label)
    else
      intent_token_builder[:intent] = intent
      radio = Gtk::RadioButton.new(intent.label) end
    radio.ssc(:toggled) do |w|
      intent_token_builder[:intent] = intent
      false
    end
    radio.ssc(:activate) do |w|
      intent_token_builder[:intent] = intent
      dialog.signal_emit(:response, Gtk::Dialog::RESPONSE_OK)
      false
    end
    dialog.vbox.closeup(radio)
    group || radio
  end
  saving_rule_checkbox(dialog, intent_token_builder, specified_model_slug(model))
  dialog.ssc(:response) do |w, response_id|
    if response_id == Gtk::Dialog::RESPONSE_OK and intent_token_builder[:intent]
      Plugin::Intent::IntentToken.open(**intent_token_builder)
    end
    w.destroy
    false
  end
  dialog.show_all
end
intent_open(intents, model: nil, uri: model.uri) click to toggle source

model: または uri: を開くintentを intents の中から選び出し、その方法で開く。 このメソッドは、まず設定されたルールでintentを選出し、一つにintentが定まれば直ちにそれで開く。 候補が一つに絞れなかった場合は、intent選択ダイアログを表示して、ユーザに決定を仰ぐ。

Args

intents

Intent modelの配列

model:

開くModel。 uri: しかわからない場合は、省略してもよい

uri:

開くURI。 model: を渡している場合は、省略してもよい

# File core/plugin/intent_selector/intent_selector.rb, line 31
def intent_open(intents, model: nil, uri: model.uri)
  recommended, suggested = divide_intents(intents, uri, specified_model_slug(model))
  if recommended.size == 1
    Plugin::Intent::IntentToken.open(
      uri: uri,
      model: model,
      intent: recommended.first,
      parent: nil)
  else
    intent_choose_dialog(recommended + suggested, model: model, uri: uri)
  end
end
into_debug_mode(exception = nil, bind = binding) click to toggle source

–debug オプション付きで起動されている場合、インタプリタに入る。

Args

exception

原因となった例外

binding

インタプリタを実行するスコープ

Return

インタプリタから復帰したら(インタプリタが起動されたら)true、 デバッグモードではない、pryがインストールされていない等、起動に失敗したらfalse

# File core/utils.rb, line 130
def into_debug_mode(exception = nil, bind = binding)
  if Mopt.debug and not Mopt.testing
    require_if_exist 'pry'
    if binding.respond_to?(:pry)
      log "error", exception if exception
      $into_debug_mode_lock.synchronize {
        begin
          $into_debug_mode = Set.new
          bind.pry
        ensure
          threads = $into_debug_mode
          $into_debug_mode = false
          threads.each &:wakeup end }
      return true end end end
jump_seq(name) click to toggle source
# File core/plugin/guide/guide.rb, line 14
def jump_seq(name)
  if defined? @sequence[name]
    store(:guide_sequence, name)
    if @sequence.has_key? name
      @sequence[name].call
    else
      @sequence[:first].call
    end end end
last_active_pane() click to toggle source
# File core/plugin/command/command.rb, line 330
def last_active_pane
  @last_active_pane ||= {} end
lazy(&proc) click to toggle source

遅延評価オブジェクトを作成する。ブロックを取って呼ばれた場合は Lazy のインスタンスを、 何も取らずに読んだ場合は MethodCallDelayer のインスタンスを返す。

# File core/lib/lazy.rb, line 86
def lazy(&proc)
  if proc
    Lazy.new(&proc)
  else
    MethodCallDelayer.new end end
list_modify_member(list, cache: :keep, twitter:) click to toggle source

リストのメンバーを取得する

Args

list

リスト

cache

キャッシュの利用方法

twitter

list にアクセス可能なユーザを示す Plugin::Twitter::World のインスタンス

Return

deferred

# File core/plugin/list/list.rb, line 87
def list_modify_member(list, cache: :keep, twitter:)
  twitter.list_members( list_id: list[:id],
                        mode: list[:mode],
                        cache: false).next{ |users|
    list.add_member(users) if users
    twitter.list_statuses(:id => list[:id],
                          :cache => cache).next{ |res|
      if res.is_a?(Array) and using?(list)
        Plugin.call(:extract_receive_message, datasource_slug(list), res) end
    }.terminate
  }.trap { |error|
    if defined?(error.httpresponse.code) && 404 == error.httpresponse.code.to_i
      Plugin.call(:list_destroy, twitter, [list])
      Plugin.activity :error, _("リスト「%{list_name} (#%{list_id})」は削除されているか、@%{screen_name} が閲覧することを禁止されています") % {
        screen_name: twitter.user_obj.idname,
        list_id: list[:id],
        list_name: list[:full_name] }
    else
      Deferred.fail(error)
    end }.terminate(_("リスト %{list_name} (#%{list_id}) のメンバーの取得に失敗しました") % {
                      list_name: list[:full_name],
                      list_id: list[:id] }) end
load_world_ifn() click to toggle source
# File core/plugin/world/world.rb, line 88
def load_world_ifn
  @worlds ||= Plugin::World::Keep.accounts.map { |id, serialized|
    provider = Diva::Model(serialized[:provider])
    if provider
      provider.new(serialized)
    else
      Miquire::Plugin.load(serialized[:provider])
      provider = Diva::Model(serialized[:provider])
      if provider
        provider.new(serialized)
      else
        activity :system, _('アカウント「%{world}」のためのプラグインが読み込めなかったため、このアカウントの登録をmikutterから解除しました。') % {world: id},
                 description: _('アカウント「%{world}」に必要な%{plugin}プラグインが見つからなかったため、このアカウントの登録をmikutterから解除しました。') % {plugin: serialized[:provider], world: id}
        Plugin.call(:world_destroy, Plugin::World::Zombie.new(slug: id))
        nil
      end
    end
  }.compact.freeze
end
localfile_hash(hash) click to toggle source
# File core/plugin/aspectframe/aspectframe.rb, line 89
def localfile_hash(hash)
  File.expand_path(File.join(Plugin::AspectFrame::CACHE_DIR, "#{hash.upcase}_5.png")) end
log(prefix, object) click to toggle source

エラーログに記録する。 内部処理用。外部からは呼び出さないこと。

# File core/utils.rb, line 224
def log(prefix, object)
  debugging_wait
  begin
    msg = "#{prefix}: #{caller_util}: #{object}"
    msg += "\nfrom " + object.backtrace.join("\nfrom ") if object.is_a? Exception
    unless $daemon
      $log_lock.synchronize do
        if msg.is_a? Exception
          __write_stderr(msg.to_s)
          __write_stderr(msg.backtrace.join("\n"))
        else
          __write_stderr(msg) end end
      if logfile
        FileUtils.mkdir_p(File.expand_path(File.dirname(logfile + '_')))
        File.open(File.expand_path("#{logfile}#{Time.now.strftime('%Y-%m-%d')}.log"), 'a'){ |wp|
          wp.write("#{Time.now.to_s}: #{msg}\n") } end end
  rescue Exception => e
    __write_stderr("critical!: #{caller(0)}: #{e.to_s}")
  end
end
logfile(fn = nil) click to toggle source

ログファイルを取得設定

# File core/utils.rb, line 267
def logfile(fn = nil)
  if(fn) then
    $logfile = fn
  end
  $logfile or nil
end
main() click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 53
def main
  Gtk::VBox.new(false, 0).
    closeup(Gtk::IntelligentTextview.new(imsorry)).
    pack_start(Gtk::ScrolledWindow.
               new.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS).
               add(Gtk::IntelligentTextview.new(backtrace)))
end
mainthread_only() click to toggle source

メインスレッド以外で呼び出されたらThreadErrorを投げる

# File core/utils.rb, line 208
def mainthread_only
  unless Thread.main == Thread.current
    raise ThreadError.new('The method can calls only main thread. but called by another thread.') end end
melt() click to toggle source

freezeされていない同じ内容のオブジェクトを作って返す。 メルト 溶けてしまいそう (実装が)dupだなんて 絶対に 言えない

# File core/utils.rb, line 327
def melt
  if frozen? then dup else self end end
metamorphose(code: raise, assign: Set.new, extract_condition: nil) click to toggle source

条件をこう、くいっと変形させてな

# File core/plugin/extract/extract.rb, line 310
def metamorphose(code: raise, assign: Set.new, extract_condition: nil)
  extract_condition ||= Hash[Plugin.filtering(:extract_condition, []).first.map{ |condition| [condition.slug, condition] }]
  case code
  when MIKU::Atom
    return code
  when MIKU::List
    return true if code.empty?
    condition = if code.size <= 2
                  extract_condition[code.car]
                else
                  extract_condition[code.cdr.car] end
    case condition
    when Plugin::Extract::ExtensibleSexpCondition
      metamorphose_sexp(code: code, condition: condition)
    when Plugin::Extract::ExtensibleCondition
      assign << "#{condition.slug} = Plugin::Extract::Calc.new(message, extract_condition[:#{condition.slug}])"
      if condition.operator
        code
      else
        # MIKU::Cons.new(:call, MIKU::Cons.new(condition.slug, nil))
        [:call, condition.slug]
      end
    else
      if code.cdr.car.is_a? Symbol and not %i[and or not].include?(code.car)
        raise Plugin::Extract::ConditionNotFoundError, _('抽出条件 `%{condition}\' が見つかりませんでした') % {condition: code.cdr.car} end
      code.map{|node| metamorphose(code: node,
                                   assign: assign,
                                   extract_condition: extract_condition) } end end end
metamorphose_sexp(code: raise, condition: raise) click to toggle source
# File core/plugin/extract/extract.rb, line 339
def metamorphose_sexp(code: raise, condition: raise)
  miku_context = MIKU::SymbolTable.new
  miku_context[:compare] = MIKU::Cons.new(code.car, nil)
  miku_context[:args] = MIKU::Cons.new(code.cdr.cdr, nil)
  begin
    miku(condition.sexp, miku_context)
  rescue => exception
    error "error occurred in code #{MIKU.unparse(condition.sexp)}"
    error miku_context
    raise exception end end
method_of_api() click to toggle source

get, post, put, deleteの何れかを返す。 nilの場合は未定義(まぁget)

# File core/plugin/twitter/mikutwitter/query.rb, line 159
def method_of_api
  aster_post = { '*' => :post }.freeze
  create_destroy_post = { 'create' => :post, 'destroy' => :post }.freeze
  @method_of_api ||= {
    'statuses' => {
      'destroy' => :post,
      'retweet' => aster_post,
      'update' => :post,
      'update_with_media' => :post },
    'direct_messages' => {
      'destroy' => :post,
      'new' => :post },
    'friendships' => {
      'create' => :post,
      'destroy' => :post,
      'update' => :post },
    'favorites' => create_destroy_post,
    'lists' => {
      'members' => {
        'create' => :post,
        'destroy' => :post,
        'create_all' => :post },
      'subscribers' => create_destroy_post,
      'destroy' => :post,
      'update' => :post,
      'create' => :post },
    'account' => {
      'end_session' => :post,
      'update_profile' => :post,
      'update_profile_background_image' => :post,
      'update_profile_colors' => :post,
      'update_profile_image' => :post,
      'settings' => :post },
    'notifications' => aster_post,
    'saved_searches' => {
      'create' => :post,
      'destroy' => aster_post },
    'geo' => {
      'place' => :post },
    'media' => {
      'upload' => :post },
    'blocks' => create_destroy_post,
    'report_spam' => :post,
    'oauth' => {
      'access_token' => :post,
      'request_token' => :post }
  }
end
miku(node, scope=MIKU::SymbolTable.new) click to toggle source
# File core/miku/miku.rb, line 15
def miku(node, scope=MIKU::SymbolTable.new)
  if(node.is_a? MIKU::Node) then
    begin
      node.miku_eval(scope)
    rescue MIKU::MikuException => e
      warn e
    rescue Exception => e
      warn "[MIKU Bug] fatal error on code #{node.inspect}"
      raise e
    end
  else
    node end end
miku_stream(stream, scope) click to toggle source
# File core/miku/miku.rb, line 28
def miku_stream(stream, scope)
  begin
    while(not stream.eof?) do
      miku(MIKU.parse(stream), scope) end
  rescue MIKU::EndofFile
  rescue MIKU::MikuException => e
    warn e end end
mikutter_error() click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 98
def mikutter_error
  name = File.expand_path(File.join(Environment::TMPDIR, 'mikutter_error'))
  if FileTest.exist?(name)
    file_get_contents(name) end end
miquire(*args) click to toggle source

ミクってかわいいよねぇ。 ツインテールいいよねー。 どう良いのかを書くとコードより長くなりそうだから詳しくは書かないけどいいよねぇ。 ふたりで寒い時とかに歩いてたら首にまいてくれるんだよー。 我ながらなんてわかりやすい説明なんだろう。 (訳: Miquire.miquire のエイリアス)

# File core/miquire.rb, line 16
def miquire(*args)
  Miquire.miquire(*args)
end
modify_extract_tabs() click to toggle source

抽出タブの現在の内容を保存する

# File core/plugin/extract/extract.rb, line 275
def modify_extract_tabs
  UserConfig[:extract_tabs] = extract_tabs.values.map(&:export_to_userconfig)
  self end
modify_world(target) click to toggle source
# File core/plugin/world/world.rb, line 75
def modify_world(target)
  if Plugin::World::Keep.accounts.has_key?(target.slug.to_sym)
    Plugin::World::Keep.account_modify target.slug, target.to_hash.merge(provider: target.class.slug)
    @worlds = nil
  end
end
mute?(params) click to toggle source

そのイベントをミュートするかどうかを返す(trueなら表示しない)

# File core/plugin/activity/activity.rb, line 63
def mute?(params)
  mute_kind = UserConfig[:activity_mute_kind]
  if mute_kind.is_a? Array
    return true if mute_kind.map(&:to_s).include? params[:kind].to_s end
  mute_kind_related = UserConfig[:activity_mute_kind_related]
  if mute_kind_related
    return true if mute_kind_related.map(&:to_s).include?(params[:kind].to_s) and !params[:related] end
  false end
mutebutton(user) click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 149
def mutebutton(user)
  changer = lambda{ |new, widget|
    if new === nil
      UserConfig[:muted_users] and UserConfig[:muted_users].include?(user.idname)
    elsif new
      add_muted_user(user)
    else
      remove_muted_user(user)
    end
  }
  Mtk::boolean(changer, _('ミュート')) end
no_mainthread() click to toggle source

メインスレッドで呼び出されたらThreadErrorを投げる

# File core/utils.rb, line 213
def no_mainthread
  if Thread.main == Thread.current
    raise ThreadError.new('The method can not calls main thread. but called by main thread.') end end
notice(msg) click to toggle source

一般メッセージを表示する。

# File core/utils.rb, line 108
def notice(msg)
  log "notice", msg if Mopt.error_level >= 3
end
now() click to toggle source
# File core/plugin/aspectframe/aspectframe.rb, line 36
def now
  Plugin::AspectFrame.now
end
object_get_contents(fn) click to toggle source

ファイル fn の内容からオブジェクトを読み込む。 fn は、object_put_contents() で保存されたファイルでなければならない。

# File core/utils.rb, line 75
def object_get_contents(fn)
  File.open(fn, 'rb'.freeze){ |input|
    Marshal.load input
  }
end
open_model(model, token: nil) click to toggle source

model をUI上で開く。 このメソッドが呼ばれたらIntentTokenを生成して、開くことを試みる。 #open_uriは、Modelが必要になった時にURIからModelの取得生成を試みるが、 このメソッドはヒントとして model を与えるため、探索が発生せず高速に処理できる。

Args

model

対象となるDiva::Model

# File core/plugin/intent/intent.rb, line 128
def open_model(model, token: nil)
  intents = Plugin.filtering(:intent_select_by_model_slug, model.class.slug, Set.new).last
  if token
    intents = intents.reject{|intent| token.intent_ancestors.include?(intent) }
  end
  head = intents.first(2)
  case head.size
  when 0
    open_uri(model.uri, token: token)
  when 1
    intent = head.first
    Plugin::Intent::IntentToken.open(
      uri: model.uri,
      model: model,
      intent: intent,
      parent: token)
  else
    Plugin.call(:intent_select, intents, model)
  end
end
open_uri(uri, token: nil) click to toggle source

uri をUI上で開く。 このメソッドが呼ばれたらIntentTokenを生成して、開くことを試みる。 #open_modelのほうが高速なので、modelオブジェクトが存在するならばopen_modelを呼ぶこと。

Args

uri

対象となるURI

token:

親となるIntentToken

# File core/plugin/intent/intent.rb, line 95
def open_uri(uri, token: nil)
  model_slugs = Plugin.filtering(:model_of_uri, uri.freeze, Set.new).last
  if model_slugs.empty?
    error "model not found to open for #{uri}"
    return
  end
  intents = model_slugs.lazy.flat_map{|model_slug|
    Plugin.filtering(:intent_select_by_model_slug, model_slug, []).last
  }
  if token
    intents = intents.reject{|intent| token.intent_ancestors.include?(intent) }
  end
  head = intents.first(2)
  case head.size
  when 0
    error "intent not found to open for #{model_slugs.to_a}"
    return
  when 1
    Plugin::Intent::IntentToken.open(
      uri: uri,
      intent: head.first,
      parent: token)
  else
    Plugin.call(:intent_select, intents, uri)
  end
end
openurl(url) click to toggle source
# File core/plugin/web/web.rb, line 10
def openurl(url)
  if UserConfig[:url_open_specified_command]
    url_open_command = UserConfig[:url_open_command]
    bg_system(url_open_command, url)
  elsif(defined? Win32API) then
    shellExecuteA = Win32API.new('shell32.dll','ShellExecuteA',%w(p p p p p i),'i')
    shellExecuteA.call(0, 'open', url.melt, 0, 0, 1)
  else
    url_open_command = find_url_open_command
    if url_open_command
      bg_system(url_open_command, url)
    else
      activity :system, _("この環境で、URLを開くためのコマンドが判別できませんでした。設定の「表示→URLを開く方法」で、URLを開く方法を設定してください。") end end
rescue => exception
  title = _('コマンド "%{command}" でURLを開こうとしましたが、開けませんでした。設定の「表示→URLを開く方法」で、URLを開く方法を設定してください。') % {command: url_open_command}
  description = {
    title: title,
    message: exception.to_s,
    backtrace: exception.backtrace.join("\n") }
  activity :system, title,
           error: exception,
           description: "%{title}\n\n%{message}\n\n%{backtrace}" % description
end
pane_order_delete(i_pane) click to toggle source

ペインを順序リストから削除する

Args

i_pane

ペイン

# File core/plugin/gtk/gtk.rb, line 584
def pane_order_delete(i_pane)
  order = UserConfig[:ui_tab_order].melt
  i_window = i_pane.parent
  order[i_window.slug] = order[i_window.slug].melt
  order[i_window.slug].delete(i_pane.slug)
  UserConfig[:ui_tab_order] = order
end
parallel(&proc) click to toggle source

平行評価オブジェクトを作成する

# File core/lib/lazy.rb, line 97
def parallel(&proc)
  Parallel.new(&proc) end
photo_variant_seeds(media) click to toggle source
# File core/plugin/twitter/twitter.rb, line 444
def photo_variant_seeds(media)
  Enumerator.new do |yielder|
    yielder << { policy: :original,
                 photo: "#{media[:media_url_https]}:orig" }
    media[:sizes].select{ |size_name, size|
      size.has_key?(:w) && size.has_key?(:h) && size.has_key?(:resize)
    }.each do |size_name, size|
      yielder << { name: size_name.to_sym,
                   width: size[:w],
                   height: size[:h],
                   policy: size[:resize].to_sym,
                   photo: "#{media[:media_url_https]}:#{size_name}" }
    end
  end
end
popup() click to toggle source
posted_url_length(url) click to toggle source

URL url がTwitterに投稿された時に何文字としてカウントされるかを返す

Args

url

String URL

Return

Fixnum URLの長さ

# File core/plugin/twitter/twitter.rb, line 75
def posted_url_length(url)
  if url.start_with?('https://'.freeze)
    @twitter_configuration[:short_url_length_https] || 23
  else
    @twitter_configuration[:short_url_length] || 22
  end
end
present_tab() click to toggle source
# File core/plugin/home_timeline/home_timeline.rb, line 23
def present_tab
  @tag ||= handler_tag do
    tab :home_timeline, _("Home Timeline") do
      set_icon Skin['timeline.png']
      timeline :home_timeline end

    on_update do |s, ms|
      timeline(:home_timeline) << ms end
  end
end
preview_icons(dir) click to toggle source

プレビューアイコンのリスト

# File core/plugin/skin_setting_gtk/skin_setting_gtk.rb, line 4
def preview_icons(dir)
  famous_icons = [ "timeline.png", "reply.png", "activity.png", "directmessage.png" ]
  skin_icons = Dir.glob(File.join(dir, "*.png")).sort.map { |_| File.basename(_) }

  (famous_icons + skin_icons).uniq.select { |_| File.exist?(File.join(dir, _)) }[0, 12]
end
preview_widget(info) click to toggle source

スキンのプレビューを表示するウィジェットを生成する

# File core/plugin/skin_setting_gtk/skin_setting_gtk.rb, line 12
def preview_widget(info)
  fix = Gtk::Fixed.new
  frame = Gtk::Frame.new
  box = Gtk::HBox.new(false)

  preview_icons(info[:dir]).each { |_|
    photos = Enumerator.new{|y|
      Plugin.filtering(:photo_filter, File.join(info[:dir], _), y)
    }
    image = Gtk::WebIcon.new(photos.first, 32, 32)
    box.pack_start(image, false, false)
  }

  fix.put(frame.add(box, nil), 16, 0)
end
profile_head(user, intent_token) click to toggle source

ユーザのプロフィールのヘッダ部を返す

Args

user

表示するUser

intent_token

ユーザを開くときに利用するIntent

Return

ヘッダ部を表すGtkコンテナ

# File core/plugin/user_detail_view/user_detail_view.rb, line 267
def profile_head(user, intent_token)
  eventbox = ::Gtk::EventBox.new
  eventbox.ssc('visibility-notify-event'){
    eventbox.style = background_color
    false }

  icon = ::Gtk::EventBox.new.add(::Gtk::WebIcon.new(user.icon_large, UserConfig[:profile_icon_size], UserConfig[:profile_icon_size]).tooltip(_('アイコンを開く')))
  icon.ssc(:button_press_event) do |this, event|
    Plugin.call(:open, user.icon_large)
    true end
  icon.ssc(:realize) do |this|
    this.window.set_cursor(Gdk::Cursor.new(Gdk::Cursor::HAND2))
    false end

  icon_alignment = Gtk::Alignment.new(0.5, 0, 0, 0)
                   .set_padding(*[UserConfig[:profile_icon_margin]]*4)

  eventbox.add(::Gtk::VBox.new(false, 0).
                add(::Gtk::HBox.new.
                     closeup(icon_alignment.add(icon)).
                     add(::Gtk::VBox.new.closeup(user_name(user, intent_token)).closeup(profile_table(user)))))
end
profile_table(user) click to toggle source

プロフィールの上のところの格子になってる奴をかえす

Args

user

表示するUser

Return

プロフィールのステータス部を表すGtkコンテナ

# File core/plugin/user_detail_view/user_detail_view.rb, line 326
def profile_table(user)
  _, profile_columns = Plugin.filtering(:user_detail_view_header_columns, user, [
                                          ['tweets',     user[:statuses_count].to_s],
                                          ['favs',       user[:favourites_count].to_s],
                                          ['followings', user[:friends_count]],
                                          ['followers',  user[:followers_count]],
                                        ])
  ::Gtk::Table.new(2, profile_columns.size).tap{|table|
    profile_columns.each_with_index do |column, index|
      key, value = column
      table.
        attach(::Gtk::Label.new(value.to_s).right, 0, 1, index, index+1).
        attach(::Gtk::Label.new(key.to_s)  .left , 1, 2, index, index+1)
    end
  }.set_row_spacing(0, 4).
    set_row_spacing(1, 4).
    set_column_spacing(0, 16)
end
refresh(cache=:keep) click to toggle source

Service について saved search を取得する

Args

cache

キャッシュの利用方法

Return

deferred

# File core/plugin/saved_search/saved_search.rb, line 81
def refresh(cache=:keep)
  Deferred.when(*Service.map { |service| refresh_for_service(service, cache) }) end
refresh_for_service(service, cache=:keep) click to toggle source

あるServiceに対してのみ saved search 一覧を取得する

Args

service

Service 対象となるService

cache

キャッシュの利用方法

Return

deferred

# File core/plugin/saved_search/saved_search.rb, line 90
def refresh_for_service(service, cache=:keep)
  service.saved_searches(cache: cache).next{ |res|
    if res
      saved_searches = {}
      res.each{ |record|
        saved_searches[record[:id]] = Plugin::SavedSearch::SavedSearch.new(record[:id],
                                                                           URI.decode(record[:query]),
                                                                           URI.decode(record[:name]),
                                                                           :"savedsearch_#{record[:id]}",
                                                                           service) }
      new_ids = saved_searches.keys
      old_ids = timelines.values.select{|s| s.service == service }.map(&:id)
      (new_ids - old_ids).each{ |id| add_tab(saved_searches[id]) }
      (old_ids - new_ids).each{ |id| delete_tab(id) }
      new_ids.each{ |id| rewind_timeline(saved_searches[id]) } end }.terminate end
refresh_tab() click to toggle source
# File core/plugin/home_timeline/home_timeline.rb, line 15
def refresh_tab
  if Enumerator.new{|y| Plugin.filtering(:worlds, y) }.any?{|w| w.class.slug == :twitter }
    present_tab
  else
    absent_tab
  end
end
register_cache(saved_search) click to toggle source

保存した検索の情報をキャッシュに登録する

Args

saved_search

保存した検索

# File core/plugin/saved_search/saved_search.rb, line 109
def register_cache(saved_search)
  type_strict saved_search => Plugin::SavedSearch::SavedSearch
  cache = at(:last_saved_search_state, {}).melt
  cache[saved_search.id] = {
    id: saved_search.id,
    query: saved_search.query,
    name: saved_search.name,
    slug: saved_search.slug,
    service_id: saved_search.service.user_obj.id
  }
  store(:last_saved_search_state, cache) end
register_world(new) click to toggle source

新たなアカウントを登録する。

Args

new

追加するアカウント(Diva::Model)

# File core/plugin/world/world.rb, line 61
def register_world(new)
  Plugin::World::Keep.account_register new.slug, new.to_hash.merge(provider: new.class.slug)
  @worlds = nil
  Plugin.call(:world_after_created, new)
  Plugin.call(:service_registered, new) # 互換性のため
rescue Plugin::World::AlreadyExistError
  description = {
    new_world: new.title,
    duplicated_world: @worlds.find{|w| w.slug == new.slug }&.title,
    world_slug: new.slug }
  activity :system, _('既に登録されているアカウントと重複しているため、登録に失敗しました。'),
           description: _('登録しようとしたアカウント「%{new_world}」は、既に登録されている「%{duplicated_world}」と同じ識別子「%{world_slug}」を持っているため、登録に失敗しました。') % description
end
relation() click to toggle source
# File core/plugin/followingcontrol/followingcontrol.rb, line 143
def relation
  @relation end
relation_bar(user) click to toggle source

フォロー関係を表示する

Args

user

対象となるユーザ

Return

リレーションバーのウィジェット(Gtk::VBox)

# File core/plugin/user_detail_view/user_detail_view.rb, line 180
def relation_bar(user)
  icon_size = Gdk::Rectangle.new(0, 0, 32, 32)
  arrow_size = Gdk::Rectangle.new(0, 0, 16, 16)
  container = ::Gtk::VBox.new(false, 4)
  Enumerator.new{|y|
    Plugin.filtering(:worlds, y)
  }.select{|world|
    world.class.slug == :twitter
  }.each{ |me|
    following = followed = nil
    w_following_label = ::Gtk::Label.new(_("関係を取得中"))
    w_followed_label = ::Gtk::Label.new("")
    w_eventbox_image_following = ::Gtk::EventBox.new
    w_eventbox_image_followed = ::Gtk::EventBox.new
    relation = if me.user_obj == user
                 ::Gtk::Label.new(_("それはあなたです!"))
               else
                 ::Gtk::HBox.new.
                   closeup(w_eventbox_image_following).
                   closeup(w_following_label) end
    relation_container = ::Gtk::HBox.new(false, icon_size.width/2)
    relation_container.closeup(::Gtk::WebIcon.new(me.user_obj.icon, icon_size).tooltip("#{me.user_obj.idname}(#{me.user_obj[:name]})"))
    relation_container.closeup(::Gtk::VBox.new.
                               closeup(relation).
                               closeup(::Gtk::HBox.new.
                                       closeup(w_eventbox_image_followed).
                                       closeup(w_followed_label)))
    relation_container.closeup(::Gtk::WebIcon.new(user.icon, icon_size).tooltip("#{user.idname}(#{user[:name]})"))
    if me.user_obj != user
      followbutton = ::Gtk::Button.new
      followbutton.sensitive = false
      # フォローしている状態の更新
      m_following_refresh = lambda { |new|
        if not w_eventbox_image_following.destroyed?
          following = new
          if not w_eventbox_image_following.children.empty?
            w_eventbox_image_following.remove(w_eventbox_image_following.children.first) end

          w_eventbox_image_following.style = w_eventbox_image_following.parent.style
          w_eventbox_image_following.add(::Gtk::WebIcon.new(Skin[new ? 'arrow_following.png' : 'arrow_notfollowing.png'], arrow_size).show_all)
          w_following_label.text = new ? _("フョローしている") : _("フョローしていない")
          followbutton.label = new ? _("解除") : _("フョロー") end }
      # フォローされている状態の更新
      m_followed_refresh = lambda { |new|
        if not w_eventbox_image_followed.destroyed?
          followed = new
          if not w_eventbox_image_followed.children.empty?
            w_eventbox_image_followed.remove(w_eventbox_image_followed.children.first) end
          w_eventbox_image_followed.style = w_eventbox_image_followed.parent.style
          w_eventbox_image_followed.add(::Gtk::WebIcon.new(Skin.get_path(new ? "arrow_followed.png" : "arrow_notfollowed.png"), arrow_size).show_all)
          w_followed_label.text = new ? _("フョローされている") : _("フョローされていない") end }
      me.friendship(target_id: user[:id], source_id: me.user_obj[:id]).next{ |rel|
        if rel and not(w_eventbox_image_following.destroyed?)
          m_following_refresh.call(rel[:following])
          m_followed_refresh.call(rel[:followed_by])
          handler_followings_created = on_followings_created do |service, dst_users|
            if service == me and dst_users.include?(user)
              m_following_refresh.call(true) end end
          handler_followings_destroy = on_followings_destroy do |service, dst_users|
            if service == me and dst_users.include?(user)
              m_following_refresh.call(false) end end
          followbutton.ssc(:clicked){
            followbutton.sensitive = false
            event = following ? :followings_destroy : :followings_created
            me.__send__(following ? :unfollow : :follow, user_id: user.id).next{ |msg|
              Plugin.call(event, me, [user])
              followbutton.sensitive = true unless followbutton.destroyed? }.
            terminate.trap{
              followbutton.sensitive = true unless followbutton.destroyed? }
            true }
          followbutton.signal_connect(:destroy){
            detach(:followings_created, handler_followings_created)
            detach(:followings_destroy, handler_followings_destroy)
            false }
          followbutton.sensitive = true
          relation_container.closeup(followbutton) end
      }.terminate.trap{
        w_following_label.text = _("取得できませんでした") } end
    container.closeup(relation_container) }
  container end
remove_muted_user(user) click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 168
  def remove_muted_user(user)
    type_strict user => User
    atomic{
      muted = (UserConfig[:muted_users] ||= []).melt
      muted.delete(user.idname)
      UserConfig[:muted_users] = muted } end

  # フォロー関係を表示する
  # ==== Args
  # [user] 対象となるユーザ
  # ==== Return
  # リレーションバーのウィジェット(Gtk::VBox)
  def relation_bar(user)
    icon_size = Gdk::Rectangle.new(0, 0, 32, 32)
    arrow_size = Gdk::Rectangle.new(0, 0, 16, 16)
    container = ::Gtk::VBox.new(false, 4)
    Enumerator.new{|y|
      Plugin.filtering(:worlds, y)
    }.select{|world|
      world.class.slug == :twitter
    }.each{ |me|
      following = followed = nil
      w_following_label = ::Gtk::Label.new(_("関係を取得中"))
      w_followed_label = ::Gtk::Label.new("")
      w_eventbox_image_following = ::Gtk::EventBox.new
      w_eventbox_image_followed = ::Gtk::EventBox.new
      relation = if me.user_obj == user
                   ::Gtk::Label.new(_("それはあなたです!"))
                 else
                   ::Gtk::HBox.new.
                     closeup(w_eventbox_image_following).
                     closeup(w_following_label) end
      relation_container = ::Gtk::HBox.new(false, icon_size.width/2)
      relation_container.closeup(::Gtk::WebIcon.new(me.user_obj.icon, icon_size).tooltip("#{me.user_obj.idname}(#{me.user_obj[:name]})"))
      relation_container.closeup(::Gtk::VBox.new.
                                 closeup(relation).
                                 closeup(::Gtk::HBox.new.
                                         closeup(w_eventbox_image_followed).
                                         closeup(w_followed_label)))
      relation_container.closeup(::Gtk::WebIcon.new(user.icon, icon_size).tooltip("#{user.idname}(#{user[:name]})"))
      if me.user_obj != user
        followbutton = ::Gtk::Button.new
        followbutton.sensitive = false
        # フォローしている状態の更新
        m_following_refresh = lambda { |new|
          if not w_eventbox_image_following.destroyed?
            following = new
            if not w_eventbox_image_following.children.empty?
              w_eventbox_image_following.remove(w_eventbox_image_following.children.first) end

            w_eventbox_image_following.style = w_eventbox_image_following.parent.style
            w_eventbox_image_following.add(::Gtk::WebIcon.new(Skin[new ? 'arrow_following.png' : 'arrow_notfollowing.png'], arrow_size).show_all)
            w_following_label.text = new ? _("フョローしている") : _("フョローしていない")
            followbutton.label = new ? _("解除") : _("フョロー") end }
        # フォローされている状態の更新
        m_followed_refresh = lambda { |new|
          if not w_eventbox_image_followed.destroyed?
            followed = new
            if not w_eventbox_image_followed.children.empty?
              w_eventbox_image_followed.remove(w_eventbox_image_followed.children.first) end
            w_eventbox_image_followed.style = w_eventbox_image_followed.parent.style
            w_eventbox_image_followed.add(::Gtk::WebIcon.new(Skin.get_path(new ? "arrow_followed.png" : "arrow_notfollowed.png"), arrow_size).show_all)
            w_followed_label.text = new ? _("フョローされている") : _("フョローされていない") end }
        me.friendship(target_id: user[:id], source_id: me.user_obj[:id]).next{ |rel|
          if rel and not(w_eventbox_image_following.destroyed?)
            m_following_refresh.call(rel[:following])
            m_followed_refresh.call(rel[:followed_by])
            handler_followings_created = on_followings_created do |service, dst_users|
              if service == me and dst_users.include?(user)
                m_following_refresh.call(true) end end
            handler_followings_destroy = on_followings_destroy do |service, dst_users|
              if service == me and dst_users.include?(user)
                m_following_refresh.call(false) end end
            followbutton.ssc(:clicked){
              followbutton.sensitive = false
              event = following ? :followings_destroy : :followings_created
              me.__send__(following ? :unfollow : :follow, user_id: user.id).next{ |msg|
                Plugin.call(event, me, [user])
                followbutton.sensitive = true unless followbutton.destroyed? }.
              terminate.trap{
                followbutton.sensitive = true unless followbutton.destroyed? }
              true }
            followbutton.signal_connect(:destroy){
              detach(:followings_created, handler_followings_created)
              detach(:followings_destroy, handler_followings_destroy)
              false }
            followbutton.sensitive = true
            relation_container.closeup(followbutton) end
        }.terminate.trap{
          w_following_label.text = _("取得できませんでした") } end
      container.closeup(relation_container) }
    container end

  # ユーザのプロフィールのヘッダ部を返す
  # ==== Args
  # [user] 表示するUser
  # [intent_token] ユーザを開くときに利用するIntent
  # ==== Return
  # ヘッダ部を表すGtkコンテナ
  def profile_head(user, intent_token)
    eventbox = ::Gtk::EventBox.new
    eventbox.ssc('visibility-notify-event'){
      eventbox.style = background_color
      false }

    icon = ::Gtk::EventBox.new.add(::Gtk::WebIcon.new(user.icon_large, UserConfig[:profile_icon_size], UserConfig[:profile_icon_size]).tooltip(_('アイコンを開く')))
    icon.ssc(:button_press_event) do |this, event|
      Plugin.call(:open, user.icon_large)
      true end
    icon.ssc(:realize) do |this|
      this.window.set_cursor(Gdk::Cursor.new(Gdk::Cursor::HAND2))
      false end

    icon_alignment = Gtk::Alignment.new(0.5, 0, 0, 0)
                     .set_padding(*[UserConfig[:profile_icon_margin]]*4)

    eventbox.add(::Gtk::VBox.new(false, 0).
                  add(::Gtk::HBox.new.
                       closeup(icon_alignment.add(icon)).
                       add(::Gtk::VBox.new.closeup(user_name(user, intent_token)).closeup(profile_table(user)))))
  end

  # ユーザ名を表示する
  # ==== Args
  # [user] 表示するUser
  # [intent_token] ユーザを開くときに利用するIntent
  # ==== Return
  # ユーザの名前の部分のGtkコンテナ
  def user_name(user, intent_token)
    w_name = ::Gtk::TextView.new
    w_name.editable = false
    w_name.cursor_visible = false
    w_name.wrap_mode = Gtk::TextTag::WRAP_CHAR
    w_name.ssc(:event) do |this, event|
      if event.is_a? ::Gdk::EventMotion
        this.get_window(::Gtk::TextView::WINDOW_TEXT)
          .set_cursor(::Gdk::Cursor.new(::Gdk::Cursor::XTERM)) end
      false end
    if Gtk::BINDING_VERSION >= [3,1,2]
      tag_sn = w_name.buffer.create_tag('sn', {foreground: '#0000ff',
                                               weight: Pango::Weight::BOLD,
                                               underline: Pango::Underline::SINGLE})
    else
      tag_sn = w_name.buffer.create_tag('sn', {foreground: '#0000ff',
                                               weight: Pango::FontDescription::WEIGHT_BOLD,
                                               underline: Pango::AttrUnderline::SINGLE})
    end
    tag_sn.ssc(:event, &user_screen_name_event_callback(user, intent_token))

    w_name.buffer.insert(w_name.buffer.start_iter, user[:idname], tag_sn)
    w_name.buffer.insert(w_name.buffer.end_iter, "\n#{user[:name]}")
    Gtk::VBox.new.add(w_name) end

  # プロフィールの上のところの格子になってる奴をかえす
  # ==== Args
  # [user] 表示するUser
  # ==== Return
  # プロフィールのステータス部を表すGtkコンテナ
  def profile_table(user)
    _, profile_columns = Plugin.filtering(:user_detail_view_header_columns, user, [
                                            ['tweets',     user[:statuses_count].to_s],
                                            ['favs',       user[:favourites_count].to_s],
                                            ['followings', user[:friends_count]],
                                            ['followers',  user[:followers_count]],
                                          ])
    ::Gtk::Table.new(2, profile_columns.size).tap{|table|
      profile_columns.each_with_index do |column, index|
        key, value = column
        table.
          attach(::Gtk::Label.new(value.to_s).right, 0, 1, index, index+1).
          attach(::Gtk::Label.new(key.to_s)  .left , 1, 2, index, index+1)
      end
    }.set_row_spacing(0, 4).
      set_row_spacing(1, 4).
      set_column_spacing(0, 16)
  end

  def background_color
    style = ::Gtk::Style.new()
    style.set_bg(::Gtk::STATE_NORMAL, 0xFF ** 2, 0xFF ** 2, 0xFF ** 2)
    style end

  def user_screen_name_event_callback(user, intent_token)
    lambda do |tag, textview, event, iter|
      case event
      when ::Gdk::EventButton
        if event.event_type == ::Gdk::Event::BUTTON_RELEASE and event.button == 1
          if intent_token.respond_to?(:forward)
            intent_token.forward
          else
            Plugin.call(:open, user.uri)
          end
          next true
        end
      when ::Gdk::EventMotion
        textview
          .get_window(::Gtk::TextView::WINDOW_TEXT)
          .set_cursor(::Gdk::Cursor.new(::Gdk::Cursor::HAND2))
      end
      false
    end
  end
end
require_if_exist(file) click to toggle source

存在するかわからないrubyファイル file を読み込む。 ただし、file が存在しない場合は例外を投げずにfalseを返す。

# File core/utils.rb, line 88
def require_if_exist(file)
  begin
    require file
    true
  rescue LoadError
    warn "require-if-exist: file not found: #{file}"
    false end end
reset_activity(model) click to toggle source

アクティビティの古い通知を一定時間後に消す

# File core/plugin/activity/activity.rb, line 73
def reset_activity(model)
  Reserver.new(60, thread: Delayer) do
    if not model.destroyed?
      iters = model.to_enum(:each).to_a
      remove_count = iters.size - UserConfig[:activity_max]
      if remove_count > 0
        iters[-remove_count, remove_count].each do |_m,_p,iter|
          @contains_uris.delete(iter[ActivityView::URI])
          model.remove(iter)
        end
      end
      reset_activity(model)
    end
  end
end
result_strict(must, &block) click to toggle source

blockの評価結果がチェックをパスしなかった場合にabortする

# File core/utils.rb, line 201
def result_strict(must, &block)
  result = block.call
  type_strict(result => must)
  result
end
ret_nth(num=0) click to toggle source

num 番目の引数をそのまま返す関数を返す

# File core/utils.rb, line 44
def ret_nth(num=0)
  lambda { |*arg| arg[num] } end
retrieve_user(user_id, services = Service.services.shuffle) click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 22
def retrieve_user(user_id, services = Service.services.shuffle)
  if services.nil? or services.empty?
    return nil end
  service = services.car
  (service/:users/:show).user(user_id: user_id,
                              cache: false).trap{
    retrieve_user(user_id, services.cdr) } end
revision() click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 92
def revision
  begin
    open('|env LC_ALL=C svn info').read.match(/Revision\s*:\s*(\d+)/)[1]
  rescue
    '' end end
rewind(direction, targets) click to toggle source
# File core/plugin/followingcontrol/followingcontrol.rb, line 146
def rewind(direction, targets)
  relation = @relation[direction.to_sym]
  targets.each { |twitter|
    user = twitter.user_obj
    twitter.__send__(direction, cache: :keep, user_id: user.id).next { |users|
      primitive = relation[user]
      if primitive and not primitive.empty?
        created = users - primitive
        Plugin.call("#{direction}_created".to_sym, twitter, created) if not created.empty?
        destroyed = primitive - users
        Plugin.call("#{direction}_destroy".to_sym, twitter, destroyed) if not destroyed.empty?
      else
        relation[user] = users
        Plugin.call("#{direction}_modified".to_sym, twitter, users)
      end
    }
  }
end
rewind_timeline(saved_search) click to toggle source

タイムラインを更新する

Args

saved_search

saved search

# File core/plugin/saved_search/saved_search.rb, line 69
def rewind_timeline(saved_search)
  type_strict saved_search => Plugin::SavedSearch::SavedSearch
  saved_search.service.search(q: saved_search.query, count: 100).next{ |res|
    timeline(saved_search.slug) << res if res.is_a? Array
  }.trap{ |e|
    timeline(saved_search.slug) << Mikutter::System::Message.new(description: _("更新中にエラーが発生しました (%{error})") % {error: e.to_s}) } end
rotten?() click to toggle source
# File core/plugin/display_requirements/display_requirements.rb, line 121
def rotten?
end
saving_rule_checkbox(dialog, intent_token_builder, model_slug) click to toggle source
# File core/plugin/intent_selector/intent_selector.rb, line 84
def saving_rule_checkbox(dialog, intent_token_builder, model_slug)
  save_check = Gtk::CheckButton.new(_('次回から、次の内容から始まるURLはこの方法で開く'))
  rule = Gtk::Entry.new.set_text(intent_token_builder[:uri].to_s)
  rule.sensitive = false
  save_check.ssc(:toggled) do |widget|
    rule.sensitive = widget.active?
    false
  end
  dialog.ssc(:response) do |w, response_id|
    if response_id == Gtk::Dialog::RESPONSE_OK and intent_token_builder[:intent] and save_check.active?
      add_intent_rule(intent: intent_token_builder[:intent],
                      str: rule.text,
                      rule: 'start',
                      model_slug: model_slug)
    end
    false
  end
  dialog.vbox.
    closeup(save_check).
    closeup(rule)
end
scan(slug, messages) click to toggle source

messagesの中で、タイムライン slug に入れるべきものがあれば入れる

Args

slug

タイムラインスラッグ

messages

入れるMessageの配列

# File core/plugin/smartthread/smartthread.rb, line 13
def scan(slug, messages)
  seeds = @timelines[slug]
  i_timeline = timeline(slug)
  if i_timeline and seeds
    SerialThread.new do
      messages.each{ |message|
        message.each_ancestor { |cur|
          if seeds.include? cur
            i_timeline << message end } } end end end
score_by_entity(tweet) click to toggle source
# File core/plugin/twitter/twitter.rb, line 370
def score_by_entity(tweet)
  score = Array.new
  cur = 0
  text = tweet.description
  tweet[:entities].flat_map{|kind, entities|
    case kind
    when :hashtags
      entity_hashtag(tweet, entities)
    when :urls
      entity_urls(tweet, entities)
    when :user_mentions
      entitiy_users(tweet, entities)
    when :symbols
    # 誰得
    when :media
      entity_media(tweet, entities)
    end
  }.compact.sort_by{|range, _|
    range.first
  }.each do |range, note|
    if range.first != cur
      score << text_note(
        description: text[cur...range.first])
    end
    score << note
    cur = range.last
  end
  if cur == 0
    return [text_note(description: text)]
  end
  if cur != text.size
    score << text_note(
      description: text[cur...text.size])
  end
  score
end
score_by_hashtag_regexp(text) click to toggle source
# File core/plugin/twitter/twitter.rb, line 517
def score_by_hashtag_regexp(text)
  score_by_regexp(text,
                  pattern: /(?:#|#)[a-zA-Z0-9_]+/,
                  reference_generator: ->(name){ Plugin::Twitter::HashTag.new(name: name) },
                  uri_generator: ->(name){ "https://twitter.com/hashtag/#{CGI.escape(name)}" })
end
score_by_regexp(text, score=Array.new, pattern:, reference_generator:, uri_generator:) click to toggle source
# File core/plugin/twitter/twitter.rb, line 524
def score_by_regexp(text, score=Array.new, pattern:, reference_generator:, uri_generator:)
  lead, target, trail = text.partition(pattern)
  score << text_note(description: lead)
  if !(target.empty? || trail.empty?)
    trim = target[1, target.size]
    puts({trim: trim, target: target})
    score << Diva::Model(:score_hyperlink).new(
      description: target,
      uri: uri_generator.(trim),
      reference: reference_generator.(trim))
    score_by_regexp(trail, score,
                    pattern: pattern,
                    reference_generator: reference_generator,
                    uri_generator: uri_generator)
  else
    score
  end
end
score_by_screen_name_regexp(text) click to toggle source
# File core/plugin/twitter/twitter.rb, line 510
def score_by_screen_name_regexp(text)
  score_by_regexp(text,
                  pattern: Plugin::Twitter::Message::MentionMatcher,
                  reference_generator: ->(name){ Plugin::Twitter::User.findbyidname(name, Diva::DataSource::USE_LOCAL_ONLY) },
                  uri_generator: ->(name){ "https://twitter.com/#{CGI.escape(name)}" })
end
send() click to toggle source
# File core/plugin/bugreport/bugreport.rb, line 61
def send
  Thread.new{
    begin
      exception = crashed_exception
      m = exception.backtrace.first.match(/(.+?):(\d+)/)
      crashed_file, crashed_line = m[1], m[2]
      param = {
        'backtrace' => JSON.generate(exception.backtrace.map{ |msg| msg.gsub(FOLLOW_DIR, '{MIKUTTER_DIR}') }),
        'file' => crashed_file.gsub(FOLLOW_DIR, '{MIKUTTER_DIR}'),
        'line' => crashed_line,
        'exception_class' => exception.class,
        'description' => exception.to_s,
        'ruby_version' => RUBY_VERSION,
        'rubygtk_version' => Gtk::BINDING_VERSION.join('.'),
        'platform' => RUBY_PLATFORM,
        'url' => 'exception',
        'version' => Environment::VERSION }
      Net::HTTP.start('mikutter.hachune.net', 4567){ |http|
        console = mikutter_error
        param['stderr'] = console if console
        eparam = encode_parameters(param)
        http.post('/', eparam) }
      File.delete(File.expand_path(File.join(Environment::TMPDIR, 'mikutter_error'))) rescue nil
      File.delete(File.expand_path(File.join(Environment::TMPDIR, 'crashed_exception'))) rescue nil
      Plugin.activity :system, _("エラー報告を送信しました。ありがとう♡")
      Plugin.call :send_bugreport, param
    rescue Timeout::Error, StandardError => e
      Plugin.activity :system, _("ピャアアアアアアアアアアアアアアアアアアアアアアアwwwwwwwwwwwwwwwwwwwwww")
      Plugin.activity :error, e.to_s, exception: e
    end } end
sequence() click to toggle source

茶番オブジェクトを新しく作る

# File core/plugin/guide/guide.rb, line 4
def sequence
  # 茶番でしか使わないクラスなので、チュートリアル時だけロードする
  require_relative 'interactive'
  Plugin::Guide::Interactive.generate end
service_by_user_id(user_id) click to toggle source

ユーザIDから対応する Service を返す

Args

user_id

ユーザID

Return

Service か、見つからなければnil

# File core/plugin/saved_search/saved_search.rb, line 134
def service_by_user_id(user_id)
  Enumerator.new{|y|
    Plugin.filtering(:worlds, y)
  }.lazy.select{ |world|
    world.class.slug == :twitter
  }.find{ |twitter|
    twitter.user_obj.id == user_id
  }
end
service_register(service) click to toggle source

service を監視対象に入れる

Args

service

監視するservice

# File core/plugin/followingcontrol/followingcontrol.rb, line 168
def service_register(service)
  @activating_services << service
  user = service.user_obj
  Deferred.when(service.followings(cache: true, user_id: user[:id]),
                service.followers(cache: true, user_id: user[:id])).next { |followings, followers|
    @relation.followings[user] = followings
    @relation.followers[user] = followers
    notice "#{user} has #{followings.size} followee(s)."
    notice "#{user} has #{followers.size} follower(s)."
    Plugin.call(:followings_modified, service, @relation.followings[user])
    Plugin.call(:followers_modified, service, @relation.followers[user])
    @activating_services.delete(service)
  }.trap { |err|
    error err
    @activating_services.delete(service)
  }
end
set_available_lists(service, newlist) click to toggle source

service がフォローしているリストを新しく設定する

Args

service

Service リストのフォロイー TODO: これ実装する

newlist

Enumerable 新しいリスト

Return

self

# File core/plugin/list/list.rb, line 162
def set_available_lists(service, newlist)
  newlist_ary = newlist.to_a
  available_list_of_service = available_lists(service).to_a
  created = (newlist_ary - available_list_of_service).freeze
  deleted = (available_list_of_service - newlist_ary).freeze
  Plugin.call(:list_created, service, created) if not created.empty?
  Plugin.call(:list_destroy, service, deleted) if not deleted.empty?
  @available_lists[service.user_obj] = newlist_ary.freeze
  @all_available_lists = nil
  Plugin.call(:list_data, service, available_lists(service)) if not(created.empty? and deleted.empty?)
  self end
setting_container() click to toggle source

設定のGtkウィジェット

# File core/plugin/list_settings/list_settings.rb, line 13
def setting_container
  tab = Plugin::ListSettings::Tab.new(self)
  available_lists.each{ |list|
    iter = tab.model.append
    iter[Plugin::ListSettings::Tab::SLUG] = list[:full_name]
    iter[Plugin::ListSettings::Tab::LIST] = list
    iter[Plugin::ListSettings::Tab::NAME] = list[:name]
    iter[Plugin::ListSettings::Tab::DESCRIPTION] = list[:description]
    iter[Plugin::ListSettings::Tab::PUBLICITY] = list[:mode] }
  Gtk::HBox.new.add(tab).closeup(tab.buttons(Gtk::VBox)).show_all end
setting_window() click to toggle source
# File core/plugin/settings/settings.rb, line 21
def setting_window
  return @window if defined?(@window) and @window
  @window = window = ::Gtk::Window.new(_('設定'))
  window.set_size_request(320, 240)
  window.set_default_size(640, 480)
  menu = Plugin::Settings::Menu.new
  settings = ::Gtk::VBox.new
  scrolled = ::Gtk::ScrolledWindow.new.set_hscrollbar_policy(::Gtk::POLICY_NEVER)

  menu.ssc(:cursor_changed) do
    if menu.selection.selected
      active_iter = menu.selection.selected
      if active_iter
        settings.hide
        settings.children.each(&settings.method(:remove))
        settings.closeup(active_iter[Plugin::Settings::Menu::COL_RECORD].widget).show_all
      end
    end
    false
  end

  window.ssc(:destroy) {
    @window = nil
    false
  }

  scrolled_menu = ::Gtk::ScrolledWindow.new.set_policy(::Gtk::POLICY_NEVER, ::Gtk::POLICY_AUTOMATIC)

  window.add(::Gtk::HPaned.new.add1(scrolled_menu.add_with_viewport(menu)).add2(scrolled.add_with_viewport(settings)))
end
show_message(message, token, force=false) click to toggle source
# File core/plugin/message_detail_view/message_detail_view.rb, line 22
def show_message(message, token, force=false)
  slug = "message_detail_view-#{message.uri}".to_sym
  if !force and Plugin::GUI::Tab.exist?(slug)
    Plugin::GUI::Tab.instance(slug).active!
  else
    container = Gtk::DivaHeaderWidget.new(message, intent_token: token)
    i_cluster = tab slug, _("詳細タブ") do
      set_icon Skin['message.png']
      set_deletable true
      temporary_tab
      shrink
      nativewidget container
      expand
      cluster nil end
    Thread.new {
      Plugin.filtering(:message_detail_view_fragments, [], i_cluster, message).first
    }.next { |tabs|
      tabs.map(&:last).each(&:call)
    }.next {
      if !force
        i_cluster.active! end
    }.trap{ |exc|
      error exc
    }
  end
end
show_profile(user, token, force=false) click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 43
def show_profile(user, token, force=false)
  slug = "profile-#{user.uri}".to_sym
  if !force and Plugin::GUI::Tab.exist?(slug)
    Plugin::GUI::Tab.instance(slug).active!
  else
    UserConfig[:profile_opened_tabs] = ((UserConfig[:profile_opened_tabs] || []) + [user.id]).uniq
    container = profile_head(user, token)
    i_cluster = tab slug, _("%{user} のプロフィール") % {user: user[:name]} do
      set_icon user.icon
      set_deletable true
      shrink
      nativewidget container
      expand
      cluster nil end
    Thread.new {
      Plugin.filtering(:user_detail_view_fragments, [], i_cluster, user).first
    }.next { |tabs|
      tabs.map(&:last).each(&:call)
    }.next {
      Plugin.call(:filter_stream_reconnect_request)
      if !force
        i_cluster.active! end
    }.terminate(_("%{user} のプロフィールの取得中にエラーが発生しました。見るなってことですかね。") % {user: user.name})
  end end
skin_infos() click to toggle source

スキンの情報を得る

# File core/plugin/skin_setting_gtk/skin_setting_gtk.rb, line 42
def skin_infos()
  default_info = { :vanilla => { :face => _("(デフォルト)"), :dir => Skin::default_dir } }

  skin_infos_tmp = skin_list.inject({}) { |hash, _|
    hash[_] = { :face => _, :dir => File.join(Skin::SKIN_ROOT, _) }
    hash
  }

  default_info.merge(skin_infos_tmp)
end
skin_list() click to toggle source

インストール済みスキンのリスト

# File core/plugin/skin_setting_gtk/skin_setting_gtk.rb, line 29
def skin_list()
  dirs = Dir.glob(File.join(Skin::SKIN_ROOT, "*")).select { |_|
    File.directory?(_)
  }.select { |_|
    Dir.glob(File.join(_, "*.png")).length != 0
  }.map { |_|
    _.gsub(/^#{Skin::SKIN_ROOT}\//, "")
  }

  dirs
end
slide_timeline_focus(src, dest) click to toggle source

タイムライン src で選択されているディスプレイ上のy座標が同じ dest のツイートに フォーカスを移動する

Args

src

フォーカスを取得するタイムライン

dest

フォーカスを設定するタイムライン

# File core/plugin/command/command.rb, line 302
def slide_timeline_focus(src, dest)
  type_strict src => Plugin::GUI::Timeline, dest => Plugin::GUI::Timeline
  y = Plugin.filtering(:gui_timeline_cursor_position, src, nil).last
  if y
    Plugin.call(:gui_timeline_move_cursor_to, dest, y) end end
spec_generate(dir) click to toggle source
# File core/boot/shell/spec.rb, line 77
def spec_generate(dir)
  dir = File.expand_path(dir)
  specfile = File.join(dir, ".mikutter.yml")
  legacy_specfile = File.join(dir, "spec")
  spec = if FileTest.exist?(specfile)
           YAML.load_file(specfile)
         elsif FileTest.exist?(legacy_specfile)
           YAML.load_file(legacy_specfile)
         else
           user = UserConfig[:verify_credentials] || {}
           idname = user[:idname]
           {"slug" => File.basename(dir).to_sym, "depends" => {"mikutter" => Environment::VERSION.to_s}, "version" => "1.0", "author" => idname} end
  slug = spec["slug"].to_sym
  basefile = File.join(dir, "#{slug}.rb")
  unless FileTest.exist? basefile
    puts "file #{basefile} notfound. select plugin slug."
    expects = Dir.glob(File.join(dir, "*.rb")).map{ |filename| File.basename(filename, '.rb') }
    if expects.empty?
      puts "please create #{basefile}."
    end
    expects.each_with_index{ |filename, index|
      puts "[#{index}] #{filename}"
    }
    print "input number or slug [q:quit, s:skip]> "
    number = STDIN.gets.chomp
    case number
    when /q/i
      abort
    when /s/i
      return
    when /\A[0-9]+\Z/
      slug = expects[number.to_i].to_sym
    else
      slug = number.to_sym end
    spec["slug"] = slug
    basefile = File.join(dir, "#{slug}.rb") end
  source = File.open(basefile){ |io| io.read }

  if not spec.has_key?("name")
    print "#{slug}: name> "
    spec["name"] = STDIN.gets.chomp end
  if not spec.has_key?("description")
    print "#{slug}: description> "
    spec["description"] = STDIN.gets.chomp end
  spec["depends"] = {"version" => "1.0", "plugin" => []} if not spec.has_key?("depends")
  spec["depends"]["plugin"] = [] if not spec["depends"].has_key?("plugin")
  depend = Depend.new(source).set_spec(spec)
  depend.parse
  content = YAML.dump(depend.spec)
  File.open(specfile, "w"){ |io| io.write content }
  puts content
end
specified_model_slug(model) click to toggle source

model のmodel slugを文字列で得る。

Args

model

Diva::Modelのインスタンス又はnil

Return

String

Modelのslug。 model がnilだった場合は空文字列

# File core/plugin/intent_selector/intent_selector.rb, line 134
def specified_model_slug(model)
  model ? model.class.slug.to_s : ''
end
start() click to toggle source
# File core/plugin/rest/rest.rb, line 24
def start
  Delayer.new do
    if Service.instances.empty?
      @account_observer ||= on_world_after_created do |_new_world|
        start
        @account_observer.detach
        @account_observer = nil
      end
    else
      Service.instances.each do |service|
        @crawlers.each{ |s| s.call(service) }
      end
      Reserver.new(60, thread: Delayer) do
        start
      end
    end
  end
end
streamerror(exception) click to toggle source
# File core/plugin/streaming/filter.rb, line 70
def streamerror(exception)
  @success_flag = false
  @fail.notify(exception) end
tab_update_icon(i_tab) click to toggle source
# File core/plugin/gtk/gtk.rb, line 526
def tab_update_icon(i_tab)
  type_strict i_tab => Plugin::GUI::TabLike
  tab = widgetof(i_tab)
  if tab
    tab.tooltip(i_tab.name)
    tab.remove(tab.child) if tab.child
    if i_tab.icon
      tab.add(::Gtk::WebIcon.new(i_tab.icon, 24, 24).show)
    else
      tab.add(::Gtk::Label.new(i_tab.name).show) end end
  self end
tcor(*types) click to toggle source

types のうちいずれかとis_a?関係ならtrueを返すProcオブジェクトを返す

# File core/utils.rb, line 188
def tcor(*types)
  lambda{ |v| types.any?{ |c| v.is_a?(c) } } end
text_note(description:) click to toggle source

TextNoteを作成する。 description: から実体参照をアンエスケープした文字列を使ってText Noteを作る。 Message#descriptionの結果が実体参照をエスケープすると Entityのインデックスがずれるので、このメソッドで行う。

# File core/plugin/twitter/twitter.rb, line 547
def text_note(description:)
  Diva::Model(:score_text).new(description: description.gsub(Plugin::Twitter::Message::DESCRIPTION_UNESCAPE_REGEXP, &Plugin::Twitter::Message::DESCRIPTION_UNESCAPE_RULE))
end
the_day?() click to toggle source
# File core/plugin/aspectframe/aspectframe.rb, line 92
def the_day?
  Plugin::AspectFrame::THE_DAY.cover?(now) end
timeline_destroy_event(i_timeline) click to toggle source

Timelineウィジェットのdestroyのコールバックを返す

Args

i_timeline

タイムラインのインターフェイス

Return

Proc

# File core/plugin/gtk/gtk.rb, line 210
def timeline_destroy_event(i_timeline)
  lambda { |this|
    i_timeline.destroy
    false } end
timeline_focus_in_event(i_timeline) click to toggle source

Timelineウィジェットのfocus_in_eventのコールバックを返す

Args

i_timeline

タイムラインのインターフェイス

Return

Proc

# File core/plugin/gtk/gtk.rb, line 190
def timeline_focus_in_event(i_timeline)
  lambda { |this, event|
    if this.focus?
      i_timeline.active!(true, true) end
    false } end
timeline_key_press_event(i_timeline) click to toggle source

Timelineウィジェットのkey_press_eventのコールバックを返す

Args

i_timeline

タイムラインのインターフェイス

Return

Proc

# File core/plugin/gtk/gtk.rb, line 201
def timeline_key_press_event(i_timeline)
  lambda { |widget, event|
    Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_timeline) } end
timeline_storage() click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 12
def timeline_storage # {slug: user}
  @timeline_storage ||= {} end
timelines() click to toggle source

id => SavedSearch

# File core/plugin/saved_search/saved_search.rb, line 43
def timelines
  @timelines ||= {} end
transform(icon) click to toggle source
# File core/plugin/aspectframe/aspectframe.rb, line 79
def transform(icon)
  if icon.perma_link
    Enumerator.new{|y|
      Plugin.filtering(:photo_filter, "http://toshia.dip.jp/img/api/#{Digest::MD5.hexdigest(icon.perma_link.to_s)[0,2].upcase}.png", y)
    }.first
  else
    icon
  end
end
trim_hidden_header(text) click to toggle source

文字列からhidden headerを除いた文字列を返す。 hidden headerが含まれていない場合は、 text を返す。

# File core/plugin/twitter/twitter.rb, line 259
def trim_hidden_header(text)
  return text unless UserConfig[:auto_populate_reply_metadata]
  mentions = text.match(%r[\A((?:@[a-zA-Z0-9_]+\s+)+)])
  forecast_receivers_sn = Set.new
  if reply?
    @to.first.each_ancestor.each do |m|
      forecast_receivers_sn << m.user.idname
      forecast_receivers_sn.merge(m.receive_user_screen_names)
    end
  end
  if mentions
    specific_screen_names = Set.new(mentions[1].split(/\s+/).map{|s|s[1, s.size]})
    [*(specific_screen_names - forecast_receivers_sn).map{|s|"@#{s}"}, text[mentions.end(0),text.size]].join(' '.freeze)
  else
    text
  end
end
trim_hidden_regions(text) click to toggle source
# File core/plugin/twitter/twitter.rb, line 253
def trim_hidden_regions(text)
  trim_hidden_header(trim_hidden_footer(text))
end
type_check(args, &proc) click to toggle source

引数のチェックをすべてパスした場合のみブロックを実行する チェックに引っかかった項目があればwarnを出力してブロックは実行せずにnilを返す。 チェックはassocできる配列か、Hashで定義する。

type_check(value => nil,              # チェックしない(常にパス)
           value => Module,           # その型とis_a?関係ならパス
           value => [:method, *args], # value.method(*args)が真を返せばパス
           value => lambda{ |x| ...}) # xにvalueを渡して実行し、真を返せばパス

チェックをすべてパスしたかどうかを真偽値で返す。 ブロックが指定されていれば、それを実行してブロックの実行結果を返す メモ: いずれかのタイプに一致するチェックを定義するにはtcorを使う

type_check object => tcor(Array, Hash)
# File core/utils.rb, line 164
def type_check(args, &proc)
  check_function = lambda{ |val, check|
    if not check
      true
    elsif check.is_a? Array
      val.__send__(*check)
    elsif check.is_a? Symbol
      val.respond_to?(check)
    elsif check.is_a?(Class) or check.is_a?(Module)
      val.is_a?(check)
    elsif check.respond_to?(:call)
      check.call(val) end }
  error = args.find{ |a| not(check_function.call(*a)) }
  if(error)
    warn "argument error: #{error[0].inspect} is not passed #{error[1].inspect}"
    warn "in #{caller_util}"
    false
  else
    if proc
      proc.call
    else
      true end end end
type_strict(args, &proc) click to toggle source

#type_checkと同じだが、チェックをパスしなかった場合にabortする #type_checkの戻り値を返す

# File core/utils.rb, line 193
def type_strict(args, &proc)
  result = type_check(args, &proc)
  if not result
    into_debug_mode(binding)
    raise ArgumentError.new end
  result end
user_name(user, intent_token) click to toggle source

ユーザ名を表示する

Args

user

表示するUser

intent_token

ユーザを開くときに利用するIntent

Return

ユーザの名前の部分のGtkコンテナ

# File core/plugin/user_detail_view/user_detail_view.rb, line 296
def user_name(user, intent_token)
  w_name = ::Gtk::TextView.new
  w_name.editable = false
  w_name.cursor_visible = false
  w_name.wrap_mode = Gtk::TextTag::WRAP_CHAR
  w_name.ssc(:event) do |this, event|
    if event.is_a? ::Gdk::EventMotion
      this.get_window(::Gtk::TextView::WINDOW_TEXT)
        .set_cursor(::Gdk::Cursor.new(::Gdk::Cursor::XTERM)) end
    false end
  if Gtk::BINDING_VERSION >= [3,1,2]
    tag_sn = w_name.buffer.create_tag('sn', {foreground: '#0000ff',
                                             weight: Pango::Weight::BOLD,
                                             underline: Pango::Underline::SINGLE})
  else
    tag_sn = w_name.buffer.create_tag('sn', {foreground: '#0000ff',
                                             weight: Pango::FontDescription::WEIGHT_BOLD,
                                             underline: Pango::AttrUnderline::SINGLE})
  end
  tag_sn.ssc(:event, &user_screen_name_event_callback(user, intent_token))

  w_name.buffer.insert(w_name.buffer.start_iter, user[:idname], tag_sn)
  w_name.buffer.insert(w_name.buffer.end_iter, "\n#{user[:name]}")
  Gtk::VBox.new.add(w_name) end
user_screen_name_event_callback(user, intent_token) click to toggle source
# File core/plugin/user_detail_view/user_detail_view.rb, line 350
def user_screen_name_event_callback(user, intent_token)
  lambda do |tag, textview, event, iter|
    case event
    when ::Gdk::EventButton
      if event.event_type == ::Gdk::Event::BUTTON_RELEASE and event.button == 1
        if intent_token.respond_to?(:forward)
          intent_token.forward
        else
          Plugin.call(:open, user.uri)
        end
        next true
      end
    when ::Gdk::EventMotion
      textview
        .get_window(::Gtk::TextView::WINDOW_TEXT)
        .set_cursor(::Gdk::Cursor.new(::Gdk::Cursor::HAND2))
    end
    false
  end
end
using?(list) click to toggle source

list が抽出タブで使われていて、更新を要求されているなら真を返す

Args

list

Diva::Model 調べるリスト

Return

使われていたら真

# File core/plugin/list/list.rb, line 129
def using?(list)
  using_lists.include? list end
using_lists() click to toggle source

現在データソースで使用されているリストを返す

Return

Enumerable データソースで使われているリスト

# File core/plugin/list/list.rb, line 113
def using_lists
  list_ids = Set.new Plugin.filtering(:extract_tabs_get, []).first.map{|tab|
    tab[:sources]
  }.select{ |sources|
    sources.is_a? Enumerable
  }.inject(Set.new, &:merge).map{ |source|
    $1.to_i if source.to_s =~ /[a-zA-Z0-9_]+_list_(\d+)/
  }.compact
  available_lists.lazy.select do |list|
    list_ids.include? list[:id] end end
warn(msg) click to toggle source

警告メッセージを表示する。

# File core/utils.rb, line 113
def warn(msg)
  log "warning", msg if Mopt.error_level >= 2
end
where_should_insert_it(insertion, src, order) click to toggle source

insertion を、 src の挿入するべき場所のインデックスを返す。 order は順番を表す配列で、 src 内のオブジェクトの前後関係を表す。 order 内に insertion が存在しない場合は一番最後のインデックスを返す

# File core/utils.rb, line 99
def where_should_insert_it(insertion, src, order)
  if(order.include?(insertion)) then
    src.dup.push(insertion).sort_by{|a|
      order.index(a) or 65536
    }.index(insertion)
  else
    src.size end end
widget_join_tab(i_tab, widget) click to toggle source

タブ tabwidget を入れる

Args

i_tab

タブ

widget

Gtkウィジェット

# File core/plugin/gtk/gtk.rb, line 497
def widget_join_tab(i_tab, widget)
  tab = widgetof(i_tab)
  return false if not tab
  i_pane = i_tab.parent
  return false if not i_pane
  pane = widgetof(i_pane)
  return false if not pane
  is_tab = i_tab.is_a?(Plugin::GUI::Tab)
  has_child = is_tab and
    not(i_tab.temporary_tab?) and
    not(i_tab.children.any?{ |child|
          not child.is_a? Plugin::GUI::TabToolbar })
  if has_child
    Plugin.call(:rewind_window_order, i_pane.parent) end
  container_index = pane.get_tab_pos_by_tab(tab)
  if container_index
    container = pane.get_nth_page(container_index)
    if container
      return container.pack_start(widget, i_tab.pack_rule[container.children.size]) end end
  if tab.parent
    raise Plugin::Gtk::GtkError, "Gtk Widget #{tab.inspect} of Tab(#{i_tab.slug.inspect}) has parent Gtk Widget #{tab.parent.inspect}" end
  container = ::Gtk::TabContainer.new(i_tab).show_all
  container.ssc(:key_press_event){ |w, event|
    Plugin::GUI.keypress(::Gtk::keyname([event.keyval ,event.state]), i_tab) }
  container.pack_start(widget, i_tab.pack_rule[container.children.size])
  pane.append_page(container, tab)
  pane.set_tab_reorderable(container, true).set_tab_detachable(container, true)
  true end
widgetof(cuscadable) click to toggle source

cuscadable に対応するGtkオブジェクトを返す

Args

cuscadable

ウィンドウ、ペイン、タブ、タイムライン等

Return

対応するGtkオブジェクト

# File core/plugin/gtk/gtk.rb, line 597
def widgetof(cuscadable)
  type_strict cuscadable => :slug
  result = @slug_dictionary.get(cuscadable)
  if result and result.destroyed?
    nil
  else
    result end end
worlds() click to toggle source

すべてのWorld Modelを順番通りに含むArrayを返す。 各要素は、アカウントの順番通りに格納されている。 外部からこのメソッド相当のことをする場合は、 worlds フィルタを利用すること。

Return

Array

アカウントModelを格納したArray

# File core/plugin/world/world.rb, line 48
def worlds
  if @worlds
    @worlds
  else
    atomic do
      load_world_ifn
    end
  end
end
yamlisp(node) click to toggle source
# File core/miku/yamlisp.rb, line 5
def yamlisp(node)
  if(node.is_a? YamLisp::Node) then
    node.yamlisp_eval
  else
    node
  end
end