dog 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/bin/dog +30 -0
  3. data/lib/dog/map.rb +136 -0
  4. data/lib/dog/viewer.rb +503 -0
  5. data/lib/dog.rb +20 -0
  6. metadata +47 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6a288fcd6df7bf73823892332fdb68473ee4c38480d6db0225126d08d6aa32fe
4
+ data.tar.gz: d451489dfc83de0a0858bf5c374dc7ee82c50e913216fcf547a3c541ffaf1aa6
5
+ SHA512:
6
+ metadata.gz: 93100e702f623c5bc573987f0f661300d0dd5acfe297325ffb41277fba6d0401fdf841cef1684e96ae79c33337081d8cc9f73d9715bc374168cb799b01377cc4
7
+ data.tar.gz: f47071eb9d5710b24461220ca75ae7ca88f0d9af5261e0627b8ccc8e1c42c046851734cab1529cbf592bfaa96c89014a750d3cc4a5eb835b5a43d1e94c04afe9
data/bin/dog ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dog'
4
+ require 'optparse'
5
+
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: dog [options]"
8
+
9
+ opts.on("-lTOPIC", "--list=TOPIC", "List the link names in a given topic") do |t|
10
+ map = Dog::Map.new
11
+ map.load
12
+
13
+ if !map.has_topic?(t)
14
+ puts "Topic \"#{t}\" not found in map!"
15
+ exit
16
+ end
17
+
18
+ map.link_names(t).each do |name|
19
+ puts name
20
+ end
21
+ exit
22
+ end
23
+
24
+ opts.on("-h", "--help", "Prints this help message") do
25
+ puts opts
26
+ exit
27
+ end
28
+ end.parse!
29
+
30
+ Dog::Viewer.new.view
data/lib/dog/map.rb ADDED
@@ -0,0 +1,136 @@
1
+ require 'yaml'
2
+ require 'json'
3
+
4
+ module Dog
5
+ class Map
6
+ attr_accessor :topic_name
7
+
8
+ def initialize
9
+ @map = {"@home" => []}
10
+ @topic_name = "@home"
11
+ end
12
+
13
+ def name(topic: @topic_name, index:)
14
+ @map[topic][index]["name"]
15
+ end
16
+
17
+ def path(topic: @topic_name, index:)
18
+ @map[topic][index]["path"]
19
+ end
20
+
21
+ def count(topic = @topic_name)
22
+ @map[topic].length
23
+ end
24
+
25
+ def move_up(topic: @topic_name, index:)
26
+ if index > 0
27
+ @map[@topic_name][index],
28
+ @map[@topic_name][index - 1] =
29
+ @map[@topic_name][index - 1],
30
+ @map[@topic_name][index]
31
+ end
32
+ end
33
+
34
+ def move_down(topic: @topic_name, index:)
35
+ if index < count(@topic_name) - 1
36
+ @map[@topic_name][index],
37
+ @map[@topic_name][index + 1] =
38
+ @map[@topic_name][index + 1],
39
+ @map[@topic_name][index]
40
+ end
41
+ end
42
+
43
+ def has_topic?(name)
44
+ @map.has_key?(name)
45
+ end
46
+
47
+ def here
48
+ @map[@topic_name]
49
+ end
50
+
51
+ def remove_topic(topic = @topic)
52
+ @map.delete(@topic)
53
+ end
54
+
55
+ def remove_link(topic: @topic_name, name:)
56
+ here.delete_if do |link|
57
+ link["name"] == name
58
+ end
59
+ end
60
+
61
+ def replace(topic: @topic_name, name:, new_name:, new_path:)
62
+ index = @map[@topic_name].index do |link|
63
+ link["name"] == name
64
+ end
65
+
66
+ if index
67
+ here[index] = create_link(new_name, new_path)
68
+ end
69
+ end
70
+
71
+ def add(topic: @topic_name, name:, path:)
72
+ @map[@topic_name].push(
73
+ create_link(name,path)
74
+ )
75
+ end
76
+
77
+ def navigate( destination )
78
+ if !@map[destination]
79
+ @map[destination] = []
80
+ end
81
+
82
+ @topic_name = destination
83
+ end
84
+
85
+ def load
86
+ begin
87
+ data = YAML.load_file(MAP_FILENAME)
88
+ @map = data if data
89
+ rescue Errno::ENOENT, Errno::ENOTDIR
90
+ return false
91
+ end
92
+ return true
93
+ end
94
+
95
+ def save
96
+ File.write(MAP_FILENAME, @map.to_yaml)
97
+ end
98
+
99
+ def [](key)
100
+ @map[key]
101
+ end
102
+
103
+ def find_incoming_links(destination)
104
+ links = []
105
+ @map.each do |key, value|
106
+ value.each do |item|
107
+ if item["path"] == destination
108
+ if key == @topic_name
109
+ links.push(
110
+ create_link("#{key} (<<)", key)
111
+ )
112
+ else
113
+ links.push(
114
+ create_link(key, key)
115
+ )
116
+ end
117
+ end
118
+ end
119
+ end
120
+ return links
121
+ end
122
+
123
+ def link_names(topic)
124
+ @map[topic].map{|l| l["name"]}
125
+ end
126
+
127
+ private
128
+
129
+ def create_link(name, path)
130
+ {
131
+ "name" => name,
132
+ "path" => path
133
+ }
134
+ end
135
+ end
136
+ end
data/lib/dog/viewer.rb ADDED
@@ -0,0 +1,503 @@
1
+ require 'dog'
2
+ require "curses"
3
+ require "uri"
4
+ require 'digest'
5
+ require 'fileutils'
6
+ require 'yaml'
7
+ require 'json'
8
+
9
+ include Curses
10
+
11
+ module Dog
12
+ class Viewer
13
+ def initialize
14
+ @map = Dog::Map.new
15
+
16
+ @first_time = @map.load
17
+ initialize_handlers
18
+
19
+ @topic = Window.new(1,cols,0,0)
20
+
21
+ colwidth = (cols/2).ceil + 1
22
+ @left = Window.new(lines/3, colwidth, 1, 0)
23
+ @right = Window.new(lines/3, colwidth, 1, cols-colwidth)
24
+
25
+ @link = Window.new(1,cols,lines/3+1,0)
26
+ @preview = Window.new(lines - lines/3 - 3,cols,lines/3+2,0)
27
+ @status = Window.new(1,cols,lines-1,0)
28
+ end
29
+
30
+ def initialize_handlers
31
+ if !load_handlers
32
+ @link_handlers = {
33
+ ".txt$" => {
34
+ "preview" => "cat %%",
35
+ "open" => "${EDITOR:-vim} %%"
36
+ },
37
+ "/$" => {
38
+ "preview" => "ls -lah %%",
39
+ "open" => "${EDITOR:-vim} %%"
40
+ },
41
+ "^http" => {
42
+ "preview" => "echo \"WIP!\"",
43
+ "open" => "lynx %%"
44
+ # "preview" => ->(x) {
45
+ # md5 = Digest::MD5.hexdigest(x)
46
+ # uri = URI(x)
47
+
48
+ # if !Dir.exist?("#{Dir.home}/.dog/cache/#{md5}")
49
+ # @preview.setpos(0,0)
50
+ # @preview.addstr("caching html and loading preview...")
51
+ # @preview.refresh
52
+
53
+ # FileUtils.mkdir_p "#{Dir.home}/.dog/cache/#{md5}"
54
+ # `wget -P #{Dir.home}/.dog/cache/#{md5} -p -q -k #{x} -U "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0"`
55
+ # end
56
+
57
+ # @preview.setpos(0,0)
58
+ # @preview.addstr(`lynx #{Dir.home}/.dog/cache/#{md5}/#{uri.host}/#{uri.path} -dump -force_html`)
59
+ # },
60
+ # "open" => ->(x) {
61
+ # md5 = Digest::MD5.hexdigest(x)
62
+ # uri = URI(x)
63
+ # system( "open -a /Applications/Firefox.app #{Dir.home}/.dog/cache/#{md5}/#{uri.host}/#{uri.path}" )
64
+ # }
65
+ }
66
+ }
67
+ end
68
+ end
69
+
70
+ def load_handlers
71
+ begin
72
+ data = YAML.load_file(HANDLERS_FILENAME)
73
+ @link_handlers = data if data
74
+ rescue Errno::ENOENT, Errno::ENOTDIR
75
+ return false
76
+ end
77
+ return true
78
+ end
79
+
80
+ def save_handlers
81
+ File.write(HANDLERS_FILENAME, @link_handlers.to_yaml)
82
+ end
83
+
84
+ def clear_window( window )
85
+ window.clear
86
+ if [@topic, @link, @status].include? window
87
+ window.box(' ', nil, ' ')
88
+ elsif [@left, @right].include? window
89
+ window.box(nil, ' ')
90
+ end
91
+ end
92
+
93
+ def resize
94
+ colwidth = (cols/2).ceil + 1
95
+
96
+ @topic.resize(1, cols)
97
+ @left.resize(lines/3, colwidth)
98
+ @right.resize(lines/3, colwidth)
99
+
100
+ @link.resize(1, cols)
101
+ @preview.resize(2*lines/3-2, cols)
102
+ @status.resize(1, cols)
103
+
104
+ @right.move(1, cols-colwidth)
105
+
106
+ @link.move(lines/3+1, 0)
107
+ @preview.move(lines/3+2, 0)
108
+ @status.move(lines-1, 0)
109
+
110
+ @windows.each do |window| window.refresh end
111
+ navigate( @map.topic_name )
112
+ end
113
+
114
+ def centerx( text = "" )
115
+ cols/2 - (text&.length || 0)/2
116
+ end
117
+
118
+ def add_menu_item( window, position, text )
119
+ window.setpos(position, 2)
120
+ window.attrset( window == @active_window && @active_item == position ? A_STANDOUT : A_NORMAL )
121
+ window.addstr( text )
122
+ window.attrset( A_NORMAL )
123
+ end
124
+
125
+ def setup_menu( window, location )
126
+ clear_window window
127
+ location&.each_with_index do |link, index|
128
+ add_menu_item( window, index, link["name"] )
129
+ end
130
+ window.refresh
131
+ end
132
+
133
+ def wrap_cursor
134
+ @active_item = @max_items - 1 if @active_item < 0
135
+ @active_item = 0 if @active_item >= @max_items
136
+ end
137
+
138
+ def active_link
139
+ wrap_cursor
140
+ if @max_items == 0
141
+ return nil
142
+ end
143
+
144
+ if @active_window == @left
145
+ @incoming[@active_item]["path"]
146
+ else
147
+ @map.path(index: @active_item)
148
+ end
149
+ end
150
+
151
+ def preview_active_file
152
+ preview_file( active_link )
153
+ end
154
+
155
+ def move_cursor( dir )
156
+ @active_item += dir
157
+
158
+ wrap_cursor
159
+
160
+ preview_active_file
161
+ end
162
+
163
+ def set_topic
164
+ show(@topic, @map.topic_name)
165
+ end
166
+
167
+ def swap_window
168
+ if @active_window == @left
169
+ @active_window = @right
170
+ @max_items = @map.count
171
+ else
172
+ if @incoming.length > 0
173
+ @active_window = @left
174
+ @max_items = @incoming.length
175
+ end
176
+ end
177
+ wrap_cursor
178
+ preview_active_file
179
+ end
180
+
181
+ def set_max_items
182
+ if @active_window == @left
183
+ @max_items = @incoming.length
184
+ else
185
+ @max_items = @map.count
186
+ end
187
+ end
188
+
189
+ def navigate( destination )
190
+ @incoming = @map.find_incoming_links(destination)
191
+
192
+ @map.navigate( destination )
193
+ refresh_display
194
+ end
195
+
196
+ def refresh_display
197
+ set_topic
198
+ setup_menu( @left, @incoming )
199
+ setup_menu( @right, @map.here )
200
+
201
+ if ( @incoming.length == 0 )
202
+ @active_window = @right
203
+ end
204
+ set_max_items
205
+
206
+ @active_item = 0
207
+
208
+ preview_file( active_link )
209
+ end
210
+
211
+ def show( window, text )
212
+ clear_window window
213
+
214
+ if window == @link || window == @topic || window == @status
215
+ text = " #{text} "
216
+ window.setpos(0, centerx(text))
217
+ elsif window == @preview
218
+ window.setpos(0, 0)
219
+ else
220
+ window.setpos(1, 1)
221
+ end
222
+
223
+ window.addstr text
224
+ window.refresh
225
+ end
226
+
227
+
228
+ def get_handler( filename )
229
+ @link_handlers.each do |regex, value|
230
+ if filename =~ Regexp.new(regex)
231
+ return value
232
+ end
233
+ end
234
+ return nil
235
+ end
236
+
237
+ def preview_file( filename )
238
+ if !filename || filename == ""
239
+ show(@link, "(empty!)")
240
+ clear_window @preview
241
+ @preview.refresh
242
+ return
243
+ end
244
+
245
+ show(@link, filename)
246
+
247
+ clear_window @preview
248
+
249
+ begin
250
+ handler = get_handler( filename )
251
+ if handler
252
+ @preview.setpos(0,0)
253
+ @preview.addstr(
254
+ `#{handler["preview"].gsub('%%',filename)}`
255
+ )
256
+ elsif filename[0] == '@'
257
+ if @map.has_topic?(filename)
258
+ @preview.setpos(0,0)
259
+ @preview.addstr(@map.link_names(filename).join("\n"))
260
+ else
261
+ @preview.setpos(0,0)
262
+ @preview.addstr("Topic not found! Open to create.")
263
+ end
264
+ else
265
+ @preview.setpos(0,0)
266
+ @preview.addstr("No matching preview handler found")
267
+ end
268
+ rescue => e
269
+ @preview.setpos(0,0)
270
+ @preview.addstr(" Error loading preview for #{filename}!\n#{e}")
271
+ end
272
+
273
+ @preview.refresh
274
+ end
275
+
276
+ def open( filename )
277
+ begin
278
+ handler = get_handler( filename )
279
+ if handler
280
+ system("#{handler["open"].gsub('%%',filename)}")
281
+ refresh_display
282
+ elsif filename[0] == '@'
283
+ navigate( filename )
284
+ else
285
+ @preview.setpos(0,0)
286
+ @preview.addstr("No matching open handler found")
287
+ end
288
+ rescue => e
289
+ @preview.setpos(0,0)
290
+ @preview.addstr(" Error opening #{filename}!\n#{e}")
291
+ end
292
+ end
293
+
294
+ def menu_loop
295
+ show(@status, "tab - next field | enter - commit | esc - cancel")
296
+ curs_set(1)
297
+ while fch = @preview.getch
298
+ begin
299
+ case fch
300
+ when KEY_RESIZE
301
+ resize
302
+ when 9
303
+ @form.driver(REQ_NEXT_FIELD)
304
+ when KEY_DOWN
305
+ @form.driver(REQ_NEXT_FIELD)
306
+ when 353
307
+ @form.driver(REQ_PREV_FIELD)
308
+ when KEY_UP
309
+ @form.driver(REQ_PREV_FIELD)
310
+ when 10
311
+ @form.driver(REQ_VALIDATION)
312
+ @form.unpost
313
+
314
+ if @editing
315
+ @map.replace(
316
+ name: @map.name(index: @active_item),
317
+ new_name: @name_field.buffer(0).strip,
318
+ new_path: @link_field.buffer(0).strip
319
+ )
320
+ else
321
+ @map.add(
322
+ name: @name_field.buffer(0).strip,
323
+ path: @link_field.buffer(0).strip
324
+ )
325
+ end
326
+ refresh_display
327
+ show(@status, "done")
328
+ break
329
+ when 127
330
+ @form.driver(REQ_DEL_PREV)
331
+ when KEY_LEFT
332
+ @form.driver(REQ_PREV_CHAR)
333
+ when KEY_RIGHT
334
+ @form.driver(REQ_NEXT_CHAR)
335
+ when 27
336
+ @form.unpost
337
+ refresh_display
338
+ show(@status, "cancelled")
339
+ break
340
+ else
341
+ fch.to_s.each_char do |letter|
342
+ @form.driver(letter)
343
+ end
344
+ end
345
+ rescue Curses::RequestDeniedError
346
+ end
347
+ end
348
+ curs_set(0)
349
+ end
350
+
351
+
352
+ def view
353
+ init_screen
354
+ start_color
355
+ curs_set(0)
356
+ noecho
357
+ cbreak
358
+
359
+ @preview.keypad(true)
360
+
361
+ @windows = [@topic, @left, @right, @link, @preview, @status]
362
+ @windows.each do |window|
363
+ clear_window window
364
+ end
365
+
366
+ @active_window = @right
367
+ @active_item = 0
368
+ @max_items = 0
369
+ @incoming = []
370
+
371
+ @name_field = Field.new( 1, cols - 4, 2, 2, 0, 0 )
372
+ @name_field.set_back(A_UNDERLINE)
373
+ @name_field.opts_off(O_BLANK | O_STATIC)
374
+ @link_field = Field.new( 1, cols - 4, 5, 2, 0, 0 )
375
+ @link_field.set_back(A_UNDERLINE)
376
+ @link_field.opts_off(O_BLANK | O_STATIC)
377
+
378
+ @form = Form.new( [@name_field, @link_field] )
379
+ @form.set_win @preview
380
+
381
+ @editing = false
382
+
383
+
384
+ refresh_display
385
+ show(@status, "woof! - press ? for help")
386
+
387
+ while ch = @right.getch
388
+ show(@status, "woof! - press ? for help")
389
+
390
+ case ch
391
+ when KEY_RESIZE
392
+ resize
393
+ when 's'
394
+ @map.save
395
+ save_handlers
396
+ show(@status, "saved!")
397
+ when 'J'
398
+ if @active_window == @right
399
+ @map.move_down(index: @active_item)
400
+ move_cursor( 1 )
401
+ end
402
+ when 'K'
403
+ if @active_window == @right
404
+ @map.move_up(index: @active_item)
405
+ move_cursor( -1 )
406
+ end
407
+ when 'j'
408
+ move_cursor( 1 )
409
+ when 'k'
410
+ move_cursor( -1 )
411
+ when 'h'
412
+ swap_window
413
+ when 'l'
414
+ swap_window
415
+ when ' '
416
+ open(active_link)
417
+ when '?'
418
+ show(@preview, HELP_TEXT)
419
+ when 'q'
420
+ break
421
+ when 'n'
422
+ clear_window @preview
423
+ @name_field.set_buffer(0, "")
424
+ @link_field.set_buffer(0, "")
425
+ @form.post
426
+ @preview.setpos(1,0)
427
+ @preview.addstr("name:")
428
+ @preview.setpos(4,0)
429
+ @preview.addstr("link:")
430
+ @preview.refresh
431
+ show(@link, "(new link)")
432
+
433
+ @editing = false
434
+
435
+ @form.driver(REQ_FIRST_FIELD)
436
+
437
+ menu_loop
438
+ when 'e'
439
+ if @active_window == @right
440
+ clear_window @preview
441
+ @name_field.set_buffer(0, @map.name(index: @active_item))
442
+ @link_field.set_buffer(0, @map.path(index: @active_item))
443
+ @form.post
444
+ @preview.setpos(1,0)
445
+ @preview.addstr("name:")
446
+ @preview.setpos(4,0)
447
+ @preview.addstr("link:")
448
+ @preview.refresh
449
+ show(@link, "(edit link)")
450
+
451
+ @editing = true
452
+
453
+ @form.driver(REQ_FIRST_FIELD)
454
+
455
+ menu_loop
456
+ end
457
+ when 'd'
458
+ if @active_window == @right
459
+ clear_window @preview
460
+ @preview.setpos(1,0)
461
+ @preview.addstr("really delete link \"#{@map.name(index: @active_item)}\"?")
462
+ @preview.setpos(3,0)
463
+ @preview.addstr("y / n")
464
+ @preview.refresh
465
+ if @preview.getch == 'y'
466
+ if active_link[0] == "@" && @map.find_incoming_links( active_link ).length == 1
467
+ target = @map.path(index: @active_item)
468
+ clear_window @preview
469
+ @preview.setpos(1,0)
470
+ @preview.addstr("this change strands the topic \"#{target}\".")
471
+ @preview.setpos(2,0)
472
+ @preview.addstr("do you want to delete that topic, too?")
473
+ @preview.setpos(3,0)
474
+ @preview.addstr("y / n")
475
+ @preview.refresh
476
+
477
+ if @preview.getch == 'y'
478
+ @map.remove_topic(target)
479
+ end
480
+ end
481
+
482
+ @map.remove_link(name: @map.name(index: @active_item))
483
+ end
484
+ @max_items = @map.here.length
485
+ wrap_cursor
486
+ preview_active_file
487
+ end
488
+ end
489
+
490
+ if @active_window == @left
491
+ setup_menu( @right, @map.here )
492
+ setup_menu( @left, @incoming )
493
+ else
494
+ setup_menu( @left, @incoming )
495
+ setup_menu( @right, @map.here )
496
+ end
497
+ @status.refresh
498
+ @active_window.refresh
499
+ end
500
+
501
+ end
502
+ end
503
+ end
data/lib/dog.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'dog/viewer'
2
+ require 'dog/map'
3
+
4
+ module Dog
5
+ MAP_FILENAME = "#{Dir.home}/.dog/map.yaml"
6
+ HANDLERS_FILENAME = "#{Dir.home}/.dog/handlers.yaml"
7
+ WELCOME_TEXT = "welcome to dog! this is your home topic. you can create a new link by pressing [n].\n\n try making a link that points to \"@newtopic\" to create a new topic!"
8
+ HELP_TEXT = <<~EOS
9
+ hjkl - move cursor
10
+ space - open link
11
+ n - new link
12
+ e - edit link
13
+ d - delete link
14
+ K - move link up within topic
15
+ J - move link down within topic
16
+ s - save map
17
+ q - quit
18
+ ? - show this help!
19
+ EOS
20
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - scout
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Bookmark and organize local and remote files
14
+ email: scout@bittybunny.com
15
+ executables:
16
+ - dog
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/dog
21
+ - lib/dog.rb
22
+ - lib/dog/map.rb
23
+ - lib/dog/viewer.rb
24
+ homepage: https://rubygems.org/gems/dog
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.4.10
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: Digital OrGanizer
47
+ test_files: []