PageRenderTime 47ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/termtter/client.rb

https://github.com/Epictetus/termtter
Ruby | 384 lines | 302 code | 61 blank | 21 comment | 19 complexity | b629052e600a3d6394e8a5e68cf93ebd MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. require 'fileutils'
  3. require 'logger'
  4. require 'termcolor'
  5. module Termtter
  6. class CommandNotFound < StandardError; end
  7. class CommandCanceled < StandardError; end
  8. module Client
  9. include Termtter::Hookable
  10. @commands = {}
  11. @filters = []
  12. @since_id = nil
  13. Thread.abort_on_exception = true
  14. class << self
  15. attr_reader :commands, :logger, :task_manager
  16. # call-seq:
  17. # plug :: Name -> (Hash) -> IO () where NAME = String | Symbol | [NAME]
  18. def plug(name, options = {})
  19. if Array === name # Obviously `name.respond_to?(:each)` is better, but for 1.8.6 compatibility we cannot.
  20. name.each {|i| plug(i, options) }
  21. return
  22. end
  23. name = name.to_s
  24. return if config.system.disable_plugins.include?(name.gsub('defaults/', ''))
  25. name_sym = name.gsub(/-/, '_').to_sym
  26. options.each do |key, value|
  27. config.plugins.__refer__(name_sym).__assign__(key.to_sym, value)
  28. end
  29. load "plugins/#{name}.rb"
  30. rescue Exception => e
  31. Termtter::Client.handle_error(e)
  32. end
  33. def public_storage
  34. @public_storage ||= {}
  35. end
  36. def memory_cache
  37. @memory_cache ||= Termtter::MemoryCache.new
  38. end
  39. def add_filter(&b)
  40. warn "add_filter method will be removed. Use Termtter::Client.register_hook(:name => ..., :point => :filter_for_output, :exec => ... ) instead."
  41. @filters << b
  42. end
  43. def clear_filter
  44. @filters.clear
  45. end
  46. def register_command(arg, opts = {}, &block)
  47. command = case arg
  48. when Command
  49. arg
  50. when Hash
  51. Command.new(arg)
  52. when String, Symbol
  53. options = { :name => arg }
  54. options.merge!(opts)
  55. options[:exec_proc] = block
  56. Command.new(options)
  57. else
  58. raise ArgumentError, 'must be given Termtter::Command, Hash or String(Symbol) with block'
  59. end
  60. @commands[command.name] = command
  61. end
  62. def remove_command(name)
  63. commands.delete(name.to_sym)
  64. end
  65. def add_command(name)
  66. if block_given?
  67. command = Command.new(:name => name)
  68. yield command
  69. @commands[command.name] = command
  70. else
  71. raise ArgumentError, 'must be given block to set parameters'
  72. end
  73. end
  74. def clear_command
  75. @commands.clear
  76. end
  77. # MEMO: attr_reader :commands してるからこれいらない気もする
  78. def get_command(name)
  79. @commands[name]
  80. end
  81. def register_macro(name, macro, options = {})
  82. command = {
  83. :name => name.to_sym,
  84. :exec_proc => lambda {|arg| execute(macro % arg)}
  85. }.merge(options)
  86. register_command(command)
  87. end
  88. # statuses => [status, status, ...]
  89. # status => {
  90. # :id => status id,
  91. # :created_at => created time,
  92. # :user_id => user id,
  93. # :name => user name,
  94. # :screen_name => user screen_name,
  95. # :source => source,
  96. # :reply_to => reply_to status id,
  97. # :text => status,
  98. # :original_data => original data,
  99. # }
  100. def output(statuses, event = :default)
  101. return if statuses.nil? || statuses.empty?
  102. event = Termtter::Event.new(event) unless event.kind_of? Termtter::Event
  103. statuses = statuses.sort_by(&:id)
  104. call_hooks(:pre_filter, statuses, event)
  105. filtered = apply_filters_for_hook(:filter_for_output, statuses.map(&:clone), event)
  106. @filters.each do |f| # TODO: code for compatibility. delete someday.
  107. # but... when is the "someday"?
  108. filtered = f.call(filtered, event)
  109. end
  110. call_hooks(:post_filter, filtered, event)
  111. get_hooks(:output).each do |hook|
  112. Termtter::Client.logger.debug { "output: call hook :output #{hook.inspect}" }
  113. hook.call(
  114. apply_filters_for_hook(:"filter_for_#{hook.name}", filtered, event),
  115. event)
  116. end
  117. Termtter::Client.logger.debug "output: call hook :output, done"
  118. end
  119. def notify(*args)
  120. ::Notify.notify(*args)
  121. end
  122. def apply_filters_for_hook(hook_name, statuses, event)
  123. get_hooks(hook_name).inject(statuses) {|s, hook|
  124. hook.call(s, event)
  125. }
  126. end
  127. def execute(text)
  128. text = text.strip
  129. @task_manager.invoke_and_wait do
  130. # FIXME: This block can become Maybe Monad
  131. get_hooks("pre_command").each {|hook|
  132. break if text == nil # interrupt if hook returns nil
  133. text = hook.call(text)
  134. }
  135. return if text.empty?
  136. command = find_command(text)
  137. raise CommandNotFound, text unless command
  138. command_str, modified_arg = command.split_command_line(text)
  139. command_str.strip!
  140. modified_arg ||= ''
  141. # FIXME: This block can become Maybe Monad
  142. get_hooks("modify_arg_for_#{command.name.to_s}").each {|hook|
  143. break if modified_arg == false # interrupt if hook return false
  144. modified_arg.strip!
  145. modified_arg = hook.call(command_str, modified_arg) || ''
  146. }
  147. modified_arg.strip!
  148. begin
  149. call_hooks("pre_exec_#{command.name.to_s}", command, modified_arg)
  150. result = command.call(command_str, modified_arg, text) # exec command
  151. call_hooks("post_exec_#{command.name.to_s}", command_str, modified_arg, result)
  152. call_hooks("post_command", text)
  153. rescue CommandCanceled
  154. return false
  155. end
  156. return true
  157. end
  158. rescue TimeoutError
  159. call_hooks("timeout", text)
  160. raise
  161. end
  162. def command_exists?(text)
  163. @commands.values.any? {|command| command.match?(text) }
  164. end
  165. def find_command(text)
  166. @commands.
  167. values.
  168. select {|command| command.match?(text) }.
  169. sort_by {|command| command.name.to_s.split(' ').size }.
  170. last
  171. end
  172. def pause
  173. @task_manager.pause
  174. end
  175. def resume
  176. @task_manager.resume
  177. end
  178. def add_task(*arg, &block)
  179. @task_manager.add_task(*arg, &block)
  180. end
  181. def delete_task(name)
  182. @task_manager.delete_task(name)
  183. end
  184. def exit
  185. puts 'finalizing...'
  186. call_hooks(:exit)
  187. @task_manager.kill
  188. end
  189. def load_config
  190. legacy_config_support() if File.exist? Termtter::CONF_DIR
  191. unless File.exist?(Termtter::CONF_FILE)
  192. require 'termtter/config_setup'
  193. ConfigSetup.run
  194. end
  195. load Termtter::CONF_FILE
  196. end
  197. def legacy_config_support
  198. case File.ftype(File.expand_path('~/.termtter'))
  199. when 'directory'
  200. # nop
  201. when 'file'
  202. move_legacy_config_file
  203. end
  204. end
  205. # MEMO: This method will be removed in Termtter 2.0.0
  206. def move_legacy_config_file
  207. FileUtils.mv(
  208. Termtter::CONF_DIR,
  209. File.expand_path('~/.termtter___'))
  210. Dir.mkdir(Termtter::CONF_DIR)
  211. FileUtils.mv(
  212. File.expand_path('~/.termtter___'),
  213. Termtter::CONF_FILE)
  214. end
  215. def logger
  216. @logger || setup_logger
  217. end
  218. def setup_logger
  219. @logger = config.logger || default_logger
  220. @logger.level = config.devel ? Logger::DEBUG : Logger::INFO
  221. call_hooks(:post_setup_logger)
  222. @logger
  223. end
  224. def default_logger
  225. logger = Logger.new(STDOUT)
  226. logger.formatter = lambda {|severity, time, progname, message|
  227. color =
  228. case severity
  229. when /^DEBUG/
  230. 'blue'
  231. when /^INFO/
  232. 'cyan'
  233. when /^WARN/
  234. 'magenta'
  235. when /^ERROR/
  236. 'red'
  237. when /^FATAL/
  238. 'on_red'
  239. else
  240. 'white'
  241. end
  242. TermColor.parse("<#{color}>" + TermColor.escape("[#{severity}] #{message}\n") + "</#{color}>")
  243. }
  244. logger
  245. end
  246. def init(&block)
  247. @init_block = block
  248. end
  249. def load_plugins
  250. plug 'defaults'
  251. plug config.system.load_plugins
  252. end
  253. def eval_init_block
  254. @init_block.call(self) if @init_block
  255. end
  256. def setup_task_manager
  257. @task_manager = Termtter::TaskManager.new(1)
  258. end
  259. def parse_options
  260. Termtter::OptParser.parse!(ARGV)
  261. end
  262. def show_splash
  263. puts TermColor.parse(config.splash)
  264. end
  265. def run
  266. parse_options
  267. show_splash unless config.system.cmd_mode
  268. setup_task_manager
  269. load_config
  270. load_plugins
  271. eval_init_block
  272. begin
  273. Termtter::API.setup
  274. rescue Rubytter::APIError => e
  275. handle_error(e)
  276. exit!
  277. end
  278. config.system.eval_scripts.each {|script| rescue_error { eval script }}
  279. config.system.run_commands.each {|cmd| rescue_error { execute(cmd) }}
  280. unless config.system.cmd_mode
  281. @task_manager.run()
  282. call_hooks(:initialize)
  283. add_task(:name => :call_hooks_after_launched, :after => 1) do
  284. call_hooks(:launched)
  285. end
  286. call_hooks(:init_command_line)
  287. end
  288. end
  289. def handle_error(e)
  290. logger.error("#{e.class.to_s}: #{e.message}")
  291. logger.error(e.backtrace.join("\n")) if (e.backtrace and config.devel)
  292. get_hooks(:on_error).each {|hook| hook.call(e) }
  293. end
  294. def rescue_error
  295. begin
  296. yield
  297. rescue Exception => e
  298. handle_error(e)
  299. end
  300. end
  301. def clear_line
  302. print "\e[0G" + "\e[K" unless win?
  303. end
  304. def confirm(message, default_yes = true, &block)
  305. pause # TODO: TaskManager から呼ばれるならこれいらないなぁ
  306. print "\"#{message.strip}\" "
  307. readline = Readline.readline(default_yes ? "[Y/n] " : "[N/y] ", false)
  308. result =
  309. if !!(/^$/ =~ readline)
  310. default_yes
  311. else
  312. !!(/^y/i =~ readline)
  313. end
  314. if result && block
  315. block.call
  316. end
  317. result
  318. ensure
  319. resume # TODO: TaskManager から呼ばれるならこれいらないなぁ
  320. end
  321. end
  322. end
  323. end