twterm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +88 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +10 -0
  6. data/Rakefile +1 -0
  7. data/bin/twterm +5 -0
  8. data/lib/extentions.rb +51 -0
  9. data/lib/twterm/app.rb +45 -0
  10. data/lib/twterm/auth.rb +23 -0
  11. data/lib/twterm/client.rb +214 -0
  12. data/lib/twterm/color_manager.rb +50 -0
  13. data/lib/twterm/config.rb +37 -0
  14. data/lib/twterm/list.rb +20 -0
  15. data/lib/twterm/notification/base.rb +24 -0
  16. data/lib/twterm/notification/error.rb +19 -0
  17. data/lib/twterm/notification/message.rb +19 -0
  18. data/lib/twterm/notifier.rb +68 -0
  19. data/lib/twterm/screen.rb +53 -0
  20. data/lib/twterm/status.rb +102 -0
  21. data/lib/twterm/tab/auto_reloadable.rb +14 -0
  22. data/lib/twterm/tab/base.rb +41 -0
  23. data/lib/twterm/tab/conversation_tab.rb +31 -0
  24. data/lib/twterm/tab/exceptions.rb +6 -0
  25. data/lib/twterm/tab/favorites.rb +7 -0
  26. data/lib/twterm/tab/list_tab.rb +33 -0
  27. data/lib/twterm/tab/mentions_tab.rb +36 -0
  28. data/lib/twterm/tab/new/list.rb +88 -0
  29. data/lib/twterm/tab/new/search.rb +52 -0
  30. data/lib/twterm/tab/new/start.rb +67 -0
  31. data/lib/twterm/tab/scrollable.rb +130 -0
  32. data/lib/twterm/tab/search_tab.rb +29 -0
  33. data/lib/twterm/tab/statuses_tab.rb +235 -0
  34. data/lib/twterm/tab/timeline_tab.rb +33 -0
  35. data/lib/twterm/tab/user_tab.rb +29 -0
  36. data/lib/twterm/tab_manager.rb +111 -0
  37. data/lib/twterm/tweetbox.rb +53 -0
  38. data/lib/twterm/user.rb +40 -0
  39. data/lib/twterm/user_window.rb +71 -0
  40. data/lib/twterm/version.rb +3 -0
  41. data/lib/twterm.rb +50 -0
  42. data/twterm.gemspec +28 -0
  43. metadata +184 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e40202371311a827db240194d0b8bf6557116ae1
