tkri 0.9.0
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.
- data/README +11 -0
- data/bin/tkri +9 -0
- data/lib/tkri.rb +585 -0
- metadata +64 -0
data/README
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
== tkri
|
2
|
+
|
3
|
+
tkri is a GUI front-end to the 'ri', or 'qri', executables. It displays
|
4
|
+
their output in a window where each word is "hyperlinked".
|
5
|
+
|
6
|
+
== Use
|
7
|
+
|
8
|
+
Launch it by typing 'tkri' at the operating system prompt. You can
|
9
|
+
provide a starting topic as an argument on the command line. Inside the
|
10
|
+
application, type the topic you wish to go to at the address bar, or
|
11
|
+
click on a word in the main text.
|
data/bin/tkri
ADDED
data/lib/tkri.rb
ADDED
@@ -0,0 +1,585 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# $Id$
|
3
|
+
|
4
|
+
# @file
|
5
|
+
# A GUI front-end to RI.
|
6
|
+
#
|
7
|
+
# @author Mooffie <mooffie@gmail.com>
|
8
|
+
|
9
|
+
require 'tk'
|
10
|
+
|
11
|
+
module Tkri
|
12
|
+
|
13
|
+
COMMAND = {
|
14
|
+
# Each platform may use a different command. The commands are indexed by any
|
15
|
+
# substring in RUBY_PLATFORM. If none matches the platform, the 'default' key
|
16
|
+
# is used.
|
17
|
+
'default' => 'qri -f ansi "%s"',
|
18
|
+
/linux/ => 'qri -f ansi "%s" 2>&1',
|
19
|
+
/darwin/ => 'qri -f ansi "%s" 2>&1',
|
20
|
+
}
|
21
|
+
|
22
|
+
TAGS = {
|
23
|
+
'bold' => { :foreground => 'blue' },
|
24
|
+
'italic' => { :foreground => '#6b8e23' }, # greenish
|
25
|
+
'code' => { :foreground => '#1874cd' }, # blueish
|
26
|
+
'header2' => { :background => '#ffe4b5' },
|
27
|
+
'header3' => { :background => '#ffe4b5' },
|
28
|
+
'keyword' => { :foreground => 'red' },
|
29
|
+
'search' => { :background => 'yellow' },
|
30
|
+
'hidden' => { :elide => true },
|
31
|
+
}
|
32
|
+
|
33
|
+
HistoryEntry = Struct.new(:topic, :cursor, :yview)
|
34
|
+
|
35
|
+
# A Tab encapsulates an @address box, where you type the topic to go to; a "Go"
|
36
|
+
# button; and an @info box in which to show the topic.
|
37
|
+
class Tab < TkFrame
|
38
|
+
|
39
|
+
attr_reader :topic
|
40
|
+
|
41
|
+
def initialize(tk_parent, app, configuration = {})
|
42
|
+
@app = app
|
43
|
+
super(tk_parent, configuration)
|
44
|
+
|
45
|
+
#
|
46
|
+
# The address bar
|
47
|
+
#
|
48
|
+
addressbar = TkFrame.new(self) { |ab|
|
49
|
+
pack :side => 'top', :fill => 'x'
|
50
|
+
TkButton.new(ab) {
|
51
|
+
text 'Go'
|
52
|
+
command { app.go }
|
53
|
+
pack :side => 'right'
|
54
|
+
}
|
55
|
+
}
|
56
|
+
@address = TkEntry.new(addressbar) {
|
57
|
+
configure :font => 'courier', :width => 30
|
58
|
+
pack :side => 'left', :expand => true, :fill => 'both'
|
59
|
+
}
|
60
|
+
|
61
|
+
#
|
62
|
+
# The info box, where the main text is displayed.
|
63
|
+
#
|
64
|
+
_frame = self
|
65
|
+
@info = TkText.new(self) { |t|
|
66
|
+
pack :side => 'left', :fill => 'both', :expand => true
|
67
|
+
TkScrollbar.new(_frame) { |s|
|
68
|
+
pack :side => 'right', :fill => 'y'
|
69
|
+
command { |*args| t.yview *args }
|
70
|
+
t.yscrollcommand { |first,last| s.set first,last }
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
TAGS.each do |name, conf|
|
75
|
+
@info.tag_configure(name, conf)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Key and mouse bindings
|
79
|
+
@address.bind('Key-Return') { go }
|
80
|
+
@address.bind('Key-KP_Enter') { go }
|
81
|
+
@info.bind('ButtonRelease-1') { |e| go_xy_word(e.x, e.y) }
|
82
|
+
# If I make the following "ButtonRelease-2" instead, the <PasteSelection>
|
83
|
+
# cancellation that follows won't work. Strange.
|
84
|
+
@info.bind('Button-2') { |e| go_xy_word(e.x, e.y, true) }
|
85
|
+
@info.bind('ButtonRelease-3') { |e| back }
|
86
|
+
@info.bind('Key-BackSpace') { |e| back; break }
|
87
|
+
|
88
|
+
# Tk doesn't support "read-only" text widget. We "disable" the following
|
89
|
+
# keys explicitly (using 'break'). We also forward these search keys to
|
90
|
+
# @app.
|
91
|
+
@info.bind('<PasteSelection>') { break }
|
92
|
+
@info.bind('Key-slash') { @app.search; break }
|
93
|
+
@info.bind('Key-n') { @app.search_next; break }
|
94
|
+
@info.bind('Key-N') { @app.search_prev; break }
|
95
|
+
|
96
|
+
@history = []
|
97
|
+
end
|
98
|
+
|
99
|
+
# Moves the keyboard focus to the address box. Also, selects all the
|
100
|
+
# text, like modern GUIs do.
|
101
|
+
def focus_address
|
102
|
+
@address.selection_range('0', 'end')
|
103
|
+
@address.icursor = 'end'
|
104
|
+
@address.focus
|
105
|
+
end
|
106
|
+
|
107
|
+
# Finds the next occurrence of a word.
|
108
|
+
def search_next_word(word)
|
109
|
+
@info.focus
|
110
|
+
highlight_word word
|
111
|
+
cursor = @info.index('insert')
|
112
|
+
pos = @info.search_with_length(Regexp.new(Regexp::quote(word), Regexp::IGNORECASE), cursor + ' 1 chars')[0]
|
113
|
+
if pos.empty?
|
114
|
+
@app.status = 'Cannot find "%s"' % word
|
115
|
+
else
|
116
|
+
set_cursor(pos)
|
117
|
+
if @info.compare(cursor, '>=', pos)
|
118
|
+
@app.status = 'Continuing search at top'
|
119
|
+
else
|
120
|
+
@app.status = ''
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Finds the previous occurrence of a word.
|
126
|
+
def search_prev_word(word)
|
127
|
+
@info.focus
|
128
|
+
highlight_word word
|
129
|
+
cursor = @info.index('insert')
|
130
|
+
pos = @info.rsearch_with_length(Regexp.new(Regexp::quote(word), Regexp::IGNORECASE), cursor)[0]
|
131
|
+
if pos.empty?
|
132
|
+
@app.status = 'Cannot find "%s"' % word
|
133
|
+
else
|
134
|
+
set_cursor(pos)
|
135
|
+
if @info.compare(cursor, '<=', pos)
|
136
|
+
@app.status = 'Continuing search at bottom'
|
137
|
+
else
|
138
|
+
@app.status = ''
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Highlights a word in the text. Used by the search methods.
|
144
|
+
def highlight_word(word)
|
145
|
+
return if word.empty?
|
146
|
+
@info.tag_remove('search', '1.0', 'end')
|
147
|
+
_highlight_word(word.downcase, @info.get('1.0', 'end').downcase, 'search')
|
148
|
+
end
|
149
|
+
|
150
|
+
def _highlight_word(word_or_regexp, text, tag_name)
|
151
|
+
pos = -1
|
152
|
+
while pos = text.index(word_or_regexp, pos + 1)
|
153
|
+
length = (word_or_regexp.is_a? String) ? word_or_regexp.length : $&.length
|
154
|
+
@info.tag_add(tag_name, '1.0 + %d chars' % pos,
|
155
|
+
'1.0 + %d chars' % (pos + length))
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Navigate to the topic mentioned under the mouse cursor (given by x,y
|
160
|
+
# coordinates)
|
161
|
+
def go_xy_word(x, y, newtab=false)
|
162
|
+
if not newtab and not @info.tag_ranges('sel').empty?
|
163
|
+
# We don't want to prohibit selecting text, so we don't trigger
|
164
|
+
# navigation if some text is selected. (Remember, this method is called
|
165
|
+
# upon releasing the mouse button.)
|
166
|
+
return
|
167
|
+
end
|
168
|
+
if (word = get_xy_word(x, y))
|
169
|
+
@app.go word, newtab
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the section (the header) the cursor is in.
|
174
|
+
def get_previous_header cursor
|
175
|
+
ret = @info.rsearch_with_length(/[\r\n]\w[^\r\n]*/, cursor)
|
176
|
+
if !ret[0].empty? and @info.compare(cursor, '>=', ret[0])
|
177
|
+
return ret[2].strip
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns the first class mentioned before the cursor.
|
182
|
+
def get_previous_class cursor
|
183
|
+
ret = @info.rsearch_with_length(/[A-Z]\w*/, cursor)
|
184
|
+
return ret[0].empty? ? nil : ret[2]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Get the "topic" under the mouse cursor.
|
188
|
+
def get_xy_word(x,y)
|
189
|
+
cursor = '@' + x.to_s + ',' + y.to_s
|
190
|
+
line = @info.get(cursor + ' linestart', cursor + ' lineend')
|
191
|
+
pos = @info.get(cursor + ' linestart', cursor).length
|
192
|
+
|
193
|
+
line = ' ' + line + ' '
|
194
|
+
pos += 1
|
195
|
+
|
196
|
+
a = pos
|
197
|
+
a -= 1 while line[a-1,1] !~ /[ (]/
|
198
|
+
z = pos
|
199
|
+
z += 1 while line[z+1,1] !~ /[ ()]/
|
200
|
+
word = line[a..z]
|
201
|
+
|
202
|
+
# Get rid of English punctuation.
|
203
|
+
word.gsub!(/[,.:;]$/, '')
|
204
|
+
|
205
|
+
# Get rid of italic, bold, and code markup.
|
206
|
+
if word =~ /^(_|\*|\+).*\1$/
|
207
|
+
word = word[1...-1]
|
208
|
+
a += 1
|
209
|
+
end
|
210
|
+
|
211
|
+
a -= 1 # Undo the `line = ' ' + line` we did previously.
|
212
|
+
@info.tag_add('keyword', '%s linestart + %d chars' % [ cursor, a ],
|
213
|
+
'%s linestart + %d chars' % [ cursor, a+word.length ])
|
214
|
+
word.strip!
|
215
|
+
|
216
|
+
return nil if word.empty?
|
217
|
+
return nil if word =~ /^-+$/ # A special case: a line of '-----'
|
218
|
+
|
219
|
+
case get_previous_header(cursor)
|
220
|
+
when 'Instance methods:'
|
221
|
+
word = topic + '#' + word
|
222
|
+
when 'Class methods:'
|
223
|
+
word = topic + '::' + word
|
224
|
+
when 'Includes:'
|
225
|
+
word = get_previous_class(cursor) + '#' + word if not word =~ /^[A-Z]/
|
226
|
+
end
|
227
|
+
|
228
|
+
return word
|
229
|
+
end
|
230
|
+
|
231
|
+
# Sets the text of the @info box, converting ANSI escape sequences to Tk
|
232
|
+
# tags.
|
233
|
+
def set_ansi_text(text)
|
234
|
+
text = text.dup
|
235
|
+
ansi_tags = {
|
236
|
+
# The following possibilities were taken from /usr/lib/ruby/1.8/rdoc/ri/ri_formatter.rb
|
237
|
+
'1' => 'bold',
|
238
|
+
'33' => 'italic',
|
239
|
+
'36' => 'code',
|
240
|
+
'4;32' => 'header2',
|
241
|
+
'32' => 'header3',
|
242
|
+
}
|
243
|
+
ranges = []
|
244
|
+
while text =~ /\x1b\[([\d;]+)m ([^\x1b]*) \x1b\[0?m/x
|
245
|
+
start = $`.length
|
246
|
+
length = $2.length
|
247
|
+
raw_length = $&.length
|
248
|
+
text[start, raw_length] = $2
|
249
|
+
ranges << { :start => start, :length => length, :tag => ansi_tags[$1] }
|
250
|
+
end
|
251
|
+
|
252
|
+
@info.delete('1.0', 'end')
|
253
|
+
@info.insert('end', text)
|
254
|
+
|
255
|
+
ranges.each do |range|
|
256
|
+
if range[:tag]
|
257
|
+
@info.tag_add(range[:tag], '1.0 + %d chars' % range[:start],
|
258
|
+
'1.0 + %d chars' % (range[:start] + range[:length]))
|
259
|
+
end
|
260
|
+
end
|
261
|
+
# Hide any remaining sequences. This may happen because our previous regexp
|
262
|
+
# (or any regexp) can't handle nested sequences.
|
263
|
+
_highlight_word(/\x1b\[([\d;]*)m/, text, 'hidden')
|
264
|
+
end
|
265
|
+
|
266
|
+
# Allow for some shortcuts when typing topics...
|
267
|
+
def fixup_topic(topic)
|
268
|
+
case topic
|
269
|
+
when 'S', 's', 'string'
|
270
|
+
'String'
|
271
|
+
when 'A', 'a', 'array'
|
272
|
+
'Array'
|
273
|
+
when 'H', 'h', 'hash'
|
274
|
+
'Hash'
|
275
|
+
when 'File::new'
|
276
|
+
# See qri bug at http://rubyforge.org/tracker/index.php?func=detail&aid=23504&group_id=2545&atid=9811
|
277
|
+
'File#new'
|
278
|
+
else
|
279
|
+
topic
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Navigates to some topic.
|
284
|
+
def go(topic=nil, skip_history=false)
|
285
|
+
topic = (topic || @address.get).strip
|
286
|
+
return if topic.empty?
|
287
|
+
if @topic and not skip_history
|
288
|
+
# Push current topic into history.
|
289
|
+
@history << HistoryEntry.new(@topic, @info.index('insert'), @info.yview[0])
|
290
|
+
end
|
291
|
+
@topic = fixup_topic(topic)
|
292
|
+
@app.status = 'Loading "%s"...' % @topic
|
293
|
+
@address.delete('0', 'end')
|
294
|
+
@address.insert('end', @topic)
|
295
|
+
focus_address
|
296
|
+
# We need to give our GUI a chance to redraw itself, so we run the
|
297
|
+
# time-consuming 'ri' command "in the next go".
|
298
|
+
TkAfter.new 100, 1 do
|
299
|
+
ri = @app.fetch_ri(@topic)
|
300
|
+
set_ansi_text(ri)
|
301
|
+
@app.refresh_tabsbar
|
302
|
+
@app.status = ''
|
303
|
+
@info.focus
|
304
|
+
set_cursor '1.0'
|
305
|
+
yield if block_given?
|
306
|
+
end.start
|
307
|
+
end
|
308
|
+
|
309
|
+
# Navigate to the previous topic viewed.
|
310
|
+
def back
|
311
|
+
if (entry = @history.pop)
|
312
|
+
go(entry.topic, true) do
|
313
|
+
@info.yview_moveto entry.yview
|
314
|
+
set_cursor entry.cursor
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Sets @info's caret position. Scroll the view if needed.
|
320
|
+
def set_cursor(pos)
|
321
|
+
@info.mark_set('insert', pos)
|
322
|
+
@info.see(pos)
|
323
|
+
end
|
324
|
+
|
325
|
+
def new?
|
326
|
+
not @topic
|
327
|
+
end
|
328
|
+
|
329
|
+
def show
|
330
|
+
pack :fill => 'both', :expand => true
|
331
|
+
end
|
332
|
+
|
333
|
+
def hide
|
334
|
+
pack_forget
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# The tabsbar holds the buttons used to switch among the tabs.
|
339
|
+
class Tabsbar < TkFrame
|
340
|
+
|
341
|
+
def initialize(tk_parent, tabs, configuration = {})
|
342
|
+
@tabs = tabs
|
343
|
+
super(tk_parent, configuration)
|
344
|
+
@buttons = []
|
345
|
+
build_buttons
|
346
|
+
end
|
347
|
+
|
348
|
+
def set_current_tab new
|
349
|
+
@buttons.each_with_index do |b, i|
|
350
|
+
b.relief = (i == new) ? 'sunken' : 'raised'
|
351
|
+
end
|
352
|
+
@tabs.set_current_tab new
|
353
|
+
end
|
354
|
+
|
355
|
+
def build_buttons
|
356
|
+
@buttons.each { |b| b.destroy }
|
357
|
+
@buttons = []
|
358
|
+
|
359
|
+
@tabs.each_with_index do |tab, i|
|
360
|
+
b = TkButton.new(self, :text => (tab.topic || '<new>')).pack :side => 'left'
|
361
|
+
b.command { set_current_tab i }
|
362
|
+
b.bind('Button-3') { @tabs.close tab }
|
363
|
+
@buttons << b
|
364
|
+
end
|
365
|
+
|
366
|
+
plus = TkButton.new(self, :text => '+').pack :side => 'left'
|
367
|
+
plus.command { @tabs.new_tab }
|
368
|
+
@buttons << plus
|
369
|
+
|
370
|
+
set_current_tab @tabs.get_current_tab
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# A 'Tabs' object holds several child objects of class 'Tab' and switches their
|
375
|
+
# visibility so that only one is visible at one time.
|
376
|
+
class Tabs < TkFrame
|
377
|
+
|
378
|
+
include Enumerable
|
379
|
+
|
380
|
+
def initialize(tk_parent, app, configuration = {})
|
381
|
+
@app = app
|
382
|
+
super(tk_parent, configuration)
|
383
|
+
@tabs = []
|
384
|
+
new_tab
|
385
|
+
end
|
386
|
+
|
387
|
+
def new_tab
|
388
|
+
tab = Tab.new(self, @app)
|
389
|
+
tab.focus_address
|
390
|
+
@tabs << tab
|
391
|
+
set_current_tab(@tabs.size - 1)
|
392
|
+
@app.refresh_tabsbar
|
393
|
+
end
|
394
|
+
|
395
|
+
def close(tab)
|
396
|
+
if (@tabs.size > 1 and i = @tabs.index(tab))
|
397
|
+
@tabs.delete_at i
|
398
|
+
tab.destroy
|
399
|
+
set_current_tab(@current - 1) if @current >= i and @current > 0
|
400
|
+
@app.refresh_tabsbar
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
def set_current_tab(new)
|
405
|
+
self.each_with_index do |tab, i|
|
406
|
+
if i == new; tab.show; else tab.hide; end
|
407
|
+
end
|
408
|
+
@current = new
|
409
|
+
end
|
410
|
+
|
411
|
+
def get_current_tab
|
412
|
+
return @current
|
413
|
+
end
|
414
|
+
|
415
|
+
def current
|
416
|
+
@tabs[get_current_tab]
|
417
|
+
end
|
418
|
+
|
419
|
+
def each
|
420
|
+
@tabs.each do |tab|
|
421
|
+
yield tab
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
end
|
426
|
+
|
427
|
+
class App
|
428
|
+
|
429
|
+
def initialize
|
430
|
+
@root = root = TkRoot.new { title 'Tkri' }
|
431
|
+
@search_word = nil
|
432
|
+
|
433
|
+
menu_spec = [
|
434
|
+
[['File', 0],
|
435
|
+
['Close tab', proc { @tabs.close @tabs.current }, 0, 'Ctrl+W' ],
|
436
|
+
'---',
|
437
|
+
['Quit', proc { exit }, 0, 'Ctrl+Q' ]],
|
438
|
+
[['Search', 0],
|
439
|
+
['Search', proc { search }, 0, '/'],
|
440
|
+
['Repeat search', proc { search_next }, 0, 'n'],
|
441
|
+
['Repeat backwards', proc { search_prev }, 7, 'N']],
|
442
|
+
# The following :menu_name=>'help' has no effect, but it should have...
|
443
|
+
# probably a bug in RubyTK.
|
444
|
+
[['Help', 0, { :menu_name => 'help' }],
|
445
|
+
['General', proc { help_general }, 0],
|
446
|
+
['Key bindings', proc { help_key_bindings }, 0]],
|
447
|
+
]
|
448
|
+
TkMenubar.new(root, menu_spec).pack(:side => 'top', :fill => 'x')
|
449
|
+
|
450
|
+
root.bind('Control-q') { exit }
|
451
|
+
root.bind('Control-w') { @tabs.close @tabs.current }
|
452
|
+
root.bind('Control-l') { @tabs.current.focus_address }
|
453
|
+
|
454
|
+
{ 'Key-slash' => 'search',
|
455
|
+
'Key-n' => 'search_next',
|
456
|
+
'Key-N' => 'search_prev',
|
457
|
+
}.each do |event, method|
|
458
|
+
root.bind(event) { |e|
|
459
|
+
send(method) if e.widget.class != TkEntry
|
460
|
+
}
|
461
|
+
end
|
462
|
+
|
463
|
+
@tabs = Tabs.new(root, self) {
|
464
|
+
pack :side => 'top', :fill => 'both', :expand => true
|
465
|
+
}
|
466
|
+
@tabsbar = Tabsbar.new(root, @tabs) {
|
467
|
+
pack :side => 'top', :fill => 'x', :before => @tabs
|
468
|
+
}
|
469
|
+
@statusbar = TkLabel.new(root, :anchor => 'w') {
|
470
|
+
pack :side => 'bottom', :fill => 'x'
|
471
|
+
}
|
472
|
+
end
|
473
|
+
|
474
|
+
def run
|
475
|
+
Tk.mainloop
|
476
|
+
end
|
477
|
+
|
478
|
+
# Navigates to some topic. This method simply delegates to the current tab.
|
479
|
+
def go(topic=nil, newtab=false)
|
480
|
+
@tabs.new_tab if newtab and not @tabs.current.new?
|
481
|
+
@tabs.current.go topic
|
482
|
+
end
|
483
|
+
|
484
|
+
# Sets the text to show in the status bar.
|
485
|
+
def status=(status)
|
486
|
+
@statusbar.configure(:text => status)
|
487
|
+
end
|
488
|
+
|
489
|
+
def refresh_tabsbar
|
490
|
+
@tabsbar.build_buttons if @tabsbar
|
491
|
+
end
|
492
|
+
|
493
|
+
def search_prev
|
494
|
+
if @search_word
|
495
|
+
@tabs.current.search_prev_word @search_word
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
def search_next
|
500
|
+
if @search_word
|
501
|
+
@tabs.current.search_next_word @search_word
|
502
|
+
else
|
503
|
+
search
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
def search
|
508
|
+
self.status = 'Type the string to search'
|
509
|
+
entry = TkEntry.new(@root).pack(:fill => 'x').focus
|
510
|
+
entry.bind('Key-Return') {
|
511
|
+
self.status = ''
|
512
|
+
@search_word = entry.get
|
513
|
+
@tabs.current.search_next_word entry.get
|
514
|
+
entry.destroy
|
515
|
+
}
|
516
|
+
['Key-Escape', 'FocusOut'].each do |event|
|
517
|
+
entry.bind(event) {
|
518
|
+
self.status = ''
|
519
|
+
entry.destroy
|
520
|
+
}
|
521
|
+
end
|
522
|
+
entry.bind('KeyRelease') {
|
523
|
+
@tabs.current.highlight_word entry.get
|
524
|
+
}
|
525
|
+
end
|
526
|
+
|
527
|
+
# Executes the 'ri' command and returns its output.
|
528
|
+
def fetch_ri topic
|
529
|
+
if !@ri_cache
|
530
|
+
@ri_cache = {}
|
531
|
+
@cached_topics = []
|
532
|
+
end
|
533
|
+
|
534
|
+
return @ri_cache[topic] if @ri_cache[topic]
|
535
|
+
|
536
|
+
command = COMMAND.select { |k,v| RUBY_PLATFORM.index(k) }.first.to_a[1] || COMMAND['default']
|
537
|
+
ri = Kernel.`(command % topic) # `
|
538
|
+
if $? != 0
|
539
|
+
ri += "\n" + "ERROR: Failed to run the command '%s' (exit code: %d). Please make sure you have this command in your PATH.\n\nYou may wish to modify this program's source (%s) to update the command to something that works on your system." % [command % topic, $?, $0]
|
540
|
+
else
|
541
|
+
if ri == "nil\n"
|
542
|
+
ri = 'Topic "%s" not found.' % topic
|
543
|
+
end
|
544
|
+
@ri_cache[topic] = ri
|
545
|
+
@cached_topics << topic
|
546
|
+
end
|
547
|
+
|
548
|
+
# Remove the oldest topic from the cache
|
549
|
+
if @cached_topics.length > 10
|
550
|
+
@ri_cache.delete @cached_topics.shift
|
551
|
+
end
|
552
|
+
|
553
|
+
return ri
|
554
|
+
end
|
555
|
+
|
556
|
+
def helpbox(title, text)
|
557
|
+
w = TkToplevel.new(:title => title)
|
558
|
+
TkText.new(w, :height => text.count("\n"), :width => 80).pack.insert('1.0', text)
|
559
|
+
TkButton.new(w, :text => 'Close', :command => proc { w.destroy }).pack
|
560
|
+
end
|
561
|
+
|
562
|
+
def help_general
|
563
|
+
helpbox('Help: General', <<EOS)
|
564
|
+
Tkri (pronounce TIK-ri) is a GUI fron-end to RI (actually, by default,
|
565
|
+
to FastRI).
|
566
|
+
EOS
|
567
|
+
end
|
568
|
+
|
569
|
+
def help_key_bindings
|
570
|
+
helpbox('Help: key bindings', <<EOS)
|
571
|
+
Left mouse button
|
572
|
+
Navigate to the topic under the cursor.
|
573
|
+
Middle mouse button
|
574
|
+
Navigate to the topic under the cursor. Opens in a new tab.
|
575
|
+
Right mouse button
|
576
|
+
Move back in the history.
|
577
|
+
Ctrl+W. Or right mouse button, on a tab button
|
578
|
+
Close the tab (unless this is the only tab).
|
579
|
+
Ctrl+L
|
580
|
+
Move the keyboard focus to the "address" box, where you can type a topic.
|
581
|
+
EOS
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
end # module Tkri
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tkri
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mooffie
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-12 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: fastri
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.3"
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email: mooffie@gmail.com
|
27
|
+
executables:
|
28
|
+
- tkri
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- README
|
35
|
+
- lib/tkri.rb
|
36
|
+
- bin/tkri
|
37
|
+
has_rdoc: false
|
38
|
+
homepage: http://rubyforge.org/projects/tkri/
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project: tkri
|
59
|
+
rubygems_version: 1.3.1
|
60
|
+
signing_key:
|
61
|
+
specification_version: 2
|
62
|
+
summary: GUI front-end to FastRI's or RI's executables.
|
63
|
+
test_files: []
|
64
|
+
|