twterm 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/twterm.rb +2 -1
- data/lib/twterm/app.rb +9 -6
- data/lib/twterm/client.rb +19 -12
- data/lib/twterm/list.rb +31 -0
- data/lib/twterm/screen.rb +1 -1
- data/lib/twterm/status.rb +19 -2
- data/lib/twterm/tab/base.rb +1 -1
- data/lib/twterm/tab/conversation_tab.rb +29 -11
- data/lib/twterm/tab/dumpable.rb +21 -0
- data/lib/twterm/tab/list_tab.rb +15 -9
- data/lib/twterm/tab/new/list.rb +1 -1
- data/lib/twterm/tab/new/search.rb +2 -1
- data/lib/twterm/tab/new/user.rb +11 -8
- data/lib/twterm/tab/scrollable.rb +1 -1
- data/lib/twterm/tab/search_tab.rb +11 -0
- data/lib/twterm/tab/statuses_tab.rb +4 -2
- data/lib/twterm/tab/user_tab.rb +14 -8
- data/lib/twterm/tab_manager.rb +24 -0
- data/lib/twterm/user.rb +7 -0
- data/lib/twterm/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b7f3f957fa6bb231072502fd2794d212db4547e
|
4
|
+
data.tar.gz: 83aecc6b84ed61620f52b69f6b65c91b6d1af67b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3b1748a40af7f4a32b93568422e0cd65dbe6a9e7a12818567e8aff5142639af6396df93febdffd0f93a19afd9020bf4ac5f86ad468f60b00c5e8cdb597ee5ee
|
7
|
+
data.tar.gz: 6df0822a5f0d59a9f1e2e260e2e43a2b5997a7a36426f5dc786f85283514c9d44ddd9ca74dc41fd7ff1fbc83085bf2eb84a6b4af8b12f33b3438a91ec7d3438c
|
data/lib/twterm.rb
CHANGED
@@ -34,6 +34,7 @@ require 'twterm/scheduler'
|
|
34
34
|
require 'twterm/status'
|
35
35
|
require 'twterm/tab_manager'
|
36
36
|
require 'twterm/tab/base'
|
37
|
+
require 'twterm/tab/dumpable'
|
37
38
|
require 'twterm/tab/exceptions'
|
38
39
|
require 'twterm/tab/scrollable'
|
39
40
|
require 'twterm/tab/statuses_tab'
|
@@ -54,6 +55,6 @@ require 'twterm/version'
|
|
54
55
|
|
55
56
|
module Twterm
|
56
57
|
class Conf
|
57
|
-
REQUIRE_VERSION = '1.0.
|
58
|
+
REQUIRE_VERSION = '1.0.9'
|
58
59
|
end
|
59
60
|
end
|
data/lib/twterm/app.rb
CHANGED
@@ -19,6 +19,7 @@ module Twterm
|
|
19
19
|
|
20
20
|
mentions_tab = Tab::MentionsTab.new(client)
|
21
21
|
TabManager.instance.add(mentions_tab)
|
22
|
+
TabManager.instance.recover_tabs
|
22
23
|
|
23
24
|
Screen.instance.refresh
|
24
25
|
|
@@ -37,15 +38,17 @@ module Twterm
|
|
37
38
|
|
38
39
|
def register_interruption_handler(&block)
|
39
40
|
fail ArgumentError, 'no block given' unless block_given?
|
40
|
-
Signal.trap(:INT)
|
41
|
-
block.call
|
42
|
-
end
|
41
|
+
Signal.trap(:INT) { block.call }
|
43
42
|
end
|
44
43
|
|
45
44
|
def reset_interruption_handler
|
46
|
-
Signal.trap(:INT)
|
47
|
-
|
48
|
-
|
45
|
+
Signal.trap(:INT) { App.instance.quit }
|
46
|
+
end
|
47
|
+
|
48
|
+
def quit
|
49
|
+
Curses.close_screen
|
50
|
+
TabManager.instance.dump_tabs
|
51
|
+
exit
|
49
52
|
end
|
50
53
|
|
51
54
|
private
|
data/lib/twterm/client.rb
CHANGED
@@ -43,9 +43,7 @@ module Twterm
|
|
43
43
|
|
44
44
|
def stream
|
45
45
|
@stream_client.on_friends do
|
46
|
-
|
47
|
-
|
48
|
-
Notifier.instance.show_message 'Connection established'
|
46
|
+
Notifier.instance.show_message 'Connection established' unless @stream_connected
|
49
47
|
@stream_connected = true
|
50
48
|
end
|
51
49
|
|
@@ -117,13 +115,19 @@ module Twterm
|
|
117
115
|
end
|
118
116
|
end
|
119
117
|
|
118
|
+
def list(list_id)
|
119
|
+
send_request do
|
120
|
+
yield List.new(@rest_client.list(list_id))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
120
124
|
def lists
|
121
125
|
send_request do
|
122
126
|
yield @rest_client.lists.map { |list| List.new(list) }
|
123
127
|
end
|
124
128
|
end
|
125
129
|
|
126
|
-
def
|
130
|
+
def list_timeline(list)
|
127
131
|
fail ArgumentError, 'argument must be an instance of List class' unless list.is_a? List
|
128
132
|
send_request do
|
129
133
|
yield @rest_client.list_timeline(list.id, count: 100).select(&@mute_filter).map(&CREATE_STATUS_PROC)
|
@@ -144,11 +148,12 @@ module Twterm
|
|
144
148
|
|
145
149
|
def show_user(query)
|
146
150
|
send_request do
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
151
|
+
user =
|
152
|
+
begin
|
153
|
+
User.new(@rest_client.user(query))
|
154
|
+
rescue Twitter::Error::NotFound
|
155
|
+
nil
|
156
|
+
end
|
152
157
|
yield user
|
153
158
|
end
|
154
159
|
end
|
@@ -249,9 +254,11 @@ module Twterm
|
|
249
254
|
begin
|
250
255
|
block.call
|
251
256
|
rescue Twitter::Error => e
|
252
|
-
Notifier.instance.show_error
|
253
|
-
|
254
|
-
|
257
|
+
Notifier.instance.show_error "Failed to send request: #{e.message}"
|
258
|
+
if e.message == 'getaddrinfo: nodename nor servname provided, or not known'
|
259
|
+
sleep 10
|
260
|
+
retry
|
261
|
+
end
|
255
262
|
end
|
256
263
|
end
|
257
264
|
end
|
data/lib/twterm/list.rb
CHANGED
@@ -2,8 +2,17 @@ module Twterm
|
|
2
2
|
class List
|
3
3
|
attr_reader :id, :name, :slug, :full_name, :mode, :description, :member_count, :subscriber_count
|
4
4
|
|
5
|
+
@@instances = {}
|
6
|
+
|
5
7
|
def initialize(list)
|
6
8
|
@id = list.id
|
9
|
+
update!(list)
|
10
|
+
|
11
|
+
@@instances[@id] = self
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def update!(list)
|
7
16
|
@name = list.name
|
8
17
|
@slug = list.slug
|
9
18
|
@full_name = list.full_name
|
@@ -11,10 +20,32 @@ module Twterm
|
|
11
20
|
@description = list.description.is_a?(Twitter::NullObject) ? '' : list.description
|
12
21
|
@member_count = list.member_count
|
13
22
|
@subscriber_count = list.subscriber_count
|
23
|
+
|
24
|
+
self
|
14
25
|
end
|
15
26
|
|
16
27
|
def ==(other)
|
17
28
|
other.is_a?(self.class) && id == other.id
|
18
29
|
end
|
30
|
+
|
31
|
+
def self.new(list)
|
32
|
+
instance = find(list.id)
|
33
|
+
instance.nil? ? super : instance.update!(list)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.find(id)
|
37
|
+
@@instances[id]
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.find_or_fetch(id)
|
41
|
+
instance = find(id)
|
42
|
+
(yield(instance) && return) if instance
|
43
|
+
|
44
|
+
Client.current.list(id) { |list| yield list }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.all
|
48
|
+
@@instances.values
|
49
|
+
end
|
19
50
|
end
|
20
51
|
end
|
data/lib/twterm/screen.rb
CHANGED
data/lib/twterm/status.rb
CHANGED
@@ -89,14 +89,24 @@ module Twterm
|
|
89
89
|
end
|
90
90
|
|
91
91
|
def in_reply_to_status(&block)
|
92
|
-
|
92
|
+
if @in_reply_to_status_id.nil?
|
93
|
+
block.call(nil)
|
94
|
+
return
|
95
|
+
end
|
93
96
|
|
94
97
|
status = Status.find(@in_reply_to_status_id)
|
95
|
-
|
98
|
+
unless status.nil?
|
99
|
+
block.call(status)
|
100
|
+
return
|
101
|
+
end
|
96
102
|
|
97
103
|
Client.current.show_status(@in_reply_to_status_id, &block)
|
98
104
|
end
|
99
105
|
|
106
|
+
def replies
|
107
|
+
Status.all.select { |s| s.in_reply_to_status_id == id }
|
108
|
+
end
|
109
|
+
|
100
110
|
def retweeted_by
|
101
111
|
User.find(@retweeted_by_user_id)
|
102
112
|
end
|
@@ -122,6 +132,13 @@ module Twterm
|
|
122
132
|
@@instances[id]
|
123
133
|
end
|
124
134
|
|
135
|
+
def find_or_fetch(id)
|
136
|
+
instance = find(id)
|
137
|
+
(yield(instance) && return) if instance
|
138
|
+
|
139
|
+
Client.current.show_status(id) { |status| yield status }
|
140
|
+
end
|
141
|
+
|
125
142
|
def parse_time(time)
|
126
143
|
(time.is_a?(String) ? Time.parse(time) : time.dup).localtime
|
127
144
|
end
|
data/lib/twterm/tab/base.rb
CHANGED
@@ -2,30 +2,48 @@ module Twterm
|
|
2
2
|
module Tab
|
3
3
|
class ConversationTab
|
4
4
|
include StatusesTab
|
5
|
+
include Dumpable
|
5
6
|
|
6
7
|
attr_reader :status
|
7
8
|
|
8
|
-
def initialize(
|
9
|
-
fail ArgumentError, 'argument must be an instance of Status class' unless status.is_a? Status
|
10
|
-
|
9
|
+
def initialize(status_id)
|
11
10
|
@title = 'Conversation'
|
12
|
-
|
13
11
|
super()
|
14
|
-
|
15
|
-
|
12
|
+
|
13
|
+
Status.find_or_fetch(status_id) do |status|
|
14
|
+
@status = status
|
15
|
+
|
16
|
+
append(status)
|
17
|
+
move_to_top
|
18
|
+
Thread.new { fetch_in_reply_to_status(status) }
|
19
|
+
Thread.new { fetch_replies(status) }
|
20
|
+
end
|
16
21
|
end
|
17
22
|
|
18
|
-
def
|
19
|
-
status.in_reply_to_status do |
|
20
|
-
return if
|
21
|
-
append(
|
22
|
-
|
23
|
+
def fetch_in_reply_to_status(status)
|
24
|
+
status.in_reply_to_status do |in_reply_to|
|
25
|
+
return if in_reply_to.nil?
|
26
|
+
append(in_reply_to)
|
27
|
+
sort
|
28
|
+
Thread.new { fetch_in_reply_to_status(in_reply_to) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch_replies(status)
|
33
|
+
status.replies.each do |reply|
|
34
|
+
prepend(reply)
|
35
|
+
sort
|
36
|
+
Thread.new { fetch_replies(reply) }
|
23
37
|
end
|
24
38
|
end
|
25
39
|
|
26
40
|
def ==(other)
|
27
41
|
other.is_a?(self.class) && status == other.status
|
28
42
|
end
|
43
|
+
|
44
|
+
def dump
|
45
|
+
@status.id
|
46
|
+
end
|
29
47
|
end
|
30
48
|
end
|
31
49
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
module Dumpable
|
4
|
+
def dump
|
5
|
+
fail NotImplementedError 'dump method must be implemented'
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
klass.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def recover(title, arg)
|
14
|
+
tab = new(arg)
|
15
|
+
tab.title = title
|
16
|
+
tab
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/twterm/tab/list_tab.rb
CHANGED
@@ -2,23 +2,25 @@ module Twterm
|
|
2
2
|
module Tab
|
3
3
|
class ListTab
|
4
4
|
include StatusesTab
|
5
|
+
include Dumpable
|
5
6
|
|
6
7
|
attr_reader :list
|
7
8
|
|
8
|
-
def initialize(
|
9
|
-
fail ArgumentError, 'argument must be an instance of List class' unless list.is_a? List
|
10
|
-
|
9
|
+
def initialize(list_id)
|
11
10
|
super()
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
List.find_or_fetch(list_id) do |list|
|
13
|
+
@list = list
|
14
|
+
@title = @list.full_name
|
15
|
+
TabManager.instance.refresh_window
|
16
|
+
fetch { move_to_top }
|
17
|
+
@auto_reloader = Scheduler.new(300) { fetch }
|
18
|
+
end
|
17
19
|
end
|
18
20
|
|
19
21
|
def fetch
|
20
22
|
client = Client.current
|
21
|
-
client.
|
23
|
+
client.list_timeline(@list) do |statuses|
|
22
24
|
statuses.reverse.each(&method(:prepend))
|
23
25
|
sort
|
24
26
|
yield if block_given?
|
@@ -26,13 +28,17 @@ module Twterm
|
|
26
28
|
end
|
27
29
|
|
28
30
|
def close
|
29
|
-
@auto_reloader.kill
|
31
|
+
@auto_reloader.kill if @auto_reloader
|
30
32
|
super
|
31
33
|
end
|
32
34
|
|
33
35
|
def ==(other)
|
34
36
|
other.is_a?(self.class) && list == other.list
|
35
37
|
end
|
38
|
+
|
39
|
+
def dump
|
40
|
+
@list.id
|
41
|
+
end
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
data/lib/twterm/tab/new/list.rb
CHANGED
@@ -25,8 +25,9 @@ module Twterm
|
|
25
25
|
|
26
26
|
input_thread = Thread.new do
|
27
27
|
close_screen
|
28
|
+
CompletionManager.instance.set_default_mode!
|
28
29
|
puts "\ninput search query"
|
29
|
-
query = (readline('
|
30
|
+
query = (readline('> ') || '').strip
|
30
31
|
resetter.call
|
31
32
|
|
32
33
|
tab = query.nil? || query.empty? ? Tab::New::Start.new : Tab::SearchTab.new(query)
|
data/lib/twterm/tab/new/user.rb
CHANGED
@@ -31,15 +31,18 @@ module Twterm
|
|
31
31
|
screen_name = (readline('> @') || '').strip
|
32
32
|
resetter.call
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
if screen_name.nil? || screen_name.empty?
|
35
|
+
TabManager.instance.switch(Tab::New::Start.new)
|
36
|
+
else
|
37
|
+
Client.current.show_user(screen_name) do |user|
|
38
|
+
if user.nil?
|
39
|
+
Notifier.instance.show_error 'User not found'
|
40
|
+
tab = Tab::New::Start.new
|
41
|
+
else
|
42
|
+
tab = Tab::UserTab.new(user.id)
|
43
|
+
end
|
44
|
+
TabManager.instance.switch(tab)
|
40
45
|
end
|
41
|
-
|
42
|
-
TabManager.instance.switch(tab)
|
43
46
|
end
|
44
47
|
end
|
45
48
|
|
@@ -2,6 +2,7 @@ module Twterm
|
|
2
2
|
module Tab
|
3
3
|
class SearchTab
|
4
4
|
include StatusesTab
|
5
|
+
include Dumpable
|
5
6
|
|
6
7
|
attr_reader :query
|
7
8
|
|
@@ -12,6 +13,7 @@ module Twterm
|
|
12
13
|
@title = "\"#{@query}\""
|
13
14
|
|
14
15
|
fetch { move_to_top }
|
16
|
+
@auto_reloader = Scheduler.new(300) { fetch }
|
15
17
|
end
|
16
18
|
|
17
19
|
def fetch
|
@@ -21,9 +23,18 @@ module Twterm
|
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
26
|
+
def close
|
27
|
+
@auto_reloader.kill if @auto_reloader
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
24
31
|
def ==(other)
|
25
32
|
other.is_a?(self.class) && query == other.query
|
26
33
|
end
|
34
|
+
|
35
|
+
def dump
|
36
|
+
@query
|
37
|
+
end
|
27
38
|
end
|
28
39
|
end
|
29
40
|
end
|
@@ -82,7 +82,7 @@ module Twterm
|
|
82
82
|
def show_user
|
83
83
|
return if highlighted_status.nil?
|
84
84
|
user = highlighted_status.user
|
85
|
-
user_tab = Tab::UserTab.new(user)
|
85
|
+
user_tab = Tab::UserTab.new(user.id)
|
86
86
|
TabManager.instance.add_and_show(user_tab)
|
87
87
|
end
|
88
88
|
|
@@ -95,7 +95,7 @@ module Twterm
|
|
95
95
|
|
96
96
|
def show_conversation
|
97
97
|
return if highlighted_status.nil?
|
98
|
-
tab = Tab::ConversationTab.new(highlighted_status)
|
98
|
+
tab = Tab::ConversationTab.new(highlighted_status.id)
|
99
99
|
TabManager.instance.add_and_show(tab)
|
100
100
|
end
|
101
101
|
|
@@ -112,6 +112,8 @@ module Twterm
|
|
112
112
|
|
113
113
|
@window.clear
|
114
114
|
|
115
|
+
return if offset < 0
|
116
|
+
|
115
117
|
statuses.reverse.drop(offset).each.with_index(offset) do |status, i|
|
116
118
|
formatted_lines = status.split(@window.maxx - 4).count
|
117
119
|
if current_line + formatted_lines + 2 > @window.maxy
|
data/lib/twterm/tab/user_tab.rb
CHANGED
@@ -2,19 +2,21 @@ module Twterm
|
|
2
2
|
module Tab
|
3
3
|
class UserTab
|
4
4
|
include StatusesTab
|
5
|
+
include Dumpable
|
5
6
|
|
6
7
|
attr_reader :user
|
7
8
|
|
8
|
-
def initialize(
|
9
|
-
fail ArgumentError, 'argument must be an instance of User class' unless user.is_a? User
|
10
|
-
|
9
|
+
def initialize(user_id)
|
11
10
|
super()
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
User.find_or_fetch(user_id) do |user|
|
13
|
+
@user = user
|
14
|
+
@title = "@#{@user.screen_name}"
|
15
|
+
TabManager.instance.refresh_window
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
fetch { move_to_top }
|
18
|
+
@auto_reloader = Scheduler.new(120) { fetch }
|
19
|
+
end
|
18
20
|
end
|
19
21
|
|
20
22
|
def fetch
|
@@ -26,13 +28,17 @@ module Twterm
|
|
26
28
|
end
|
27
29
|
|
28
30
|
def close
|
29
|
-
@auto_reloader.kill
|
31
|
+
@auto_reloader.kill if @auto_reloader
|
30
32
|
super
|
31
33
|
end
|
32
34
|
|
33
35
|
def ==(other)
|
34
36
|
other.is_a?(self.class) && user == other.user
|
35
37
|
end
|
38
|
+
|
39
|
+
def dump
|
40
|
+
@user.id
|
41
|
+
end
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
data/lib/twterm/tab_manager.rb
CHANGED
@@ -3,6 +3,8 @@ module Twterm
|
|
3
3
|
include Singleton
|
4
4
|
include Curses
|
5
5
|
|
6
|
+
DUMPED_TABS_FILE = "#{ENV['HOME']}/.twterm/dumped_tabs"
|
7
|
+
|
6
8
|
def initialize
|
7
9
|
@tabs = []
|
8
10
|
@index = 0
|
@@ -78,6 +80,28 @@ module Twterm
|
|
78
80
|
end
|
79
81
|
end
|
80
82
|
|
83
|
+
def recover_tabs
|
84
|
+
return unless File.exist? DUMPED_TABS_FILE
|
85
|
+
|
86
|
+
data = YAML.load(File.read(DUMPED_TABS_FILE))
|
87
|
+
data.each do |klass, title, arg|
|
88
|
+
tab = klass.recover(title, arg)
|
89
|
+
add(tab)
|
90
|
+
end
|
91
|
+
rescue
|
92
|
+
Notifier.instance.show_error 'Failed to recover tabs'
|
93
|
+
end
|
94
|
+
|
95
|
+
def dump_tabs
|
96
|
+
data = @tabs.each_with_object([]) do |tab, arr|
|
97
|
+
next unless tab.is_a? Tab::Dumpable
|
98
|
+
arr << [tab.class, tab.title, tab.dump]
|
99
|
+
end
|
100
|
+
File.open(DUMPED_TABS_FILE, 'w', 0600) do |f|
|
101
|
+
f.write data.to_yaml
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
81
105
|
def refresh_window
|
82
106
|
@window.clear
|
83
107
|
current_tab_id = current_tab.object_id
|
data/lib/twterm/user.rb
CHANGED
@@ -57,6 +57,13 @@ module Twterm
|
|
57
57
|
@@instances[id]
|
58
58
|
end
|
59
59
|
|
60
|
+
def self.find_or_fetch(id)
|
61
|
+
instance = find(id)
|
62
|
+
(yield(instance) && return) if instance
|
63
|
+
|
64
|
+
Client.current.show_user(id) { |user| yield user }
|
65
|
+
end
|
66
|
+
|
60
67
|
def self.cleanup
|
61
68
|
referenced_users = Status.all.map(&:user)
|
62
69
|
referenced_users.each(&:touch!)
|
data/lib/twterm/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: twterm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryota Kameoka
|
@@ -159,6 +159,7 @@ files:
|
|
159
159
|
- lib/twterm/status.rb
|
160
160
|
- lib/twterm/tab/base.rb
|
161
161
|
- lib/twterm/tab/conversation_tab.rb
|
162
|
+
- lib/twterm/tab/dumpable.rb
|
162
163
|
- lib/twterm/tab/exceptions.rb
|
163
164
|
- lib/twterm/tab/favorites.rb
|
164
165
|
- lib/twterm/tab/list_tab.rb
|