tkri 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|