4
+ data.tar.gz: 626203d268f804937122696f04ab2200e1f8dbd5
5
+ SHA512:
6
+ metadata.gz: 59b2bceeb082f670e52f639dca1ffc1411cb1ccc6b6a8371806c8acdb259d0af5474bbf5d09d38706dbe821e1d0d2f90518726682d860db8c49901c8977a067c
7
+ data.tar.gz: 2489774aa2fabd526a67aa15b440cb0befa2f01372991ead8267d24bcfa1f19cf42739a67ac09ed35dd69dfad93acf351865526be6fc4080bb3c2b6760cdf2f9
data/.gitignore ADDED
@@ -0,0 +1,88 @@
1
+ # Created by https://www.gitignore.io
2
+
3
+ ### OSX ###
4
+ .DS_Store
5
+ .AppleDouble
6
+ .LSOverride
7
+
8
+ # Icon must end with two \r
9
+ Icon
10
+
11
+
12
+ # Thumbnails
13
+ ._*
14
+
15
+ # Files that might appear on external disk
16
+ .Spotlight-V100
17
+ .Trashes
18
+
19
+ # Directories potentially created on remote AFP share
20
+ .AppleDB
21
+ .AppleDesktop
22
+ Network Trash Folder
23
+ Temporary Items
24
+ .apdisk
25
+
26
+
27
+ ### Ruby ###
28
+ *.gem
29
+ *.rbc
30
+ /.config
31
+ /coverage/
32
+ /InstalledFiles
33
+ /pkg/
34
+ /spec/reports/
35
+ /test/tmp/
36
+ /test/version_tmp/
37
+ /tmp/
38
+
39
+ ## Specific to RubyMotion:
40
+ .dat*
41
+ .repl_history
42
+ build/
43
+
44
+ ## Documentation cache and generated files:
45
+ /.yardoc/
46
+ /_yardoc/
47
+ /doc/
48
+ /rdoc/
49
+
50
+ ## Environment normalisation:
51
+ /.bundle/
52
+ /vendor/bundle/
53
+ /lib/bundler/man/
54
+
55
+ # for a library or gem, you might want to ignore these files since the code is
56
+ # intended to run in multiple environments; otherwise, check them in:
57
+ # Gemfile.lock
58
+ # .ruby-version
59
+ # .ruby-gemset
60
+
61
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
62
+ .rvmrc
63
+
64
+
65
+ ### vim ###
66
+ [._]*.s[a-w][a-z]
67
+ [._]s[a-w][a-z]
68
+ *.un~
69
+ Session.vim
70
+ .netrwhist
71
+ *~
72
+
73
+
74
+ ### Tags ###
75
+ # Ignore tags created by etags, ctags, gtags (GNU global) and cscope
76
+ TAGS
77
+ !TAGS/
78
+ tags
79
+ !tags/
80
+ gtags.files
81
+ GTAGS
82
+ GRTAGS
83
+ GPATH
84
+ cscope.files
85
+ cscope.out
86
+ cscope.in.out
87
+ cscope.po.out
88
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in twterm.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Ryota Kameoka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # Twterm
2
+
3
+ - A full-featured CLI Twitter client
4
+ - http://twterm.ryota-ka.me/
5
+
6
+ ## Installation
7
+
8
+ ```
9
+ gem install twterm
10
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/twterm ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'twterm'
4
+
5
+ Twterm::App.instance.run
data/lib/extentions.rb ADDED
@@ -0,0 +1,51 @@
1
+ class String
2
+ def width
3
+ each_char.map { |c| c.bytesize == 1 ? 1 : 2 }.reduce(0, &:+)
4
+ end
5
+
6
+ def split_by_width(width)
7
+ cnt = 0
8
+ str = ''
9
+ chunks = []
10
+
11
+ each_char do |c|
12
+ if c == "\n"
13
+ chunks << str
14
+ str = ''
15
+ cnt = 0
16
+ next
17
+ end
18
+
19
+ cnt += c.width
20
+ if cnt > width
21
+ chunks << str
22
+ str = ''
23
+ cnt = 0
24
+ end
25
+ str << c unless str.empty? && c == ' '
26
+ end
27
+ chunks << str unless str.empty?
28
+ chunks
29
+ end
30
+ end
31
+
32
+ class Integer
33
+ def format
34
+ to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\1,')
35
+ end
36
+ end
37
+
38
+ class Curses::Window
39
+ def bold(&block)
40
+ attron(Curses::A_BOLD)
41
+ block.call
42
+ attroff(Curses::A_BOLD)
43
+ end
44
+
45
+ def with_color(fg, bg = :black, &block)
46
+ color_pair_index = Twterm::ColorManager.instance.get_color_pair_index(fg, bg)
47
+ attron(Curses.color_pair(color_pair_index))
48
+ block.call
49
+ attroff(Curses.color_pair(color_pair_index))
50
+ end
51
+ end
data/lib/twterm/app.rb ADDED
@@ -0,0 +1,45 @@
1
+ module Twterm
2
+ class App
3
+ include Singleton
4
+
5
+ def initialize
6
+ Config.load
7
+ Auth.authenticate_user if Config[:screen_name].nil?
8
+
9
+ Screen.instance
10
+
11
+ client = Client.new(Config[:user_id], Config[:screen_name], Config[:access_token], Config[:access_token_secret])
12
+
13
+ timeline = Tab::TimelineTab.new(client)
14
+ TabManager.instance.add_and_show(timeline)
15
+
16
+ mentions_tab = Tab::MentionsTab.new(client)
17
+ TabManager.instance.add(mentions_tab)
18
+
19
+ Screen.instance.refresh
20
+
21
+ client.stream
22
+ UserWindow.instance
23
+
24
+ reset_interruption_handler
25
+ end
26
+
27
+ def run
28
+ Screen.instance.wait
29
+ Screen.instance.refresh
30
+ end
31
+
32
+ def register_interruption_handler(&block)
33
+ fail ArgumentError, 'no block given' unless block_given?
34
+ Signal.trap(:INT) do
35
+ block.call
36
+ end
37
+ end
38
+
39
+ def reset_interruption_handler
40
+ Signal.trap(:INT) do
41
+ exit
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ module Twterm
2
+ module Auth
3
+ def authenticate_user
4
+ consumer = OAuth::Consumer.new(
5
+ 'vLNSVFgXclBJQJRZ7VLMxL9lA',
6
+ 'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5',
7
+ site: 'https://api.twitter.com'
8
+ )
9
+ request_token = consumer.get_request_token
10
+ Launchy.open request_token.authorize_url
11
+ print 'input PIN: '
12
+ pin = (STDIN.gets || '').strip
13
+ access_token = request_token.get_access_token(oauth_verifier: pin)
14
+
15
+ Config[:access_token] = access_token.token
16
+ Config[:access_token_secret] = access_token.secret
17
+ Config[:screen_name] = access_token.params[:screen_name]
18
+ Config[:user_id] = access_token.params[:user_id]
19
+ end
20
+
21
+ module_function :authenticate_user
22
+ end
23
+ end
@@ -0,0 +1,214 @@
1
+ module Twterm
2
+ class Client
3
+ attr_reader :user_id, :screen_name
4
+
5
+ CREATE_STATUS_PROC = -> (s) { Status.new(s) }
6
+
7
+ @@instances = []
8
+
9
+ def initialize(user_id, screen_name, token, secret)
10
+ @user_id = user_id
11
+ @screen_name = screen_name
12
+
13
+ @rest_client = Twitter::REST::Client.new do |config|
14
+ config.consumer_key = 'vLNSVFgXclBJQJRZ7VLMxL9lA'
15
+ config.consumer_secret = 'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5'
16
+ config.access_token = token
17
+ config.access_token_secret = secret
18
+ end
19
+
20
+ TweetStream.configure do |config|
21
+ config.consumer_key = 'vLNSVFgXclBJQJRZ7VLMxL9lA'
22
+ config.consumer_secret = 'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5'
23
+ config.oauth_token = token
24
+ config.oauth_token_secret = secret
25
+ config.auth_method = :oauth
26
+ end
27
+
28
+ @stream_client = TweetStream::Client.new
29
+
30
+ @callbacks = {}
31
+ @@instances << self
32
+ end
33
+
34
+ def stream
35
+ @stream_client.on_timeline_status do |tweet|
36
+ status = Status.new(tweet)
37
+ invoke_callbacks(:timeline_status, status)
38
+ invoke_callbacks(:mention, status) if status.text.include? "@#{@screen_name}"
39
+ end
40
+
41
+ @stream_client.on_delete do |status_id|
42
+ timeline.delete_status(status_id)
43
+ end
44
+
45
+ @stream_client.on_event(:favorite) do |event|
46
+ break if event[:source][:screen_name] == @screen_name
47
+ message = "@#{event[:source][:screen_name]} has favorited your tweet: #{event[:target_object][:text]}"
48
+ Notifier.instance.show_message(message)
49
+ end
50
+
51
+ @stream_client.on_no_data_received do
52
+ connect_stream
53
+ end
54
+
55
+ connect_stream
56
+ end
57
+
58
+ def connect_stream
59
+ @stream_client.stop_stream
60
+ @streaming_thread.kill if @streaming_thread.is_a? Thread
61
+
62
+ Notifier.instance.show_message 'Trying to connect to Twitter...'
63
+ @streaming_thread = Thread.new do
64
+ begin
65
+ @stream_client.userstream
66
+ rescue EventMachine::ConnectionError
67
+ Notifier.instance.show_error 'Connection failed'
68
+ sleep 30
69
+ retry
70
+ end
71
+ Notifier.instance.show_message 'Connection established'
72
+ end
73
+ end
74
+
75
+ def post(text, in_reply_to = nil)
76
+ send_request do
77
+ if in_reply_to.is_a? Status
78
+ text = "@#{in_reply_to.user.screen_name} #{text}"
79
+ @rest_client.update(text, in_reply_to_status_id: in_reply_to.id)
80
+ else
81
+ @rest_client.update(text)
82
+ end
83
+ end
84
+ end
85
+
86
+ def home_timeline
87
+ send_request do
88
+ yield @rest_client.home_timeline(count: 200).map(&CREATE_STATUS_PROC)
89
+ end
90
+ end
91
+
92
+ def mentions
93
+ send_request do
94
+ yield @rest_client.mentions(count: 200).map(&CREATE_STATUS_PROC)
95
+ end
96
+ end
97
+
98
+ def user_timeline(user_id)
99
+ send_request do
100
+ yield @rest_client.user_timeline(user_id, count: 200).map(&CREATE_STATUS_PROC)
101
+ end
102
+ end
103
+
104
+ def lists
105
+ send_request do
106
+ yield @rest_client.lists.map { |list| List.new(list) }
107
+ end
108
+ end
109
+
110
+ def list(list)
111
+ fail ArgumentError, 'argument must be an instance of List class' unless list.is_a? List
112
+ send_request do
113
+ yield @rest_client.list_timeline(list.id, count: 200).map(&CREATE_STATUS_PROC)
114
+ end
115
+ end
116
+
117
+ def search(query)
118
+ send_request do
119
+ yield @rest_client.search(query, count: 100).map(&CREATE_STATUS_PROC)
120
+ end
121
+ end
122
+
123
+ def show_status(status_id)
124
+ send_request do
125
+ yield Status.new(@rest_client.status(status_id))
126
+ end
127
+ end
128
+
129
+ def favorite(status)
130
+ return false unless status.is_a? Status
131
+
132
+ send_request do
133
+ @rest_client.favorite(status.id)
134
+ status.favorite!
135
+ yield status if block_given?
136
+ end
137
+
138
+ self
139
+ end
140
+
141
+ def unfavorite(status)
142
+ fail ArgumentError, 'argument must be an instance of Status class' unless status.is_a? Status
143
+
144
+ send_request do
145
+ @rest_client.unfavorite(status.id)
146
+ status.unfavorite!
147
+ yield status if block_given?
148
+ end
149
+ end
150
+
151
+ def retweet(status)
152
+ return false unless status.is_a? Status
153
+
154
+ send_request do
155
+ begin
156
+ @rest_client.retweet!(status.id)
157
+ status.retweet!
158
+ yield status if block_given?
159
+ rescue Twitter::Error::AlreadyRetweeted, Twitter::Error::NotFound, Twitter::Error::Forbidden
160
+ Notifier.instance.show_error 'Retweet attempt failed'
161
+ end
162
+ end
163
+ end
164
+
165
+ def on_timeline_status(&block)
166
+ fail ArgumentError, 'no block given' unless block_given?
167
+ on(:timeline_status, &block)
168
+ end
169
+
170
+ def on_mention(&block)
171
+ fail ArgumentError, 'no block given' unless block_given?
172
+ on(:mention, &block)
173
+ end
174
+
175
+ class << self
176
+ def new(user_id, screen_name, token, secret)
177
+ detector = -> (instance) { instance.user_id == user_id }
178
+ instance = @@instances.find(&detector)
179
+ instance.nil? ? super : instance
180
+ end
181
+
182
+ def current
183
+ @@instances[0]
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ def on(event, &block)
190
+ @callbacks[event] ||= []
191
+ @callbacks[event] << block
192
+ self
193
+ end
194
+
195
+ def invoke_callbacks(event, data = nil)
196
+ return if @callbacks[event].nil?
197
+
198
+ @callbacks[event].each { |cb| cb.call(data) }
199
+ self
200
+ end
201
+
202
+ def send_request(&block)
203
+ Thread.new do
204
+ begin
205
+ block.call
206
+ rescue Twitter::Error => e
207
+ Notifier.instance.show_error 'Failed to send request'
208
+ sleep 10
209
+ retry if e.message == 'getaddrinfo: nodename nor servname provided, or not known'
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,50 @@
1
+ module Twterm
2
+ class ColorManager
3
+ include Singleton
4
+ include Curses
5
+
6
+ COLORS = [:black, :white, :red, :green, :blue, :yellow, :cyan, :magenta]
7
+ CURSES_COLORS = {
8
+ black: COLOR_BLACK,
9
+ white: COLOR_WHITE,
10
+ red: COLOR_RED,
11
+ green: COLOR_GREEN,
12
+ blue: COLOR_BLUE,
13
+ yellow: COLOR_YELLOW,
14
+ cyan: COLOR_CYAN,
15
+ magenta: COLOR_MAGENTA
16
+ }
17
+
18
+ def initialize
19
+ @colors = {
20
+ black: {}, white: {}, red: {}, green: {},
21
+ blue: {}, yellow: {}, cyan: {}, magenta: {}
22
+ }
23
+ @count = 0
24
+ end
25
+
26
+ def get_color_pair_index(fg, bg)
27
+ fail ArgumentError, 'invalid color name' unless COLORS.include? fg
28
+ fail ArgumentError, 'invalid color name' unless COLORS.include? bg
29
+
30
+ return @colors[bg][fg] unless @colors[bg][fg].nil?
31
+
32
+ add_color(fg, bg)
33
+ end
34
+
35
+ private
36
+
37
+ def add_color(fg, bg)
38
+ fail ArgumentError, 'invalid color name' unless COLORS.include? fg
39
+ fail ArgumentError, 'invalid color name' unless COLORS.include? bg
40
+
41
+ @count += 1
42
+ index = @count
43
+
44
+ Curses.init_pair(index, CURSES_COLORS[fg], CURSES_COLORS[bg])
45
+ @colors[bg][fg] = index
46
+
47
+ index
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ module Twterm
2
+ module Config
3
+ CONFIG_FILE = "#{ENV['HOME']}/.twterm.yml"
4
+
5
+ def [](key)
6
+ @config[key]
7
+ end
8
+
9
+ def []=(key, value)
10
+ @config[key] = value
11
+ save
12
+ end
13
+
14
+ def load
15
+ unless File.exist? CONFIG_FILE
16
+ @config = {}
17
+ return
18
+ end
19
+ @config = YAML.load(File.read(CONFIG_FILE)) || {}
20
+ end
21
+
22
+ private
23
+
24
+ def save
25
+ begin
26
+ file = File.open(CONFIG_FILE, 'w', 0600)
27
+ file.write @config.to_yaml
28
+ rescue
29
+ puts 'exception raised'
30
+ ensure
31
+ file.close
32
+ end
33
+ end
34
+
35
+ module_function :[], :[]=, :load, :save
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ module Twterm
2
+ class List
3
+ attr_reader :id, :name, :slug, :full_name, :mode, :description, :member_count, :subscriber_count
4
+
5
+ def initialize(list)
6
+ @id = list.id
7
+ @name = list.name
8
+ @slug = list.slug
9
+ @full_name = list.full_name
10
+ @mode = list.mode
11
+ @description = list.description.is_a?(Twitter::NullObject) ? '' : list.description
12
+ @member_count = list.member_count
13
+ @subscriber_count = list.subscriber_count
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(self.class) && id == other.id
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module Twterm
2
+ module Notification
3
+ module Base
4
+ attr_reader :time, :fg_color, :bg_color
5
+
6
+ def initialize(message)
7
+ @message = message
8
+ @time = Time.now
9
+ end
10
+
11
+ def show_with_width(width)
12
+ @message.gsub("\n", ' ')
13
+ end
14
+
15
+ def fg_color
16
+ fail NotImplementedError, 'fg_color method must be implemented'
17
+ end
18
+
19
+ def bg_color
20
+ fail NotImplementedError, 'bg_color method must be implemented'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module Twterm
2
+ module Notification
3
+ class Error
4
+ include Base
5
+
6
+ def initialize(message)
7
+ super
8
+ end
9
+
10
+ def fg_color
11
+ :white
12
+ end
13
+
14
+ def bg_color
15
+ :red
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Twterm
2
+ module Notification
3
+ class Message
4
+ include Base
5
+
6
+ def initialize(message)
7
+ super
8
+ end
9
+
10
+ def fg_color
11
+ :white
12
+ end
13
+
14
+ def bg_color
15
+ :blue
16
+ end
17
+ end
18
+ end
19
+ end