squared 0.4.7 → 0.4.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cf1642a52eeb080a0aa4dadb9c7fb5a3060612904bd06a8161ab18b4e333a9a
4
- data.tar.gz: 7bad82be155782b37036cc2d447395a37585ed33b4d37c5281e3ccd3f902ea33
3
+ metadata.gz: 81e1fa9c773a59c8cb351dec47eb27b4c858c6266c83bd14be02c621cf68ad57
4
+ data.tar.gz: 23ccad44a3f5fbc1ade8d5950692fd401566f862b0131debebdd9d5d1a4d6dab
5
5
  SHA512:
6
- metadata.gz: 77fda3077ec4c929fad3003d7255d5aeb7b0f7fa2d14a717678772572a3a972214247aabc6911d8e717103b855ae1109c81c118ca5564f46fd4e79ecfc62cf6a
7
- data.tar.gz: 29e84ae67dc2aad4ac61c99407443d65210f96af60649784c9264a75c686473b919328a9ea6d97b6bda6e4fd61aeaa97df4e196bdf87f6e32819dfea1306c76a
6
+ metadata.gz: '00945ddcf48ae0482bdb40f143117ae464a03491fadf6c33b2e34c9f50b4d628c4b72b43c1a388e2c8c175067f62970c7ac60721330c1340a48103c4b972b560'
7
+ data.tar.gz: d3846c85547ee3acb6d040703d21b7a61630f502c17da71f8d0ddf9032c67930819b933b3c3896a0e2b23c2664bfc2ed5454d95313849a9885c429d0cfacb5f1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,105 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.9] - 2025-04-27
4
+
5
+ ### Added
6
+
7
+ - Git command reset action commit is interactive.
8
+ - Git command commit action all is interactive.
9
+ - Git command log and diff commit based actions are interactive.
10
+ - Git command branch action move and copy are interactive.
11
+ - Git command rebase and merge are interactive.
12
+ - Git command checkout action detach is interactive.
13
+ - Git command branch action delete for remotes is interactive.
14
+ - Git command restore action source was created.
15
+ - Git command tag actions can choose a remote to push.
16
+ - Git command switch was implemented.
17
+ - Git command branch action create is interactive.
18
+ - Git command reset action mode and patch are interactive.
19
+ - Git command restore action staged and worktree are interactive.
20
+ - Git command log and checkout commit based actions are interactive.
21
+ - Ruby command ruby action script is interactive.
22
+ - Ruby command irb is interactive.
23
+ - Git command commit action fixup was implemented.
24
+ - Git command commit action all can accept add command options.
25
+ - Git command branch action create and track can upload tracking.
26
+ - Git command add was implemented.
27
+ - Python command venv action run is interactive.
28
+
29
+ ### Changed
30
+
31
+ - Ruby depend with bundler excludes development group in production.
32
+ - OptionPartition class first and pass parameters were combined.
33
+ - Git command stash action list can accept log options.
34
+ - Git command branch action set was renamed track.
35
+ - Ruby autodetect can specify version manager for local project.
36
+ - Git pattern matching for HEAD was revised.
37
+
38
+ ### Removed
39
+
40
+ - Git command branch action edit is not commonly used.
41
+ - Git command rev action branch only echoed valid branches.
42
+ - Git command rev action parseopt is not relevant to a workspace.
43
+
44
+ ## [0.3.10] - 2025-04-27
45
+
46
+ ### Fixed
47
+
48
+ - Git command reset action commit used invalid delimeter.
49
+ - Git single option values were not merged as per specification.
50
+ - Project script task did not fire first and last callbacks.
51
+ - Git checkout action commit did not include options.
52
+
53
+ ### Removed
54
+
55
+ - Git command files action ignored could not be used alone.
56
+
57
+ ## [0.2.10] - 2025-04-27
58
+
59
+ ### Fixed
60
+
61
+ - Project run and script tasks did not fire first and last callbacks.
62
+ - Git command clone did not read booleans for recurse-submodules.
63
+
64
+ ## [0.1.7] - 2025-04-27
65
+
66
+ ### Fixed
67
+
68
+ - Project directory context method option pass was inverted.
69
+ - Shell options with spaces and without quotes were not escaped.
70
+ - Git task status did not display branch information.
71
+ - Ruby copy method argument include was ignored when used directly.
72
+ - Git commit could not push branch without same name as remote.
73
+
74
+ ## [0.4.8] - 2025-04-21
75
+
76
+ ### Added
77
+
78
+ - Workspace project tasks can be executed with a block.
79
+ - Gem command outdated supports semantic version upgrades.
80
+ - Project task unpack supports "gem" extension.
81
+ - Ruby command version was implemented.
82
+ - Ruby command file and script were implemented.
83
+ - Git global command autostash was created.
84
+ - Workspace static method exclude for global tasks was created.
85
+ - Git command checkout action branch and track are interactive.
86
+ - Git command branch action set and delete are interactive.
87
+ - Git command tag action delete is interactive.
88
+ - Docker command image run and container exec are interactive.
89
+ - Docker command network connect and disconnect are interactive.
90
+
91
+ ### Changed
92
+
93
+ - Project methods with conflicting names were given "!" suffix.
94
+ - Docker exec args are run without escape protection.
95
+ - Format method enable_aixterm no longer accepts a block.
96
+
97
+ ### Fixed
98
+
99
+ - Git command log action view did not parse options.
100
+ - Project task unpack did not use custom extensions.
101
+ - Docker did not append secrets when given a string.
102
+
3
103
  ## [0.4.7] - 2025-04-17
