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
data/lib/rakpak/entry.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Rakpak
|
|
4
|
-
# One filesystem row. Stats are taken once, lazily, and never raise.
|
|
5
4
|
class Entry
|
|
6
5
|
attr_reader :path, :name
|
|
7
6
|
|
|
@@ -64,13 +63,11 @@ module Rakpak
|
|
|
64
63
|
Theme::NORMAL
|
|
65
64
|
end
|
|
66
65
|
|
|
67
|
-
#
|
|
68
|
-
# valid UTF-8 replaced so downcase cannot raise on them.
|
|
66
|
+
# Scrub invalid UTF-8 first, or downcase raises.
|
|
69
67
|
def fold
|
|
70
68
|
@fold ||= (@name.valid_encoding? ? @name : @name.scrub("?")).downcase
|
|
71
69
|
end
|
|
72
70
|
|
|
73
|
-
# Dirs first, then case-insensitive natural order.
|
|
74
71
|
def sort_key
|
|
75
72
|
[dir? ? 0 : 1, fold, @name]
|
|
76
73
|
end
|
data/lib/rakpak/formats.rb
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Rakpak
|
|
4
|
-
# What this machine can actually do. Nothing here is assumed; every codec
|
|
5
|
-
# is probed against PATH so the UI can grey out what is missing instead of
|
|
6
|
-
# failing halfway through a job.
|
|
7
4
|
module Tools
|
|
8
5
|
module_function
|
|
9
6
|
|
|
@@ -27,8 +24,7 @@ module Rakpak
|
|
|
27
24
|
@zip_bz2 = false
|
|
28
25
|
end
|
|
29
26
|
|
|
30
|
-
# GNU tar, bsdtar (macOS) and busybox tar disagree about flags.
|
|
31
|
-
# once so the UI only offers switches this tar actually understands.
|
|
27
|
+
# GNU tar, bsdtar (macOS) and busybox tar disagree about flags.
|
|
32
28
|
def tar_flavor
|
|
33
29
|
return @tar_flavor if defined?(@tar_flavor)
|
|
34
30
|
|
|
@@ -54,7 +50,6 @@ module Rakpak
|
|
|
54
50
|
|
|
55
51
|
end
|
|
56
52
|
|
|
57
|
-
# A tar compression backend.
|
|
58
53
|
Codec = Struct.new(:id, :label, :ext, :bin, :levels, :default, :threads, :blurb,
|
|
59
54
|
:level_opt, keyword_init: true) do
|
|
60
55
|
def available? = bin.nil? || Tools.available?(bin)
|
|
@@ -62,10 +57,8 @@ module Rakpak
|
|
|
62
57
|
def why_not = available? ? nil : "#{bin} not installed"
|
|
63
58
|
def container? = false
|
|
64
59
|
|
|
65
|
-
# ".tar.gz" for a tarball, ".gz" when compressing a lone file.
|
|
66
60
|
def single_ext = ext.delete_prefix(".tar")
|
|
67
61
|
|
|
68
|
-
# The compressor reading a named file and writing to stdout.
|
|
69
62
|
def argv(level)
|
|
70
63
|
return nil if bin.nil?
|
|
71
64
|
|
|
@@ -76,8 +69,7 @@ module Rakpak
|
|
|
76
69
|
parts
|
|
77
70
|
end
|
|
78
71
|
|
|
79
|
-
# tar
|
|
80
|
-
# The long form is understood by both GNU tar and bsdtar.
|
|
72
|
+
# tar splits --use-compress-program on spaces itself; GNU tar and bsdtar both accept it.
|
|
81
73
|
def filter(level) = argv(level)&.join(" ")
|
|
82
74
|
end
|
|
83
75
|
|
|
@@ -102,7 +94,6 @@ module Rakpak
|
|
|
102
94
|
|
|
103
95
|
def self.tar_codec(id) = TAR_CODECS.find { |c| c.id == id }
|
|
104
96
|
|
|
105
|
-
# A zip container written by Info-ZIP, with one of its entry methods.
|
|
106
97
|
ZipMethod = Struct.new(:id, :label, :flag, :levels, :default, :blurb, keyword_init: true) do
|
|
107
98
|
def available?
|
|
108
99
|
flag == "bzip2" ? Tools.zip_has_bzip2? : Tools.available?("zip")
|
|
@@ -127,15 +118,11 @@ module Rakpak
|
|
|
127
118
|
blurb: ".zip with no compression")
|
|
128
119
|
].freeze
|
|
129
120
|
|
|
130
|
-
# Everything the zip target can produce: a .zip of anything, or a lone
|
|
131
|
-
# file run through one compressor (notes.txt.gz).
|
|
132
121
|
COMPRESSORS = (ZIP_METHODS + TAR_CODECS.reject { |c| c.id == :none }).freeze
|
|
133
122
|
|
|
134
123
|
def self.compressor(id) = COMPRESSORS.find { |c| c.id == id }
|
|
135
124
|
|
|
136
|
-
# Toggleable switches, rendered as a checklist.
|
|
137
125
|
Flag = Struct.new(:id, :label, :args, :on, :blurb, :needs, keyword_init: true) do
|
|
138
|
-
# `needs` lists the tar flavours that understand this switch.
|
|
139
126
|
def supported?(flavor) = needs.nil? || needs.include?(flavor)
|
|
140
127
|
end
|
|
141
128
|
|
|
@@ -143,7 +130,6 @@ module Rakpak
|
|
|
143
130
|
BOTH = %i[gnu bsd unknown].freeze
|
|
144
131
|
ALL = %i[gnu bsd busybox unknown].freeze
|
|
145
132
|
|
|
146
|
-
# Only flags this machine's tar understands are offered.
|
|
147
133
|
def self.tar_flags(flavor = Tools.tar_flavor)
|
|
148
134
|
[
|
|
149
135
|
Flag.new(id: :verbose, label: "verbose", args: ["-v"], on: true, needs: ALL,
|
data/lib/rakpak/job.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
require_relative "plan"
|
|
4
4
|
|
|
5
5
|
module Rakpak
|
|
6
|
-
# Runs a plan's steps on a worker thread, streaming output into a ring
|
|
7
|
-
# buffer. The UI polls it; backgrounding is just "stop looking at it".
|
|
8
6
|
class Job
|
|
9
7
|
KEEP = 400
|
|
10
8
|
|
|
@@ -24,7 +22,7 @@ module Rakpak
|
|
|
24
22
|
@verbose = true
|
|
25
23
|
@cancel = false
|
|
26
24
|
@pid = nil
|
|
27
|
-
@spawned = nil
|
|
25
|
+
@spawned = nil
|
|
28
26
|
@started_at = nil
|
|
29
27
|
@finished_at = nil
|
|
30
28
|
end
|
|
@@ -57,8 +55,6 @@ module Rakpak
|
|
|
57
55
|
|
|
58
56
|
def total_files = @total_files
|
|
59
57
|
|
|
60
|
-
# nil when there is nothing to measure progress against: no file total,
|
|
61
|
-
# or a step whose tool is not listing members (verbose off).
|
|
62
58
|
def fraction
|
|
63
59
|
return nil unless @total_files&.positive? && @verbose
|
|
64
60
|
|
|
@@ -67,8 +63,7 @@ module Rakpak
|
|
|
67
63
|
[base + (per * [@file_count.to_f / @total_files, 1.0].min), 1.0].min
|
|
68
64
|
end
|
|
69
65
|
|
|
70
|
-
#
|
|
71
|
-
# being extracted into has only its own inode size, which means nothing.
|
|
66
|
+
# A directory's own size means nothing mid-extraction.
|
|
72
67
|
def output_size
|
|
73
68
|
out = writing_now
|
|
74
69
|
return nil if out.nil? || File.directory?(out)
|
|
@@ -78,8 +73,6 @@ module Rakpak
|
|
|
78
73
|
nil
|
|
79
74
|
end
|
|
80
75
|
|
|
81
|
-
# A step redirected to a file is writing that file; otherwise the tool is
|
|
82
|
-
# writing the plan's output itself.
|
|
83
76
|
def writing_now
|
|
84
77
|
i = [@step_index, @steps.size - 1].min
|
|
85
78
|
@steps[i]&.[](3) || @plan.outputs[[i, @plan.outputs.size - 1].min]
|
|
@@ -101,7 +94,6 @@ module Rakpak
|
|
|
101
94
|
kill_current
|
|
102
95
|
end
|
|
103
96
|
|
|
104
|
-
# Blocks until the worker is finished, for at most `secs`.
|
|
105
97
|
def wait(secs = nil) = @thread&.join(secs)
|
|
106
98
|
|
|
107
99
|
private
|
|
@@ -113,7 +105,7 @@ module Rakpak
|
|
|
113
105
|
return unless pid
|
|
114
106
|
|
|
115
107
|
begin
|
|
116
|
-
Process.kill("TERM", -pid) #
|
|
108
|
+
Process.kill("TERM", -pid) # whole group: tar and its compressor
|
|
117
109
|
rescue StandardError
|
|
118
110
|
nil
|
|
119
111
|
end
|
|
@@ -145,7 +137,7 @@ module Rakpak
|
|
|
145
137
|
@step_index = idx
|
|
146
138
|
@file_count = 0
|
|
147
139
|
@verbose = verbose
|
|
148
|
-
# A cancel
|
|
140
|
+
# A cancel between steps must not start the next one.
|
|
149
141
|
return finish(:cancelled) if @cancel
|
|
150
142
|
|
|
151
143
|
push("▸ #{label}: #{Plan.show_cmd(argv, stdout)}")
|
|
@@ -166,8 +158,7 @@ module Rakpak
|
|
|
166
158
|
end
|
|
167
159
|
|
|
168
160
|
def failure_message(label, status)
|
|
169
|
-
#
|
|
170
|
-
# case run_step already recorded the useful message.
|
|
161
|
+
# nil status: spawn failed and run_step already set @error.
|
|
171
162
|
return @error || "#{label} could not be started" if status.nil?
|
|
172
163
|
|
|
173
164
|
if status.signaled?
|
|
@@ -187,14 +178,8 @@ module Rakpak
|
|
|
187
178
|
@finished_at = now
|
|
188
179
|
end
|
|
189
180
|
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
# was actually interrupted. Archives finished by earlier steps are whole
|
|
193
|
-
# and must survive, and a step that never spawned wrote nothing.
|
|
194
|
-
#
|
|
195
|
-
# An extraction is the other way round: its output is a folder full of
|
|
196
|
-
# files that were there before, or are the part of the job that did
|
|
197
|
-
# work. Those are not ours to throw away.
|
|
181
|
+
# Remove only the interrupted step's half-written archive; earlier ones are whole.
|
|
182
|
+
# Extraction output may hold pre-existing files, so it is never removed.
|
|
198
183
|
def cleanup_incomplete
|
|
199
184
|
return unless @plan.clobbers_output?
|
|
200
185
|
|
|
@@ -207,15 +192,9 @@ module Rakpak
|
|
|
207
192
|
push("could not remove #{File.basename(path)}: #{e.message}")
|
|
208
193
|
end
|
|
209
194
|
|
|
210
|
-
# `stdout` names a file the command's output is the archive for (gzip -c);
|
|
211
|
-
# otherwise stdout joins stderr in the log.
|
|
212
195
|
def run_step(argv, stdout = nil)
|
|
213
196
|
out = stdout || @plan.outputs[@step_index]
|
|
214
|
-
#
|
|
215
|
-
# and the confirm screen has said an existing one will be overwritten.
|
|
216
|
-
# zip would otherwise update it in place, keeping members that no
|
|
217
|
-
# longer exist. An extraction writes into a folder instead, which must
|
|
218
|
-
# be left exactly as it is.
|
|
197
|
+
# zip would otherwise update an existing archive in place, keeping stale members.
|
|
219
198
|
clear_path(out) if @plan.clobbers_output?
|
|
220
199
|
@spawned = out
|
|
221
200
|
|
|
@@ -250,8 +229,7 @@ module Rakpak
|
|
|
250
229
|
rescue EOFError
|
|
251
230
|
break
|
|
252
231
|
end
|
|
253
|
-
#
|
|
254
|
-
# has to be made printable before it can meet the UTF-8 frame.
|
|
232
|
+
# Filenames are arbitrary bytes; scrub before they meet the UTF-8 frame.
|
|
255
233
|
buf << chunk.force_encoding(Encoding::UTF_8).scrub("·")
|
|
256
234
|
# tar emits newlines, zip rewrites lines with \r.
|
|
257
235
|
while (m = buf.match(/[\r\n]/))
|
|
@@ -269,11 +247,7 @@ module Rakpak
|
|
|
269
247
|
status
|
|
270
248
|
end
|
|
271
249
|
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
# lstat, not exist?: a symlink whose target is missing does not "exist",
|
|
275
|
-
# and leaving one here would mean opening the path later and writing
|
|
276
|
-
# through it to wherever it points. Remove the link itself.
|
|
250
|
+
# lstat: a dangling symlink is not exist?, and writing through it would escape the folder.
|
|
277
251
|
def clear_path(path)
|
|
278
252
|
return unless path
|
|
279
253
|
|
|
@@ -285,14 +259,11 @@ module Rakpak
|
|
|
285
259
|
File.unlink(path) unless st.directory?
|
|
286
260
|
end
|
|
287
261
|
|
|
288
|
-
#
|
|
289
|
-
# the spawn, anything already sitting there could be a symlink out of the
|
|
290
|
-
# folder. O_EXCL means we either create the file or refuse to write at
|
|
291
|
-
# all, and O_NOFOLLOW refuses a link even if one appears first.
|
|
262
|
+
# O_EXCL|O_NOFOLLOW: never write through a symlink planted after clear_path.
|
|
292
263
|
SINK_FLAGS = File::WRONLY | File::CREAT | File::EXCL |
|
|
293
264
|
(defined?(File::NOFOLLOW) ? File::NOFOLLOW : 0)
|
|
294
265
|
|
|
295
|
-
# 0666 so the result
|
|
266
|
+
# 0666 so the result respects umask, like a shell redirect.
|
|
296
267
|
def open_sink(path) = File.open(path, SINK_FLAGS, 0o666)
|
|
297
268
|
|
|
298
269
|
def record(line)
|
data/lib/rakpak/modal.rb
CHANGED
|
@@ -4,8 +4,6 @@ require_relative "theme"
|
|
|
4
4
|
require_relative "text"
|
|
5
5
|
|
|
6
6
|
module Rakpak
|
|
7
|
-
# Base class for the centred overlay panels. Subclasses fill the body;
|
|
8
|
-
# geometry, frame, title and footer are handled here.
|
|
9
7
|
class Modal
|
|
10
8
|
attr_reader :result
|
|
11
9
|
|
|
@@ -15,7 +13,6 @@ module Rakpak
|
|
|
15
13
|
@result = nil
|
|
16
14
|
end
|
|
17
15
|
|
|
18
|
-
# Desired [width, height] given the screen.
|
|
19
16
|
def dims(screen)
|
|
20
17
|
[[screen.w - 6, 72].min, [screen.h - 4, 20].min]
|
|
21
18
|
end
|
|
@@ -41,11 +38,9 @@ module Rakpak
|
|
|
41
38
|
|
|
42
39
|
def body(screen, x, y, w, h); end
|
|
43
40
|
|
|
44
|
-
# :done, :cancel or nil
|
|
45
41
|
def handle(_key) = nil
|
|
46
42
|
end
|
|
47
43
|
|
|
48
|
-
# A vertical list of choices, some of which may be unavailable.
|
|
49
44
|
class SelectModal < Modal
|
|
50
45
|
Item = Struct.new(:label, :value, :blurb, :enabled, :why, keyword_init: true)
|
|
51
46
|
|
|
@@ -122,7 +117,6 @@ module Rakpak
|
|
|
122
117
|
end
|
|
123
118
|
end
|
|
124
119
|
|
|
125
|
-
# A form of mixed rows: cycling choices, numeric ranges and toggles.
|
|
126
120
|
class FormModal < Modal
|
|
127
121
|
Row = Struct.new(:kind, :label, :hint, :get, :set, :values, keyword_init: true)
|
|
128
122
|
|
|
@@ -214,7 +208,6 @@ module Rakpak
|
|
|
214
208
|
end
|
|
215
209
|
end
|
|
216
210
|
|
|
217
|
-
# The reason the current settings cannot be run, or nil.
|
|
218
211
|
def unusable
|
|
219
212
|
@rows.each do |row|
|
|
220
213
|
next unless row.kind == :choice
|
|
@@ -269,8 +262,6 @@ module Rakpak
|
|
|
269
262
|
end
|
|
270
263
|
end
|
|
271
264
|
|
|
272
|
-
# Single-line text field with the editing keys people expect. `validate`
|
|
273
|
-
# is given the trimmed text and returns a reason to refuse it, or nil.
|
|
274
265
|
class InputModal < Modal
|
|
275
266
|
def initialize(title:, value: "", hint: "", footer: "enter accept · esc back", validate: nil)
|
|
276
267
|
super(title: title, footer: footer)
|
|
@@ -294,11 +285,10 @@ module Rakpak
|
|
|
294
285
|
field(screen, x, y + (@hint.empty? ? 0 : 2), w, active: true)
|
|
295
286
|
end
|
|
296
287
|
|
|
297
|
-
# The text box on its own, for embedding in another panel.
|
|
298
288
|
def field(screen, x, row, w, active: true)
|
|
299
289
|
screen.fill(x, row, w, 1, " ", Theme::SEL_BG)
|
|
300
290
|
width = w - 2
|
|
301
|
-
# Scroll
|
|
291
|
+
# Scroll by display columns, not characters.
|
|
302
292
|
off = 0
|
|
303
293
|
off += 1 while off < @cur && Text.width(@buf[off...@cur]) >= width
|
|
304
294
|
shown = +""
|
|
@@ -342,7 +332,9 @@ module Rakpak
|
|
|
342
332
|
when :ctrl_u then @buf.slice!(0, @cur) && (@cur = 0)
|
|
343
333
|
when :ctrl_k then @buf.slice!(@cur..)
|
|
344
334
|
when :ctrl_w
|
|
345
|
-
left = @buf[0, @cur].
|
|
335
|
+
left = @buf[0, @cur].rstrip
|
|
336
|
+
boundary = left.rindex(/\s/)
|
|
337
|
+
left = boundary ? left[0..boundary] : ""
|
|
346
338
|
@buf = left + (@buf[@cur..] || "")
|
|
347
339
|
@cur = left.length
|
|
348
340
|
when :space then insert(" ")
|
|
@@ -359,8 +351,6 @@ module Rakpak
|
|
|
359
351
|
end
|
|
360
352
|
end
|
|
361
353
|
|
|
362
|
-
# Where the archive goes: two ready-made folders and a field for any
|
|
363
|
-
# other. Typing anything moves to the field; 1, 2 and 3 pick directly.
|
|
364
354
|
class WhereModal < Modal
|
|
365
355
|
attr_reader :index
|
|
366
356
|
|
|
@@ -417,8 +407,6 @@ module Rakpak
|
|
|
417
407
|
nil
|
|
418
408
|
end
|
|
419
409
|
|
|
420
|
-
# Once the field is active, j, k and digits are text like anything
|
|
421
|
-
# else; arrows and tab still move between the choices.
|
|
422
410
|
def field_key(key)
|
|
423
411
|
@index = 2
|
|
424
412
|
@field.handle(key)
|
|
@@ -444,9 +432,71 @@ module Rakpak
|
|
|
444
432
|
end
|
|
445
433
|
end
|
|
446
434
|
|
|
447
|
-
#
|
|
448
|
-
|
|
449
|
-
|
|
435
|
+
# Result is the typed names, or :tag to go mark exclusions in the file view.
|
|
436
|
+
class ExcludeModal < Modal
|
|
437
|
+
def initialize(text:, marked:)
|
|
438
|
+
super(title: "exclude", footer: "↑↓ pick · enter continue · esc back")
|
|
439
|
+
@field = InputModal.new(title: "", value: text)
|
|
440
|
+
@marked = marked
|
|
441
|
+
@index = 0
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def text = @field.text
|
|
445
|
+
|
|
446
|
+
def dims(screen)
|
|
447
|
+
[[[screen.w - 6, 84].min, 44].max, [9 + [@marked.size, 1].max, screen.h - 2].min]
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def body(screen, x, y, w, h)
|
|
451
|
+
choice(screen, x, y, w, "1. Names", 0)
|
|
452
|
+
@field.field(screen, x + 12, y, w - 12, active: @index.zero?)
|
|
453
|
+
screen.put(x + 3, y + 1, Text.fit("commas between; each matches at any depth, with all it holds", w - 3),
|
|
454
|
+
Theme::MODAL_BG + Theme::DIM)
|
|
455
|
+
choice(screen, x, y + 2, w, "2. Tag files and directories", 1)
|
|
456
|
+
if @marked.empty?
|
|
457
|
+
screen.put(x, y + 4, "nothing marked", Theme::MODAL_BG + Theme::FAINT)
|
|
458
|
+
return
|
|
459
|
+
end
|
|
460
|
+
screen.put(x, y + 4, "marked", Theme::MODAL_BG + Theme::TITLE)
|
|
461
|
+
rows = h - 5
|
|
462
|
+
return if rows < 1
|
|
463
|
+
|
|
464
|
+
shown = @marked.size > rows ? @marked.first(rows - 1) : @marked
|
|
465
|
+
shown.each_with_index do |m, i|
|
|
466
|
+
screen.put(x + 3, y + 5 + i, Text.fit_left(m, w - 3), Theme::MODAL_BG + Theme::NORMAL)
|
|
467
|
+
end
|
|
468
|
+
return if shown.size == @marked.size
|
|
469
|
+
|
|
470
|
+
screen.put(x + 3, y + 5 + shown.size, "… #{@marked.size - shown.size} more", Theme::MODAL_BG + Theme::FAINT)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def choice(screen, x, y, w, label, i)
|
|
474
|
+
sel = i == @index
|
|
475
|
+
bg = sel ? Theme::CUR_BG : Theme::MODAL_BG
|
|
476
|
+
screen.fill(x - 1, y, w + 2, 1, " ", bg)
|
|
477
|
+
screen.put(x, y, label, bg + (sel ? "\e[1;38;5;231m" : Theme::NORMAL))
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def handle(key)
|
|
481
|
+
case key
|
|
482
|
+
when :esc then return :cancel
|
|
483
|
+
when :enter
|
|
484
|
+
@result = @index.zero? ? text : :tag
|
|
485
|
+
return :done
|
|
486
|
+
when :up, :down, :tab then @index = 1 - @index
|
|
487
|
+
else
|
|
488
|
+
# On row 2, j/k move; anything else is typing, so it lands in the field.
|
|
489
|
+
if @index == 1 && %w[j k].include?(key) then @index = 0
|
|
490
|
+
else
|
|
491
|
+
@index = 0
|
|
492
|
+
@field.handle(key)
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
nil
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Only enter runs it: a single letter (b reads as "back") is too easy to hit by mistake.
|
|
450
500
|
class ConfirmModal < Modal
|
|
451
501
|
def initialize(title:, lines:, warnings: [], errors: [], footer: nil)
|
|
452
502
|
super(title: title,
|
|
@@ -462,8 +512,7 @@ module Rakpak
|
|
|
462
512
|
[w, [h, screen.h - 2].min]
|
|
463
513
|
end
|
|
464
514
|
|
|
465
|
-
#
|
|
466
|
-
# panel is that you can read exactly what will run.
|
|
515
|
+
# Wrap, don't clip: the user must see exactly what will run.
|
|
467
516
|
def laid_out(w)
|
|
468
517
|
@lines.flat_map do |style, text|
|
|
469
518
|
if style == :cmd
|
|
@@ -514,8 +563,6 @@ module Rakpak
|
|
|
514
563
|
end
|
|
515
564
|
end
|
|
516
565
|
|
|
517
|
-
# Transient notice panel. Scrolls when the content is taller than the
|
|
518
|
-
# terminal, which the key list usually is on a short screen.
|
|
519
566
|
class MessageModal < Modal
|
|
520
567
|
def initialize(title:, lines:, style: Theme::ERR)
|
|
521
568
|
super(title: title, footer: "any key to dismiss")
|