rakpak 1.1.0 → 1.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.
- checksums.yaml +4 -4
- data/README.md +11 -5
- data/lib/rakpak/app.rb +53 -50
- data/lib/rakpak/browser.rb +86 -36
- data/lib/rakpak/dir_cache.rb +1 -3
- data/lib/rakpak/entry.rb +1 -4
- data/lib/rakpak/formats.rb +2 -16
- data/lib/rakpak/job.rb +12 -41
- data/lib/rakpak/modal.rb +70 -23
- data/lib/rakpak/plan.rb +70 -55
- data/lib/rakpak/screen.rb +2 -10
- data/lib/rakpak/sizer.rb +2 -6
- data/lib/rakpak/term.rb +3 -11
- data/lib/rakpak/text.rb +3 -12
- data/lib/rakpak/theme.rb +1 -2
- data/lib/rakpak/unpack.rb +10 -47
- data/lib/rakpak/version.rb +1 -1
- data/lib/rakpak.rb +5 -23
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2eb31aeb19c23cee757195d888132a931ea62030b0df07e88585c9e15fa5eea1
|
|
4
|
+
data.tar.gz: 9edb48577409aa19d2f1cb03115c881b2a7a1c755ccc8005d419e95428266db3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d629766dd61b2c108c34e500a38e73cb465262ec36454d1e8714572f6104359cecc8712fbef2314a784d2070681a15fa821c081e39e292eb42794bdd508e0f10
|
|
7
|
+
data.tar.gz: cd9fae98590a0d321f061588eebece2f015a74937a58c345a46d4f6b18b1a04492c85b7e1c11a474397d9fded85173bbb4d4fe1032974f03c3c33550eb08ffc4
|
data/README.md
CHANGED
|
@@ -76,13 +76,18 @@ exit status is non-zero if any of them failed.
|
|
|
76
76
|
3. **Set the flags.** Compression method, level, and the switches for that
|
|
77
77
|
format. Compressed tarballs default to gzip, the one every system can
|
|
78
78
|
read.
|
|
79
|
-
4. **
|
|
79
|
+
4. **Leave things out.** Type names separated by commas, like
|
|
80
|
+
`node_modules, .cache, *.log`. Each one is left out wherever it turns
|
|
81
|
+
up, along with everything inside it. Or pick *Tag files and directories*
|
|
82
|
+
to walk the folders you are packing and press `space` on the exact spots
|
|
83
|
+
to skip; `p` brings you back. Skipped when only files are packed.
|
|
84
|
+
5. **Say where.** This directory, your home directory, or one you type
|
|
80
85
|
into the field right there. `~`, `$HOME` and absolute paths all work,
|
|
81
86
|
and typing anything jumps to the field.
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
6. **Name it.** The extension is added for you.
|
|
88
|
+
7. **Confirm.** You see the exact command before anything runs. Only `enter`
|
|
84
89
|
starts it.
|
|
85
|
-
|
|
90
|
+
8. **Watch it, or press `b`** to drop it into the background and keep browsing.
|
|
86
91
|
|
|
87
92
|
If nothing is tagged, `p` archives whatever the cursor is on.
|
|
88
93
|
|
|
@@ -115,8 +120,9 @@ files that were already there are never touched.
|
|
|
115
120
|
| `ctrl-d` `ctrl-u` | half page |
|
|
116
121
|
| `gh` `~` / `gr` | home / root |
|
|
117
122
|
| `space` | tag or untag, then move down |
|
|
123
|
+
| `x` | exclude or include, then move down: left out of anything packed around it |
|
|
118
124
|
| `a` / `d` | tag / untag everything in this folder |
|
|
119
|
-
| `D` | clear all tags |
|
|
125
|
+
| `D` | clear all tags and exclusions |
|
|
120
126
|
| `T` | review tagged items (`space` removes) |
|
|
121
127
|
| `t` | show or hide the queue pane |
|
|
122
128
|
| `/` | filter this folder |
|
data/lib/rakpak/app.rb
CHANGED
|
@@ -11,8 +11,6 @@ require_relative "job"
|
|
|
11
11
|
require_relative "sizer"
|
|
12
12
|
|
|
13
13
|
module Rakpak
|
|
14
|
-
# Review panel for the tag set: the one place to see everything picked up
|
|
15
|
-
# across the filesystem, and drop entries without walking back to them.
|
|
16
14
|
class TagsModal < Modal
|
|
17
15
|
def initialize(tags:, sizer:)
|
|
18
16
|
super(title: "tagged", footer: "space/d remove · D clear all · esc back")
|
|
@@ -63,11 +61,9 @@ module Rakpak
|
|
|
63
61
|
|
|
64
62
|
class App
|
|
65
63
|
SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
66
|
-
WIZARD = %i[target options where output confirm].freeze
|
|
64
|
+
WIZARD = %i[target options exclude where output confirm].freeze
|
|
67
65
|
UNPACK = %i[dest_where dest_name unpack_confirm].freeze
|
|
68
66
|
|
|
69
|
-
# `pack` is a list of paths to tag before the first frame; when it is
|
|
70
|
-
# non-empty the archive prompts open immediately (rakpak -p).
|
|
71
67
|
def initialize(start_dir = Dir.home, pack: [])
|
|
72
68
|
@sizer = Sizer.new
|
|
73
69
|
@browser = Browser.new(start_dir, sizer: @sizer)
|
|
@@ -113,8 +109,7 @@ module Rakpak
|
|
|
113
109
|
|
|
114
110
|
private
|
|
115
111
|
|
|
116
|
-
#
|
|
117
|
-
# to remove its half-written archive before the terminal is handed back.
|
|
112
|
+
# Give jobs a moment to remove half-written archives before the terminal is restored.
|
|
118
113
|
def stop_jobs
|
|
119
114
|
running = @jobs.select(&:running?)
|
|
120
115
|
return if running.empty?
|
|
@@ -160,8 +155,6 @@ module Rakpak
|
|
|
160
155
|
"#{Text.bytes(j.output_size)} · #{Text.duration(j.elapsed)}#{more} [b] jobs", Theme::ACCENT]
|
|
161
156
|
end
|
|
162
157
|
|
|
163
|
-
# ------------------------------------------------------------- drawing
|
|
164
|
-
|
|
165
158
|
MIN_W = 40
|
|
166
159
|
MIN_H = 10
|
|
167
160
|
|
|
@@ -197,8 +190,6 @@ module Rakpak
|
|
|
197
190
|
@screen.put([(@screen.w - Text.width(want)) / 2, 0].max, (@screen.h / 2) + 1, want, Theme::DIM)
|
|
198
191
|
end
|
|
199
192
|
|
|
200
|
-
# Only the running state says which direction the work goes; the rest
|
|
201
|
-
# read the same either way.
|
|
202
193
|
def job_title(job, state)
|
|
203
194
|
case state
|
|
204
195
|
when :running then job.plan.gerund
|
|
@@ -218,7 +209,6 @@ module Rakpak
|
|
|
218
209
|
|
|
219
210
|
y = 2
|
|
220
211
|
job.plan.outputs.each do |out|
|
|
221
|
-
# A folder being extracted into has no size worth showing.
|
|
222
212
|
size = begin
|
|
223
213
|
File.directory?(out) ? nil : File.size(out)
|
|
224
214
|
rescue StandardError
|
|
@@ -260,7 +250,7 @@ module Rakpak
|
|
|
260
250
|
|
|
261
251
|
s.hline(0, y, s.w, Theme::BORDER)
|
|
262
252
|
y += 1
|
|
263
|
-
rows = [s.h - y - 1, 0].max
|
|
253
|
+
rows = [s.h - y - 1, 0].max
|
|
264
254
|
job.tail(rows).each_with_index do |line, i|
|
|
265
255
|
style = if line.start_with?("▸") then Theme::ACCENT
|
|
266
256
|
elsif line.match?(/error|cannot|denied|warning/i) then Theme::WARN
|
|
@@ -287,8 +277,6 @@ module Rakpak
|
|
|
287
277
|
end
|
|
288
278
|
end
|
|
289
279
|
|
|
290
|
-
# ------------------------------------------------------------ dispatch
|
|
291
|
-
|
|
292
280
|
def dispatch(key)
|
|
293
281
|
return modal_key(key) if @modal
|
|
294
282
|
return job_key(key) if @mode == :job
|
|
@@ -300,6 +288,8 @@ module Rakpak
|
|
|
300
288
|
when :tags then open_tags
|
|
301
289
|
when :help then open_help
|
|
302
290
|
when :jobs then open_jobs
|
|
291
|
+
when :resume then resume_wizard
|
|
292
|
+
when :unmarkable then notify("that one is being packed; untag it instead", Theme::WARN)
|
|
303
293
|
end
|
|
304
294
|
end
|
|
305
295
|
|
|
@@ -316,12 +306,12 @@ module Rakpak
|
|
|
316
306
|
end
|
|
317
307
|
when :enter
|
|
318
308
|
@mode = :browse unless job.running?
|
|
319
|
-
when "q" then request_quit
|
|
309
|
+
when "q" then request_quit
|
|
320
310
|
end
|
|
321
311
|
end
|
|
322
312
|
|
|
323
313
|
def modal_key(key)
|
|
324
|
-
#
|
|
314
|
+
# Raw mode delivers ctrl-c as a keystroke.
|
|
325
315
|
res = key == :ctrl_c ? :cancel : @modal.handle(key)
|
|
326
316
|
return if res.nil?
|
|
327
317
|
|
|
@@ -340,13 +330,12 @@ module Rakpak
|
|
|
340
330
|
return
|
|
341
331
|
end
|
|
342
332
|
|
|
343
|
-
|
|
333
|
+
# While exclusions are being marked the wizard is paused; help closing must not move it.
|
|
334
|
+
return if @wizard_pos.nil? || @browser.scoped?
|
|
344
335
|
|
|
345
336
|
res == :done ? wizard_forward(modal) : wizard_back
|
|
346
337
|
end
|
|
347
338
|
|
|
348
|
-
# -------------------------------------------------------------- wizard
|
|
349
|
-
|
|
350
339
|
def start_wizard
|
|
351
340
|
sel = @browser.selection
|
|
352
341
|
if sel.empty?
|
|
@@ -359,6 +348,7 @@ module Rakpak
|
|
|
359
348
|
sel.each { |p| @sizer.request(p) }
|
|
360
349
|
@plan = Plan.new(paths: sel, outdir: @browser.cwd)
|
|
361
350
|
@name_edited = false
|
|
351
|
+
@exclude_text = ""
|
|
362
352
|
@where = 0
|
|
363
353
|
@where_text = ""
|
|
364
354
|
@wizard = WIZARD
|
|
@@ -366,8 +356,6 @@ module Rakpak
|
|
|
366
356
|
open_wizard_step
|
|
367
357
|
end
|
|
368
358
|
|
|
369
|
-
# Unpacking follows the cursor rather than the tag set: an archive is one
|
|
370
|
-
# thing with one destination, not a pile to gather up.
|
|
371
359
|
def start_unpack
|
|
372
360
|
e = @browser.current
|
|
373
361
|
unless e && File.file?(e.path) && Unpack.archive?(e.path)
|
|
@@ -391,10 +379,6 @@ module Rakpak
|
|
|
391
379
|
open_wizard_step
|
|
392
380
|
end
|
|
393
381
|
|
|
394
|
-
# A lone file loses its extension ("notes.txt" becomes notes.tar.gz)
|
|
395
|
-
# unless the output is that file compressed on its own (notes.txt.gz).
|
|
396
|
-
# A folder keeps its name whole, dots and all. Several items take the
|
|
397
|
-
# folder name.
|
|
398
382
|
def default_basename(sel)
|
|
399
383
|
name = if sel.size > 1 then File.basename(@browser.cwd)
|
|
400
384
|
elsif File.directory?(sel.first) || @plan&.single_compress? then File.basename(sel.first)
|
|
@@ -419,8 +403,8 @@ module Rakpak
|
|
|
419
403
|
end
|
|
420
404
|
|
|
421
405
|
def wizard_forward(modal)
|
|
422
|
-
|
|
423
|
-
return if @wizard_pos
|
|
406
|
+
# A step may have ended the wizard itself, or paused it.
|
|
407
|
+
return if apply_step(@wizard[@wizard_pos], modal) == :paused || @wizard_pos.nil?
|
|
424
408
|
|
|
425
409
|
@wizard_pos += 1
|
|
426
410
|
open_wizard_step
|
|
@@ -446,6 +430,7 @@ module Rakpak
|
|
|
446
430
|
case step
|
|
447
431
|
when :target then target_modal
|
|
448
432
|
when :options then options_modal
|
|
433
|
+
when :exclude then exclude_modal
|
|
449
434
|
when :where then where_modal
|
|
450
435
|
when :output then output_modal
|
|
451
436
|
when :confirm then confirm_modal
|
|
@@ -458,11 +443,15 @@ module Rakpak
|
|
|
458
443
|
def apply_step(step, modal)
|
|
459
444
|
case step
|
|
460
445
|
when :target then @plan.target = modal.result
|
|
446
|
+
when :exclude
|
|
447
|
+
@exclude_text = modal.text
|
|
448
|
+
return pick_excludes if modal.result == :tag
|
|
449
|
+
|
|
450
|
+
@plan.excludes = Plan.parse_excludes(modal.text)
|
|
461
451
|
when :where
|
|
462
452
|
@where = modal.index
|
|
463
453
|
@where_text = modal.text
|
|
464
|
-
# ~ and $HOME
|
|
465
|
-
# even one with a dollar sign in its name.
|
|
454
|
+
# Only typed text expands ~ and $HOME; a browsed folder may contain a literal $.
|
|
466
455
|
@plan.outdir = @where == 2 ? Rakpak.expand_dir(modal.result) : modal.result
|
|
467
456
|
when :output
|
|
468
457
|
@name_edited = true
|
|
@@ -518,7 +507,6 @@ module Rakpak
|
|
|
518
507
|
end
|
|
519
508
|
end
|
|
520
509
|
|
|
521
|
-
# A codec, its level, then that format's switches.
|
|
522
510
|
def codec_form(side, title:, label:, flags_label:, flags_when_container: false)
|
|
523
511
|
rows = [
|
|
524
512
|
FormModal::Row.new(kind: :choice, label: label, hint: "", values: side.choices,
|
|
@@ -540,6 +528,25 @@ module Rakpak
|
|
|
540
528
|
end
|
|
541
529
|
end
|
|
542
530
|
|
|
531
|
+
# Nothing to exclude from inside a lone file.
|
|
532
|
+
def exclude_modal
|
|
533
|
+
return nil unless @plan.paths.any? { |p| File.directory?(p) }
|
|
534
|
+
|
|
535
|
+
@plan.excluded = @browser.excludes.to_a
|
|
536
|
+
ExcludeModal.new(text: @exclude_text,
|
|
537
|
+
marked: @plan.excluded.map { |p| @plan.relative(p) })
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def pick_excludes
|
|
541
|
+
@browser.scope_to(@plan.paths, @plan.base)
|
|
542
|
+
:paused
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def resume_wizard
|
|
546
|
+
@browser.leave_scope
|
|
547
|
+
open_wizard_step
|
|
548
|
+
end
|
|
549
|
+
|
|
543
550
|
def where_modal
|
|
544
551
|
WhereModal.new(here: @browser.cwd, home: Dir.home, index: @where, text: @where_text,
|
|
545
552
|
validate: method(:writable_folder))
|
|
@@ -557,7 +564,6 @@ module Rakpak
|
|
|
557
564
|
validate: method(:writable_folder), title: "unpack it where?")
|
|
558
565
|
end
|
|
559
566
|
|
|
560
|
-
# nil when there is no folder to name, which open_wizard_step skips over.
|
|
561
567
|
def dest_name_modal
|
|
562
568
|
return nil if @plan.single?
|
|
563
569
|
|
|
@@ -565,16 +571,12 @@ module Rakpak
|
|
|
565
571
|
value: @dest_name,
|
|
566
572
|
hint: "a new folder in #{Text.tilde(@dest_parent)} · . unpacks straight in",
|
|
567
573
|
validate: lambda { |name|
|
|
568
|
-
# The folder was chosen on the previous screen, so this
|
|
569
|
-
# is a bare name and can never reach outside it.
|
|
570
574
|
if name.include?("/") then "just a name, no slashes; the folder was picked already"
|
|
571
575
|
elsif name.include?("\0") || name == ".." then "not a valid name"
|
|
572
576
|
end
|
|
573
577
|
})
|
|
574
578
|
end
|
|
575
579
|
|
|
576
|
-
# "." is how you say "no subfolder, straight into the folder I picked",
|
|
577
|
-
# and a lone compressed file never has one to begin with.
|
|
578
580
|
def sync_dest
|
|
579
581
|
@plan.dest = @dest_name == "." ? @dest_parent : File.join(@dest_parent, @dest_name)
|
|
580
582
|
end
|
|
@@ -599,8 +601,6 @@ module Rakpak
|
|
|
599
601
|
value: @plan.basename,
|
|
600
602
|
hint: "in #{Text.tilde(@plan.outdir)} · #{@plan.ext} added automatically",
|
|
601
603
|
validate: lambda { |name|
|
|
602
|
-
# The folder was chosen on the previous screen; this is
|
|
603
|
-
# a bare filename, so it can never reach outside it.
|
|
604
604
|
if name.include?("/") then "just a name, no slashes; the folder was picked already"
|
|
605
605
|
elsif name.include?("\0") || [".", ".."].include?(name) then "not a valid name"
|
|
606
606
|
end
|
|
@@ -611,9 +611,12 @@ module Rakpak
|
|
|
611
611
|
lines = []
|
|
612
612
|
lines << [:head, "#{@plan.paths.size} item#{'s' unless @plan.paths.size == 1} " \
|
|
613
613
|
"from #{Text.tilde(@plan.base)}"]
|
|
614
|
-
@plan.members
|
|
615
|
-
|
|
616
|
-
|
|
614
|
+
lines.concat(listing(@plan.members))
|
|
615
|
+
if @plan.excludes.any? || @plan.excluded.any?
|
|
616
|
+
lines << [:head, "leaves out"]
|
|
617
|
+
lines << [:key, " #{@plan.excludes.join(', ')}"] if @plan.excludes.any?
|
|
618
|
+
lines.concat(listing(@plan.excluded.map { |p| @plan.relative(p) }))
|
|
619
|
+
end
|
|
617
620
|
lines << [:head, "writes #{Text.tilde(@plan.output)}"]
|
|
618
621
|
lines << [:key, ""]
|
|
619
622
|
@plan.preview.each do |label, cmd|
|
|
@@ -625,11 +628,16 @@ module Rakpak
|
|
|
625
628
|
warnings: @plan.warnings, errors: @plan.problems)
|
|
626
629
|
end
|
|
627
630
|
|
|
631
|
+
def listing(names)
|
|
632
|
+
lines = names.first(6).map { |m| [:key, " #{m}"] }
|
|
633
|
+
lines << [:key, " … #{names.size - 6} more"] if names.size > 6
|
|
634
|
+
lines << [:key, ""]
|
|
635
|
+
end
|
|
636
|
+
|
|
628
637
|
def finish_wizard
|
|
629
638
|
@wizard_pos = nil
|
|
630
639
|
end
|
|
631
640
|
|
|
632
|
-
# Always opens the job view; b from there sends it to the background.
|
|
633
641
|
def launch
|
|
634
642
|
job = Job.new(@plan, total_files: @plan.total_members(@sizer)).start
|
|
635
643
|
@jobs << job
|
|
@@ -654,8 +662,6 @@ module Rakpak
|
|
|
654
662
|
end
|
|
655
663
|
end
|
|
656
664
|
|
|
657
|
-
# --------------------------------------------------------------- panels
|
|
658
|
-
|
|
659
665
|
def open_tags
|
|
660
666
|
list = @browser.tags.to_a.sort
|
|
661
667
|
modal = TagsModal.new(tags: list, sizer: @sizer)
|
|
@@ -679,8 +685,9 @@ module Rakpak
|
|
|
679
685
|
["g g", "top"], ["G", "bottom"], ["ctrl-d ctrl-u", "half page"],
|
|
680
686
|
["g h", "home"], ["g r", "root"], ["~", "home"],
|
|
681
687
|
["space", "tag / untag, then move down"],
|
|
688
|
+
["x", "exclude / include, then move down"],
|
|
682
689
|
["a", "tag everything here"], ["d", "untag everything here"],
|
|
683
|
-
["D", "clear all tags"], ["T", "review tagged items"],
|
|
690
|
+
["D", "clear all tags and exclusions"], ["T", "review tagged items"],
|
|
684
691
|
["t", "show / hide the queue pane"],
|
|
685
692
|
["/", "filter this folder"], [".", "show hidden"],
|
|
686
693
|
["ctrl-r", "reload"],
|
|
@@ -714,16 +721,12 @@ module Rakpak
|
|
|
714
721
|
return if done.empty?
|
|
715
722
|
|
|
716
723
|
done.each do |j|
|
|
717
|
-
# Printed after the TUI
|
|
718
|
-
# or file name carrying an escape sequence must be defanged.
|
|
724
|
+
# Printed to the shell after the TUI exits, so defang escape sequences in names.
|
|
719
725
|
puts "#{Text.plain(j.plan.outputs.first)} #{Text.plain(j.plan.report_note)}"
|
|
720
726
|
end
|
|
721
727
|
end
|
|
722
728
|
end
|
|
723
729
|
|
|
724
|
-
# The option form follows the codec the user just picked: the level row
|
|
725
|
-
# takes that codec's range, and rows that only apply to a zip container
|
|
726
|
-
# (its switches) appear only while a container method is selected.
|
|
727
730
|
class DynamicForm < FormModal
|
|
728
731
|
def initialize(title:, rows:, side:, extra: [], extra_when_container: false)
|
|
729
732
|
super(title: title, rows: rows + extra)
|
data/lib/rakpak/browser.rb
CHANGED
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
4
|
require_relative "dir_cache"
|
|
5
|
+
require_relative "plan"
|
|
5
6
|
require_relative "theme"
|
|
6
7
|
require_relative "text"
|
|
7
8
|
|
|
8
9
|
module Rakpak
|
|
9
|
-
# The yazi-style three-pane file browser: parent, current, preview.
|
|
10
|
-
# Owns navigation and the tag set; knows nothing about archiving.
|
|
11
10
|
class Browser
|
|
12
|
-
|
|
11
|
+
# Marking exclusions mid-pack: walking stays under root, back is where to return.
|
|
12
|
+
Scope = Struct.new(:root, :paths, :back)
|
|
13
|
+
|
|
14
|
+
attr_reader :cwd, :tags, :excludes, :filter
|
|
13
15
|
|
|
14
16
|
def initialize(start_dir = Dir.pwd, sizer: nil)
|
|
15
17
|
@dirs = DirCache.new
|
|
16
18
|
@sizer = sizer
|
|
17
19
|
@tags = Set.new
|
|
20
|
+
@excludes = Set.new
|
|
21
|
+
@scope = nil
|
|
18
22
|
@cursor = {}
|
|
19
23
|
@show_hidden = false
|
|
20
24
|
@filter = nil
|
|
21
25
|
@filtering = false
|
|
22
|
-
@show_queue = true
|
|
26
|
+
@show_queue = true
|
|
23
27
|
@pending = nil
|
|
24
28
|
@preview_cache = {}
|
|
25
29
|
@cwd = resolve(start_dir)
|
|
@@ -31,10 +35,12 @@ module Rakpak
|
|
|
31
35
|
d
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
# ---------------------------------------------------------------- state
|
|
35
|
-
|
|
36
38
|
def entries
|
|
37
39
|
list = @dirs.list(@cwd, hidden: @show_hidden) || []
|
|
40
|
+
# Above the packed items, only they are reachable.
|
|
41
|
+
if @scope && @cwd == @scope.root && !@scope.paths.include?(@cwd)
|
|
42
|
+
list = list.select { |e| @scope.paths.include?(e.path) }
|
|
43
|
+
end
|
|
38
44
|
return list unless @filter && !@filter.empty?
|
|
39
45
|
|
|
40
46
|
needle = @filter.downcase
|
|
@@ -54,18 +60,19 @@ module Rakpak
|
|
|
54
60
|
def show_queue? = @show_queue
|
|
55
61
|
def filtering? = @filtering
|
|
56
62
|
|
|
63
|
+
def scoped? = !@scope.nil?
|
|
64
|
+
|
|
57
65
|
def tag(path)
|
|
58
66
|
path = File.expand_path(path)
|
|
67
|
+
@excludes.delete(path)
|
|
59
68
|
return unless @tags.add?(path)
|
|
60
69
|
|
|
61
|
-
# A figure may linger from a cursor-only pack of this path;
|
|
62
|
-
# must not trust it.
|
|
70
|
+
# A figure may linger from a cursor-only pack of this path; don't trust it.
|
|
63
71
|
@sizer&.forget(path)
|
|
64
72
|
@sizer&.request(path)
|
|
65
73
|
end
|
|
66
74
|
|
|
67
|
-
#
|
|
68
|
-
# its figure; re-tagging later must measure again, not serve a stale one.
|
|
75
|
+
# All tag removals must pass here so the sizer drops its now-stale figure.
|
|
69
76
|
def untag(path)
|
|
70
77
|
return unless @tags.delete?(path)
|
|
71
78
|
|
|
@@ -78,17 +85,55 @@ module Rakpak
|
|
|
78
85
|
@tags.include?(entry.path) ? untag(entry.path) : tag(entry.path)
|
|
79
86
|
end
|
|
80
87
|
|
|
88
|
+
def exclude(path)
|
|
89
|
+
path = File.expand_path(path)
|
|
90
|
+
untag(path)
|
|
91
|
+
@excludes << path
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def toggle_exclude(entry = current)
|
|
95
|
+
return unless entry
|
|
96
|
+
|
|
97
|
+
@excludes.delete?(entry.path) || exclude(entry.path)
|
|
98
|
+
end
|
|
99
|
+
|
|
81
100
|
def tag_all = entries.each { |e| tag(e.path) }
|
|
82
101
|
def untag_all_here = entries.each { |e| untag(e.path) }
|
|
83
|
-
|
|
102
|
+
|
|
103
|
+
def clear_tags
|
|
104
|
+
@tags.to_a.each { |p| untag(p) }
|
|
105
|
+
@excludes.clear
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# One packed folder opens inside it; several open at their common parent.
|
|
109
|
+
def scope_to(paths, base)
|
|
110
|
+
root = paths.size == 1 && File.directory?(paths.first) ? paths.first : base
|
|
111
|
+
@scope = Scope.new(root, paths, [@cwd, @cursor[@cwd], @filter])
|
|
112
|
+
@cwd = root
|
|
113
|
+
@filter = nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def leave_scope
|
|
117
|
+
@cwd, cursor, @filter = @scope.back
|
|
118
|
+
@cursor[@cwd] = cursor
|
|
119
|
+
@scope = nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def mark_in_scope
|
|
123
|
+
e = current
|
|
124
|
+
return unless e
|
|
125
|
+
return :unmarkable unless @scope.paths.any? { |p| Plan.inside?(e.path, p) }
|
|
126
|
+
|
|
127
|
+
toggle_exclude(e)
|
|
128
|
+
move(1)
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
84
131
|
|
|
85
132
|
def replace_tags(list)
|
|
86
133
|
(@tags.to_a - list).each { |p| untag(p) }
|
|
87
134
|
list.each { |p| tag(p) }
|
|
88
135
|
end
|
|
89
136
|
|
|
90
|
-
# Selections implied by the current state: explicit tags, else whatever
|
|
91
|
-
# the cursor is sitting on.
|
|
92
137
|
def selection
|
|
93
138
|
return @tags.to_a.sort if @tags.any?
|
|
94
139
|
|
|
@@ -104,14 +149,10 @@ module Rakpak
|
|
|
104
149
|
@tags.each { |p| @sizer.request(p) }
|
|
105
150
|
end
|
|
106
151
|
|
|
107
|
-
# ----------------------------------------------------------- navigation
|
|
108
|
-
|
|
109
152
|
def move(delta)
|
|
110
153
|
self.index = index + delta
|
|
111
154
|
end
|
|
112
155
|
|
|
113
|
-
# One step at a time wraps: up from the top lands on the last entry,
|
|
114
|
-
# down from the bottom on the first. Page moves still stop at the ends.
|
|
115
156
|
def step(delta)
|
|
116
157
|
n = entries.size
|
|
117
158
|
return if n.zero?
|
|
@@ -130,7 +171,7 @@ module Rakpak
|
|
|
130
171
|
end
|
|
131
172
|
|
|
132
173
|
def ascend
|
|
133
|
-
return if @cwd == "/"
|
|
174
|
+
return if @cwd == "/" || @cwd == @scope&.root
|
|
134
175
|
|
|
135
176
|
child = @cwd
|
|
136
177
|
@cwd = File.dirname(@cwd)
|
|
@@ -142,13 +183,13 @@ module Rakpak
|
|
|
142
183
|
def goto(dir)
|
|
143
184
|
d = File.expand_path(dir)
|
|
144
185
|
return false unless File.directory?(d) && File.readable?(d)
|
|
186
|
+
return false if @scope && d != @scope.root && !Plan.inside?(d, @scope.root)
|
|
145
187
|
|
|
146
188
|
@cwd = d
|
|
147
189
|
@filter = nil
|
|
148
190
|
true
|
|
149
191
|
end
|
|
150
192
|
|
|
151
|
-
# Open the folder holding `path` with the cursor on it.
|
|
152
193
|
def jump_to(path)
|
|
153
194
|
path = File.expand_path(path)
|
|
154
195
|
return false unless goto(File.dirname(path))
|
|
@@ -159,7 +200,6 @@ module Rakpak
|
|
|
159
200
|
!idx.nil?
|
|
160
201
|
end
|
|
161
202
|
|
|
162
|
-
# Returns :quit, :archive, :unpack, :tags, :help, :jobs or nil.
|
|
163
203
|
def handle(key)
|
|
164
204
|
return handle_filter(key) if @filtering
|
|
165
205
|
|
|
@@ -170,8 +210,15 @@ module Rakpak
|
|
|
170
210
|
when "h" then return goto(Dir.home) && nil
|
|
171
211
|
when "r" then return goto("/") && nil
|
|
172
212
|
end
|
|
173
|
-
#
|
|
174
|
-
|
|
213
|
+
# Not a chord: handle it as its own key so a stray g never swallows q, p or space.
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if @scope
|
|
217
|
+
case key
|
|
218
|
+
when :space, "x" then return mark_in_scope
|
|
219
|
+
when "p", :esc then return :resume
|
|
220
|
+
when "a", "d", "D", "u", "T", "b" then return nil
|
|
221
|
+
end
|
|
175
222
|
end
|
|
176
223
|
|
|
177
224
|
result = case key
|
|
@@ -188,6 +235,9 @@ module Rakpak
|
|
|
188
235
|
when :space
|
|
189
236
|
toggle_tag
|
|
190
237
|
move(1)
|
|
238
|
+
when "x"
|
|
239
|
+
toggle_exclude
|
|
240
|
+
move(1)
|
|
191
241
|
when "a" then tag_all
|
|
192
242
|
when "d" then untag_all_here
|
|
193
243
|
when "D" then clear_tags
|
|
@@ -238,8 +288,6 @@ module Rakpak
|
|
|
238
288
|
|
|
239
289
|
def page = @page_size || 10
|
|
240
290
|
|
|
241
|
-
# -------------------------------------------------------------- drawing
|
|
242
|
-
|
|
243
291
|
def draw(screen, status_line)
|
|
244
292
|
@page_size = screen.h - 5
|
|
245
293
|
draw_header(screen)
|
|
@@ -250,8 +298,6 @@ module Rakpak
|
|
|
250
298
|
draw_footer(screen, screen.h - 1, status_line)
|
|
251
299
|
end
|
|
252
300
|
|
|
253
|
-
# Left to right: the queue (when shown), parent, current, preview. The
|
|
254
|
-
# queue takes its slice first and the usual three share what is left.
|
|
255
301
|
def layout(w)
|
|
256
302
|
panes = []
|
|
257
303
|
x = 0
|
|
@@ -281,7 +327,10 @@ module Rakpak
|
|
|
281
327
|
def draw_header(screen)
|
|
282
328
|
screen.fill(0, 0, screen.w, 1, " ", Theme::HEAD)
|
|
283
329
|
screen.put(1, 0, "rakpak", Theme::HEAD + "\e[1;38;5;39m")
|
|
284
|
-
right =
|
|
330
|
+
right = if @scope then "marking exclusions"
|
|
331
|
+
elsif @excludes.any? then "#{tag_summary} · #{@excludes.size} excluded"
|
|
332
|
+
else tag_summary
|
|
333
|
+
end
|
|
285
334
|
avail = screen.w - 10 - Text.width(right) - 3
|
|
286
335
|
screen.put(9, 0, Text.fit_left(Text.tilde(@cwd), [avail, 1].max), Theme::HEAD + "\e[38;5;231m")
|
|
287
336
|
screen.put(screen.w - Text.width(right) - 1, 0, right,
|
|
@@ -309,9 +358,6 @@ module Rakpak
|
|
|
309
358
|
end
|
|
310
359
|
end
|
|
311
360
|
|
|
312
|
-
# Everything queued for the next pack, wherever on the disk it lives:
|
|
313
|
-
# the name, then where it is, with the size at the edge and the running
|
|
314
|
-
# total on top. Stays put while you keep browsing.
|
|
315
361
|
def draw_queue(screen, pane, y, h)
|
|
316
362
|
x = pane[:x]
|
|
317
363
|
w = pane[:w]
|
|
@@ -363,12 +409,16 @@ module Rakpak
|
|
|
363
409
|
row = y + i
|
|
364
410
|
sel = (top + i) == cur
|
|
365
411
|
tagged = @tags.include?(e.path)
|
|
412
|
+
excluded = @excludes.include?(e.path)
|
|
366
413
|
bg = if sel && active then Theme::CUR_BG
|
|
367
414
|
elsif sel then Theme::SEL_BG
|
|
368
415
|
end
|
|
369
416
|
screen.fill(x, row, w, 1, " ", bg) if bg
|
|
370
|
-
mark_style = tagged
|
|
371
|
-
|
|
417
|
+
mark, mark_style = if tagged then [" ▌", Theme::TAG]
|
|
418
|
+
elsif excluded then [" ✗", Theme::ERR]
|
|
419
|
+
else [" ", Theme::FAINT]
|
|
420
|
+
end
|
|
421
|
+
screen.put(x, row, mark, bg.to_s + mark_style)
|
|
372
422
|
name_style = bg ? bg + (sel && active ? "\e[1;38;5;231m" : e.style) : e.style
|
|
373
423
|
avail = w - 3
|
|
374
424
|
avail -= 5 unless pane[:kind] == :parent
|
|
@@ -423,8 +473,7 @@ module Rakpak
|
|
|
423
473
|
end
|
|
424
474
|
|
|
425
475
|
def file_preview(entry, w, h)
|
|
426
|
-
# Reading a FIFO or
|
|
427
|
-
# would freeze the UI with the cursor merely resting on the entry.
|
|
476
|
+
# Reading a FIFO or device blocks and would freeze the UI.
|
|
428
477
|
kind = entry.target_stat
|
|
429
478
|
return [["not a regular file", Theme::FAINT]] unless kind&.file?
|
|
430
479
|
|
|
@@ -444,7 +493,6 @@ module Rakpak
|
|
|
444
493
|
[[e.class.name.split("::").last, Theme::ERR]]
|
|
445
494
|
end
|
|
446
495
|
|
|
447
|
-
# Bytes per row adapts to the pane so the ASCII column always fits.
|
|
448
496
|
def hexdump(bytes, rows, width)
|
|
449
497
|
return [] if rows <= 0
|
|
450
498
|
|
|
@@ -489,6 +537,8 @@ module Rakpak
|
|
|
489
537
|
|
|
490
538
|
HINTS = [["space", "tag"], ["←→", "nav"], ["a", "all"], ["t", "queue"], ["/", "find"],
|
|
491
539
|
[".", "hidden"], ["p", "pack"], ["u", "unpack"], ["?", "help"]].freeze
|
|
540
|
+
SCOPE_HINTS = [["space", "exclude"], ["←→", "nav"], ["/", "find"], [".", "hidden"],
|
|
541
|
+
["p", "back to pack"], ["?", "help"]].freeze
|
|
492
542
|
|
|
493
543
|
def draw_footer(screen, y, status_line)
|
|
494
544
|
screen.fill(0, y, screen.w, 1, " ", nil)
|
|
@@ -497,7 +547,7 @@ module Rakpak
|
|
|
497
547
|
return
|
|
498
548
|
end
|
|
499
549
|
x = 1
|
|
500
|
-
HINTS.each do |k, label|
|
|
550
|
+
(@scope ? SCOPE_HINTS : HINTS).each do |k, label|
|
|
501
551
|
break if x + Text.width(k) + Text.width(label) + 3 > screen.w
|
|
502
552
|
|
|
503
553
|
x = screen.put(x, y, k, Theme::KEY)
|
data/lib/rakpak/dir_cache.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
require_relative "entry"
|
|
4
4
|
|
|
5
5
|
module Rakpak
|
|
6
|
-
# Directory listings, memoised per (path, show_hidden). Cheap enough that
|
|
7
|
-
# the parent/preview panes can ask on every keystroke.
|
|
8
6
|
class DirCache
|
|
9
7
|
MAX_ENTRIES = 20_000
|
|
10
8
|
|
|
@@ -31,7 +29,7 @@ module Rakpak
|
|
|
31
29
|
names.reject! { |n| n.start_with?(".") } unless hidden
|
|
32
30
|
names = names.first(MAX_ENTRIES)
|
|
33
31
|
names.map { |n| Entry.new(File.join(path, n), n) }.sort_by(&:sort_key)
|
|
34
|
-
rescue StandardError
|
|
32
|
+
rescue StandardError
|
|
35
33
|
nil
|
|
36
34
|
end
|
|
37
35
|
end
|