4
104
 
5
105
  ### Added
@@ -454,7 +554,7 @@
454
554
  - Node tasks without any action are not displayed.
455
555
  - Git fetch commands that do not apply to pull are rejected.
456
556
 
457
- ## [0.1.3] - 2024-01-02
557
+ ## [0.1.3] - 2025-01-02
458
558
 
459
559
  ### Fixed
460
560
 
@@ -533,6 +633,8 @@
533
633
 
534
634
  - Changelog was created.
535
635
 
636
+ [0.4.9]: https://github.com/anpham6/squared/releases/tag/v0.4.9-ruby
637
+ [0.4.8]: https://github.com/anpham6/squared/releases/tag/v0.4.8-ruby
536
638
  [0.4.7]: https://github.com/anpham6/squared/releases/tag/v0.4.7-ruby
537
639
  [0.4.6]: https://github.com/anpham6/squared/releases/tag/v0.4.6-ruby
538
640
  [0.4.5]: https://github.com/anpham6/squared/releases/tag/v0.4.5-ruby
@@ -541,6 +643,7 @@
541
643
  [0.4.2]: https://github.com/anpham6/squared/releases/tag/v0.4.2-ruby
542
644
  [0.4.1]: https://github.com/anpham6/squared/releases/tag/v0.4.1-ruby
543
645
  [0.4.0]: https://github.com/anpham6/squared/releases/tag/v0.4.0-ruby
646
+ [0.3.10]: https://github.com/anpham6/squared/releases/tag/v0.3.10-ruby
544
647
  [0.3.9]: https://github.com/anpham6/squared/releases/tag/v0.3.9-ruby
545
648
  [0.3.8]: https://github.com/anpham6/squared/releases/tag/v0.3.8-ruby
546
649
  [0.3.7]: https://github.com/anpham6/squared/releases/tag/v0.3.7-ruby
@@ -551,6 +654,7 @@
551
654
  [0.3.2]: https://github.com/anpham6/squared/releases/tag/v0.3.2-ruby
552
655
  [0.3.1]: https://github.com/anpham6/squared/releases/tag/v0.3.1-ruby
553
656
  [0.3.0]: https://github.com/anpham6/squared/releases/tag/v0.3.0-ruby
