dog 0.2.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.
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: []