657
+ [0.2.10]: https://github.com/anpham6/squared/releases/tag/v0.2.10-ruby
554
658
  [0.2.9]: https://github.com/anpham6/squared/releases/tag/v0.2.9-ruby
555
659
  [0.2.8]: https://github.com/anpham6/squared/releases/tag/v0.2.8-ruby
556
660
  [0.2.7]: https://github.com/anpham6/squared/releases/tag/v0.2.7-ruby
@@ -561,6 +665,7 @@
561
665
  [0.2.2]: https://github.com/anpham6/squared/releases/tag/v0.2.2-ruby
562
666
  [0.2.1]: https://github.com/anpham6/squared/releases/tag/v0.2.1-ruby
563
667
  [0.2.0]: https://github.com/anpham6/squared/releases/tag/v0.2.0-ruby
668
+ [0.1.7]: https://github.com/anpham6/squared/releases/tag/v0.1.7-ruby
564
669
  [0.1.6]: https://github.com/anpham6/squared/releases/tag/v0.1.6-ruby
565
670
  [0.1.5]: https://github.com/anpham6/squared/releases/tag/v0.1.5-ruby
566
671
  [0.1.4]: https://github.com/anpham6/squared/releases/tag/v0.1.4-ruby
data/README.ruby.md CHANGED
@@ -83,7 +83,7 @@ Workspace::Application
83
83
  "CFLAGS" => "-fPIC -O1"
84
84
  })
85
85
  .add("optparse", doc: "rake rdoc", group: "default") # Uses bundler/gem_tasks (without C extensions)
86
- .add("logger", copy: { from: "lib", glob: "**/*.rb", into: "~/.rvm/gems/ruby-3.4.0/gems/logger-1.6.1" }, clean: ["tmp/"]) # autodetect: true
86
+ .add("logger", copy: { from: "lib", glob: "**/*.rb", into: "~/.rvm/gems/ruby-3.4.0/gems/logger-1.6.1" }, clean: ["tmp/"]) # autodetect: true | "rvm" | "rbenv" | "asdf" | "bundler"
87
87
  .add("e-mc", "emc", copy: { from: "publish", scope: "@e-mc", also: [:pir, "squared-express/"] }, ref: :node) # Node
88
88
  .add("pi-r", "pir", copy: { from: "publish", scope: "@pi-r" }, clean: ["publish/**/*.js", "tmp/"]) # Trailing slash required for directories
89
89
  .add("squared", script: ["build:stage1", "build:stage2"], group: "app") do # Copy target (main)
@@ -278,6 +278,14 @@ Workspace::Application
278
278
  .with(:ruby) do
279
279
  run("gem build") # gem build
280
280
  # OR
281
+ run("gem build", on: { first: -> { p "2" }, last: -> { p "4" } }) do # run | depend | graph | clean | doc | lint | test
282
+ p "1"
283
+ end
284
+ # OR
285
+ run(on: { first: -> { p "pass" }, last: -> { p "pass" } }) do
286
+ p "1"
287
+ end
288
+ # OR
281
289
  run(["gem build", "--force", { "RUBY_VERSION" => "3.4.0" }]) # RUBY_VERSION="3.4.0" gem build --force
282
290
  # OR
283
291
  run({ #
@@ -429,25 +437,28 @@ Most project classes will inherit from `Git` which enables these tasks:
429
437
  | :--------- | :--------------- | :-------------------------------------------- |
430
438
  | branch | branch | create set delete move copy list edit current |
431
439
  | checkout | checkout | commit branch track detach path |
432
- | commit | commit | add all amend amend-orig |
440
+ | commit | commit | add all amend amend-orig fixup |
433
441
  | diff | diff | head cached branch files between contain |
434
442
  | fetch | fetch | origin remote |
435
- | files | ls-files | cached modified deleted others ignored |
436
- | git | | clean mv rm |
443
+ | files | ls-files | cached modified deleted others |
444
+ | git | | add clean mv rm revert |
437
445
  | merge | merge | commit no-commit send |
438
446
  | pull | pull | origin remote |
439
447
  | rebase | rebase | branch onto send |
440
448
  | refs | ls-remote --refs | heads tags remote |
441
449
  | reset | reset | commit index patch mode |
442
450
  | restore | restore | staged worktree |
443
- | rev | rev | commit branch output parseopt |
451
+ | rev | rev | commit output |
444
452
  | show | show | format oneline |
445
453
  | stash | stash | push pop apply drop list |
454
+ | switch | switch | create detach merge |
446
455
  | tag | tag | add sign delete list |
447
456
 
448
457
  You can disable all of them at once using the `exclude` property.
449
458
 
450
459
  ```ruby
460
+ Workspace::Application.exclude('autostash', 'rebase')
461
+
451
462
  Workspace::Application
452
463
  .new
453
464
  .add("squared", exclude: :git)
@@ -462,6 +473,16 @@ Workspace::Application
462
473
  .pass("pull", ref: :node) { read_packagemanager(:private) }
463
474
  ```
464
475
 
476
+ ### Commit Hash
477
+
478
+ Commands which use commit hashes are parsed using a ":" prefix as to not be confused for an option.
479
+
480
+ ```sh
481
+ rake squared:log:view[:af012345] # git log af012345
482
+ rake squared:log:view[#{af012345}] # deprecated
483
+ rake squared:log:view[H1,HEAD^5,all,lib,./H12345] # git log --all @~1 @^5 -- 'lib' 'H12345'
484
+ ```
485
+
465
486
  ## Environment
466
487
 
467
488
  ### Path
@@ -551,39 +572,46 @@ GIT_AUTOSTASH_${NAME}=0 # rebase (project only)
551
572
 
552
573
  | Command | Flag | ENV |
553
574
  | :--------- | :---------------- | :-------------------------------------------------------------------- |
575
+ | branch | create | TRACK=0,1,s FORCE |
576
+ | branch | move copy | FORCE |
577
+ | branch | set delete | COUNT=n |
578
+ | branch | global | SYNC |
579
+ | checkout | branch | DETACH TRACK=s COUNT=n |
580
+ | checkout | detach | REFLOG=1 |
581
+ | checkout | track | COUNT=n |
582
+ | checkout | global path | HEAD=s PATHSPEC=s |
583
+ | checkout | * | FORCE MERGE |
584
+ | clone | * | DEPTH=n ORIGIN=s BRANCH=s LOCAL=0,1 |
585
+ | commit | * | UPSTREAM=s DRY_RUN EDIT=0 M|MESSAGE=s |
586
+ | diff | -between -contain | MERGE_BASE |
587
+ | diff | head branch | INDEX=n |
588
+ | diff | * | PATHSPEC=s |
589
+ | fetch | -remote | ALL |
590
+ | fetch | remote | REFSPEC=s |
591
+ | fetch | * | FORCE RECURSE_SUBMODULES=0,1,s |
592
+ | git | rm | PATHSPEC=s |
593
+ | log | * | PATHSPEC=s |
554
594
  | pull | rebase | AUTOSTASH |
555
595
  | pull | remote | REFSPEC=s |
556
596
  | pull | * | REBASE=0,1 FORCE RECURSE_SUBMODULES=0,1,s |
557
597
  | rebase | branch | HEAD=s |
558
598
  | rebase | onto | INTERACTIVE I HEAD=s |
559
- | fetch | -remote | ALL |
560
- | fetch | remote | REFSPEC=s |
561
- | fetch | * | FORCE RECURSE_SUBMODULES=0,1,s |
562
- | clone | * | DEPTH=n ORIGIN=s BRANCH=s LOCAL=0,1 |
563
- | stash | push | PATHSPEC=s |
564
- | stash | global | ALL=0,1 KEEP_INDEX=0,1 INCLUDE_UNTRACKED=0,1 STAGED=0,1 MESSAGE=s M=s |
565
- | status | global | LONG IGNORE_SUBMODULES=s,0-3 PATHSPEC=s |
566
- | revbuild | global | UNTRACKED_FILES=s IGNORE_SUBMODULES=s IGNORED=s (status) |
567
599
  | reset | mode (mixed) | N REFRESH=0 |
568
600
  | reset | index | PATHSPEC=s |
601
+ | reset | commit | COUNT=n REFLOG=1 |
569
602
  | reset | -commit | HEAD=s |
570
- | checkout | branch | DETACH TRACK=s |
571
- | checkout | global path | HEAD=s PATHSPEC=s |
572
- | checkout | * | FORCE MERGE |
573
- | tag | add | SIGN FORCE HEAD=s |
574
- | tag | sign | FORCE HEAD=s |
575
- | log | * | PATHSPEC=s |
576
- | diff | -between -contain | MERGE_BASE |
577
- | diff | head branch | INDEX=n |
578
- | diff | * | PATHSPEC=s |
579
- | commit | * | MESSAGE=s M=s REPOSITORY=s DRY_RUN EDIT=0 |
580
- | branch | create | TRACK=0,1,s FORCE |
581
- | branch | move copy | FORCE |
582
- | branch | global | SYNC |
583
603
  | restore | * | PATHSPEC=s |
584
- | show | -online | ABBREV=n |
604
+ | revbuild | global | UNTRACKED_FILES=s IGNORE_SUBMODULES=s IGNORED=s (status) |
605
+ | stash | push | PATHSPEC=s |
606
+ | stash | global | ALL=0,1 KEEP_INDEX=0,1 INCLUDE_UNTRACKED=0,1 STAGED=0,1 M|MESSAGE=s |
607
+ | status | global | BRANCH LONG IGNORE_SUBMODULES=s,0-3 PATHSPEC=s |
608
+ | switch | detach | REFLOG=1 |
609
+ | switch | -detach | HEAD=s |
610
+ | switch | * | FORCE |
611
+ | tag | add | SIGN FORCE HEAD=s M|MESSAGE=s |
612
+ | tag | sign | FORCE HEAD=s M|MESSAGE=s |
613
+ | tag | delete | COUNT=n |
585
614
  | rev | commit branch | HEAD=s |
586
- | git | rm | PATHSPEC=s |
587
615
 
588
616
  ### Docker
589
617
 
@@ -592,6 +620,7 @@ DOCKER_OPTIONS=q,no-cache # all
592
620
  DOCKER_OPTIONS_${NAME}=v,no-cache=false # project only (override)
593
621
  DOCKER_TAG=latest # all
594
622
  DOCKER_TAG_${NAME}=v0.1.0 # project only (override)
623
+ DOCKER_ALL=1 # list every image/container
595
624
  ```
596
625
 
597
626
  | Command | Flag | ENV |
@@ -11,6 +11,7 @@ module Squared
11
11
  COMMON: true,
12
12
  VERBOSE: nil,
13
13
  BANNER: true,
14
+ CHOICE: 25,
14
15
  QUOTE: "'",
15
16
  SPACE: ' => ',
16
17
  GRAPH: ['|', '-', '|', '\\', '-'].freeze,
@@ -12,7 +12,7 @@ module Squared
12
12
  super[/[^:]+\z/, 0]
13
13
  end
14
14
 
15
- def_delegators :@data, :+, :each, :each_with_index, :entries, :merge, :to_a, :include?
15
+ def_delegators :@data, :+, :each, :each_with_index, :entries, :merge, :include?
16
16
 
17
17
  def initialize(data = [])
18
18
  @data = Set.new(data)
@@ -22,6 +22,10 @@ module Squared
22
22
  @data.add(val.to_sym)
23
23
  end
24
24
 
25
+ def to_a
26
+ @data.to_a.freeze
27
+ end
28
+
25
29
  def to_s
26
30
  @data.to_s.sub('Set', SymSet.to_s)
27
31
  end
@@ -71,6 +75,20 @@ module Squared
71
75
  block_given? ? ret.reject(&blk) : ret
72
76
  end
73
77
 
78
+ def and(*args)
79
+ self << '&&'
80
+ merge(args)
81
+ end
82
+
83
+ def or(*args)
84
+ self << '||'
85
+ merge(args)
86
+ end
87
+
88
+ def with(*args, &blk)
89
+ temp('&&', *args, &blk)
90
+ end
91
+
74
92
  def temp(*args, &blk)
75
93
  args.compact!
76
94
  ret = pass(&blk)
@@ -28,18 +28,22 @@ module Squared
28
28
  bright_cyan!: '106',
29
29
  bright_white!: '107'
30
30
  }.freeze
31
- AIX_GRAPH = ['│', '─', '├', '└', '┬'].freeze
32
- AIX_BORDER = ['│', '─', '╭', '╮', '╯', '╰', '├', '┤', '┬', '┴'].freeze
31
+ BOX_GRAPH = ['│', '─', '├', '└', '┬'].freeze
32
+ BOX_BORDER = ['│', '─', '╭', '╮', '╯', '╰', '├', '┤', '┬', '┴'].freeze
33
33
  TEXT_STYLE = [:bold, :dim, :italic, :underline, :blinking, nil, :inverse, :hidden, :strikethrough].freeze
34
- private_constant :AIX_TERM, :AIX_GRAPH, :AIX_BORDER, :TEXT_STYLE
34
+ private_constant :AIX_TERM, :BOX_GRAPH, :BOX_BORDER, :TEXT_STYLE
35
35
 
36
36
  def enable_aixterm
37
37
  unless (colors = __get__(:colors)).frozen?
38
38
  colors.merge!(AIX_TERM)
39
- ARG[:GRAPH] = AIX_GRAPH
40
- ARG[:BORDER] = AIX_BORDER
41
39
  end
42
- block_given? ? yield(self) : self
40
+ self
41
+ end
42
+
43
+ def enable_drawing
44
+ ARG[:GRAPH] = BOX_GRAPH
45
+ ARG[:BORDER] = BOX_BORDER
46
+ self
43
47
  end
44
48
 
45
49
  private
@@ -57,7 +61,7 @@ module Squared
57
61
  end
58
62
  wrap = ->(s, n) { "\x1B[#{n.join(';')}m#{s}\x1B[0m" }
59
63
  code = []
60
- args.concat(as_a(styles)).each_with_index do |type, i|
64
+ args.concat(as_a(styles)).flatten.each_with_index do |type, i|
61
65
  next unless type
62
66
 
63
67
  if index == -1
@@ -68,16 +72,16 @@ module Squared
68
72
  end
69
73
  if type.is_a?(::Numeric)
70
74
  f, b = type.to_s.split('.')
71
- s = wrap.(s, ['38', '5', f]) if f[0] != '-' && f.to_i <= 255
75
+ s = wrap.call(s, ['38', '5', f]) if f[0] != '-' && f.to_i <= 255
72
76
  if b
73
77
  b = b[0, 3]
74
- s = wrap.(s, ['48', '5', b]) unless b.to_i > 255
78
+ s = wrap.call(s, ['48', '5', b]) unless b.to_i > 255
75
79
  end
76
80
  else
77
81
  t = type.to_sym
78
82
  if (c = __get__(:colors)[t])
79
83
  if index == -1
80
- s = wrap.(s, [c])
84
+ s = wrap.call(s, [c])
81
85
  else
82
86
  code << c
83
87
  end
@@ -95,7 +99,7 @@ module Squared
95
99
  end
96
100
  return ret.join if index == -1
97
101
 
98
- ret = wrap.(ret, code) unless code.empty?
102
+ ret = wrap.call(ret, code) unless code.empty?
99
103
  return ret unless data
100
104
 
101
105
  out = ''.dup
@@ -133,28 +137,21 @@ module Squared
133
137
 
134
138
  set = ->(k, v) { data[k] = check_style(v, empty: empty) }
135
139
  if key.is_a?(::Hash)
136
- key.each { |k, v| set.(k, v || args) }
140
+ key.each { |k, v| set.call(k, v || args) }
137
141
  else
138
- set.(key.to_sym, args)
142
+ set.call(key.to_sym, args)
139
143
  end
140
144
  end
141
145
 
142
146
  def log_sym(level)
143
147
  if level.is_a?(::Numeric)
144
148
  case level
145
- when Logger::DEBUG
146
- :debug
147
- when Logger::INFO
148
- :info
149
- when Logger::WARN
150
- :warn
151
- when Logger::ERROR
152
- :error
153
- when Logger::FATAL
154
- :fatal
155
- else
156
- :unknown
157
- end
149
+ when Logger::DEBUG then :debug
150
+ when Logger::INFO then :info
151
+ when Logger::WARN then :warn
152
+ when Logger::ERROR then :error
153
+ when Logger::FATAL then :fatal
154
+ else :unknown end
158
155
  else
159
156
  level.to_s.downcase.to_sym
160
157
  end
@@ -181,7 +178,7 @@ module Squared
181
178
  end
182
179
  if args.size > 1 && !hint
183
180
  title = log_title(level, color: false)
184
- sub = { pat: /^(#{title})(.+)$/, styles: __get__(:theme)[:logger][log_sym(level)] } if color
181
+ sub = { pat: /\A(#{Regexp.escape(title)})(.*)\z/m, styles: __get__(:theme)[:logger][log_sym(level)] } if color
185
182
  emphasize(args, title: title + (subject ? " #{subject}" : ''), sub: sub)
186
183
  else
187
184
  msg = [log_title(level, color: color)]
@@ -219,18 +216,18 @@ module Squared
219
216
  max = ->(v) { n = [n, v.max_by(&:size).size].max }
220
217
  set = lambda do |v|
221
218
  ret = as_a(v, :to_s)
222
- max.(ret)
219
+ max.call(ret)
223
220
  ret
224
221
  end
225
- title &&= set.(title)
226
- footer &&= set.(footer)
222
+ title &&= set.call(title)
223
+ footer &&= set.call(footer)
227
224
  if val.is_a?(::Array)
228
225
  lines = val.map(&:to_s)
229
226
  else
230
227
  lines = val.to_s.lines(chomp: true)
231
228
  lines[0] = "#{val.class}: #{lines.first}" if (err = val.is_a?(StandardError))
232
229
  end
233
- n = cols || max.(lines)
230
+ n = cols || max.call(lines)
234
231
  if $stdout.tty?
235
232
  require 'io/console'
236
233
  (n = [n, $stdout.winsize[1] - 4].min) rescue nil
@@ -242,7 +239,7 @@ module Squared
242
239
  ret = sub_style(ret, styles: border) if border
243
240
  ret
244
241
  end
245
- sub = as_a(sub)
242
+ sub = as_a sub
246
243
  pr = lambda do |line|
247
244
  s = line.ljust(n)
248
245
  sub.each { |h| s = sub_style(s, **h) }
@@ -253,13 +250,13 @@ module Squared
253
250
  end
254
251
  s
255
252
  end
256
- out << draw.(b2, b3)
253
+ out << draw.call(b2, b3)
257
254
  if title
258
- out.concat(title.map { |t| pr.(t) })
259
- out << draw.(b6, b7)
255
+ out.concat(title.map { |t| pr.call(t) })
256
+ out << draw.call(b6, b7)
260
257
  end
261
- lines.each { |line| out << pr.(line) }
262
- out << draw.(b5, b4)
258
+ lines.each { |line| out << pr.call(line) }
259
+ out << draw.call(b5, b4)
263
260
  if footer
264
261
  unless sub.empty? && !right
265
262
  footer.map! do |s|
@@ -292,7 +289,7 @@ module Squared
292
289
  end
293
290
 
294
291
  def stripext(val)
295
- File.basename(val, File.extname(val))
292
+ File.basename(val, '.*')
296
293
  end
297
294
 
298
295
  def raise_error(*args, hint: nil, kind: ArgumentError)
@@ -5,14 +5,14 @@ module Squared
5
5
  module Prompt
6
6
  module_function
7
7
 
8
- def confirm(msg, default = nil, agree: 'Y', cancel: 'N', attempts: 5, timeout: 15)
8
+ def confirm(msg, default = nil, agree: 'Y', cancel: 'N', attempts: 5, timeout: 30)
9
9
  require 'readline'
10
10
  require 'timeout'
11
11
  agree = /^#{agree}$/i if agree.is_a?(::String)
12
12
  cancel = /^#{cancel}$/i if cancel.is_a?(::String)
13
13
  Timeout.timeout(timeout) do
14
14
  begin
15
- while (ch = Readline.readline(msg, true))
15
+ while (ch = Readline.readline(msg))
16
16
  ch = ch.chomp
17
17
  case (ch.empty? ? default : ch)
18
18
  when agree
@@ -21,7 +21,7 @@ module Squared
21
21
  return false
22
22
  end
23
23
  attempts -= 1
24
- exit 1 unless attempts >= 0
24
+ exit 1 unless attempts > 0
25
25
  end
26
26
  rescue Interrupt
27
27
  puts
@@ -31,6 +31,69 @@ module Squared
31
31
  end
32
32
  end
33
33
  end
34
+
35
+ def choice(msg, list = nil, min: 1, max: 1, multiple: false, auto: true, force: true, attempts: 5, timeout: 60)
36
+ require 'readline'
37
+ require 'timeout'
38
+ if list
39
+ items = []
40
+ list.each_with_index do |val, index|
41
+ puts "#{index.succ.to_s.rjust(2)}. #{val}"
42
+ items << val.chomp
43
+ end
44
+ max = items.size
45
+ if auto
46
+ msg = "#{msg}: [1-#{max}#{if multiple
47
+ "|,#{multiple.is_a?(Numeric) ? "{#{multiple}}" : ''}"
48
+ end}] "
49
+ end
50
+ end
51
+ return unless max >= min
52
+
53
+ valid = ->(s) { s.match?(/^-?\d+$/) && s.to_i.between?(min, max) }
54
+ Timeout.timeout(timeout) do
55
+ begin
56
+ while (ch = Readline.readline(msg))
57
+ unless (ch = ch.strip).empty?
58
+ if multiple
59
+ a = ch.split(/\s*,\s*/)
60
+ b = a.select { |s| valid.call(s) }.map!(&:to_i).sort
61
+ next unless a.size == b.size
62
+ return b unless items
63
+ next if multiple.is_a?(::Numeric) && multiple != b.size
64
+
65
+ return b.map! { |i| items[i - 1] }
66
+ elsif valid.call(ch)
67
+ return items ? items[ch.to_i - 1] : ch.to_i
68
+ end
69
+ end
70
+ attempts -= 1
71
+ next if attempts > 0
72
+ break unless force
73
+
74
+ exit 1
75
+ end
76
+ rescue Interrupt
77
+ puts
78
+ exit 0
79
+ else
80
+ multiple ? [] : nil
81
+ end
82
+ end
83
+ end
84
+
85
+ def readline(msg, history = false, force: nil)
86
+ require 'readline'
87
+ case force
88
+ when ::TrueClass, ::FalseClass
89
+ msg = "#{msg} (#{force ? 'required' : 'optional'}): "
90
+ ret = Readline.readline(msg, history)&.strip || ''
91
+ raise_error 'user cancelled' if force && ret.empty?
92
+ ret
93
+ else
94
+ Readline.readline(msg, history)
95
+ end
96
+ end
34
97
  end
35
98
  end
36
99
  end