squared 0.5.25 → 0.6.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.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # squared 0.5
1
+ # squared 0.6
2
2
 
3
3
  * [source](https://github.com/anpham6/squared-ruby)
4
4
  * [docs](https://squared.readthedocs.io)
@@ -14,6 +14,7 @@
14
14
  | 2025-06-16 | 0.5.0 | 2.5.0 | 3.4.3 |
15
15
  | ---------- | ----- | ----- | ----- |
16
16
  | 2025-08-23 | 0.5.5 | | 3.4.5 |
17
+ | 2025-10-31 | 0.6.0 | | 3.4.7 |
17
18
 
18
19
  The range chart indicates the latest Ruby tested against at the time of release.
19
20
 
@@ -23,9 +24,23 @@ The range chart indicates the latest Ruby tested against at the time of release.
23
24
  gem install squared
24
25
  ```
25
26
 
27
+ ### Optional
28
+
29
+ * [Repo](https://source.android.com/docs/setup/reference/repo)
30
+ * https://github.com/anpham6/squared-repo
31
+ * Python 3.6
32
+ * Not compatible with Windows
33
+
34
+ ```sh
35
+ mkdir -p ~/.bin
36
+ PATH="${HOME}/.bin:${PATH}"
37
+ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
38
+ chmod a+rx ~/.bin/repo
39
+ ```
40
+
26
41
  ## Example - Rakefile
27
42
 
28
- Projects from any accessible folder can be added relative to the parent directory or absolutely. Missing projects will simply be excluded from the task runner.
43
+ Projects from any accessible folder can be added relative to the parent directory (e.g. *REPO_ROOT*) or absolutely. The same Rakefile can also manage other similarly cloned `Repo` repositories remotely by setting the `REPO_ROOT` environment variable to the location. Missing projects will simply be excluded from the task runner.
29
44
 
30
45
  ```ruby
31
46
  require "squared"
@@ -41,6 +56,14 @@ require "squared/app" # All workspace related mod
41
56
 
42
57
  # NODE_ENV = production
43
58
 
59
+ # REPO_ROOT = /workspaces |
60
+ # REPO_HOME = /workspaces/squared | Dir.pwd
61
+ # rake = /workspaces/squared/Rakefile | main?
62
+ # OR
63
+ # REPO_ROOT = /workspaces | Dir.pwd
64
+ # rake = /workspaces/Rakefile |
65
+ # REPO_HOME = /workspaces/squared | main: "squared"
66
+
44
67
  # pathname = /workspaces/pathname
45
68
  # optparse = /workspaces/optparse
46
69
  # log = /workspaces/logger
@@ -55,6 +78,7 @@ require "squared/app" # All workspace related mod
55
78
  Workspace::Application
56
79
  .new(Dir.pwd, main: "squared") # Dir.pwd? (main? is implicitly basename)
57
80
  .banner("group", "project", styles: ["yellow", "black"], border: "bold") # name | project | path | ref | group? | parent? | version?
81
+ .repo("https://github.com/anpham6/squared-repo", "nightly", script: ["build:dev", "build:prod"], ref: :node) # Repo (optional)
58
82
  .run("rake install", ref: :ruby)
59
83
  .depend(false, group: "default")
60
84
  .clean("rake clean", group: "default")
@@ -68,7 +92,7 @@ Workspace::Application
68
92
  .add("e-mc", "emc", copy: { from: "publish", scope: "@e-mc", also: [:pir, "squared-express/"] }, ref: :node) # Node
69
93
  .add("pi-r", "pir", copy: { from: "publish", scope: "@pi-r" }, clean: ["publish/**/*.js", "tmp/"]) # Trailing slash required for directories
70
94
  .add("pi-r2", "pir2", copy: { from: :npm, also: %i[squared express], files: ["LICENSE", ["README.md.ruby", "README.md"]] }) # Uses dist files from NPM package spec
71
- .add("squared", init: "pnpm", script: ["build:stage1", "build:stage2"], group: "app") do # Use pnpm/yarn/berry for depend + Copy target (main)
95
+ .add("squared", init: 'pnpm', script: ["build:stage1", "build:stage2"], group: "app") do # Use pnpm/yarn/berry for depend + Copy target (main)
72
96
  # Repo (global)
73
97
  as(:run, "build:dev", "dev") # npm run build:dev -> npm run dev
74
98
  as(:run, { "build:dev": "dev", "build:prod": "prod" })
@@ -119,20 +143,59 @@ Workspace::Application
119
143
  # android = /workspaces/android-docs
120
144
  # chrome = /workspaces/chrome-docs
121
145
 
146
+ # ...symbol?, string, ...string?, ...symbol?
147
+ # extend name args project
148
+
149
+ Node = Workspace::Project::Node # tsc
150
+ Node.options("build:dev", "target=es2022", project: "tsconfig.json") # :node (ref)
151
+ Node.options("build:dev", "outDir=tmp", :squared) # squared (project name)
152
+
153
+ Ruby = Workspace::Project::Ruby # ruby | gem | rake | bundle | irb
154
+ Ruby.options("lint", "rubocop", opts: ["gemfile=Gemfile"]) # :ruby
155
+ Ruby.options("lint", "--parallel", :squared)
156
+
157
+ Python = Workspace::Project::Python # pip
158
+ Python.options("build", opts: ["r=requirements.txt"]) # :python
159
+ Python.options(:build, "build:dev", opts: ["no-deps"])
160
+
122
161
  Workspace::Application
123
- .new(ENV["SQUARED_HOME"], prefix: "rb", common: false) # Local styles
162
+ .new(ENV["SQUARED_HOME"], prefix: "rb", common: false) # Local styles
124
163
  .group("ruby", "default", run: "rake build", copy: "rake install", clean: "rake clean", ref: :ruby, override: {
125
164
  pathname: {
126
- run: "rake compile" # rake rb:pathname:build
165
+ run: "rake compile" # rake rb:pathname:build
127
166
  }
128
167
  })
129
- .with(:python, editable: false) do # ref=Symbol | group=String
130
- banner([:name, ": ", :version], "path") # chrome-docs: 0.1.0 | /workspaces/chrome-docs
131
- doc("make html") # rake rb:doc:python
132
- run(false) # rake rb:build:python (disabled)
133
- exclude(%i[base git]) # Project::Git.ref (superclass)
134
- add("android-docs", "android") # rake rb:android:doc
135
- add("chrome-docs", "chrome") # rake rb:chrome:doc
168
+ .add("squared") do
169
+ apply :run, proc { # rake rb:squared:build
170
+ tsc(with: scriptname || "build:dev", verbose: true) # Repo (global)
171
+ # OR
172
+ tsc("target=es2022", "outDir=tmp", project: "tsconfig.json") # 1
173
+ tsc("clean=true", "./sqd/tsconfig.json", build: true) # 2
174
+ run("npm run build:dev", { "RUBYOPT" => "-W2" }, from: :run) # 3
175
+
176
+ # Parallel
177
+ [
178
+ Thread.new { tsc(sync: false) },
179
+ Thread.new { tsc(sync: false) },
180
+ Thread.new { run(sync: false) }
181
+ ]
182
+ .each(&:join)
183
+ }
184
+ apply :lint, proc {
185
+ bundle("exec", "-A --cache=true", with: "lint", verbose: true) # bundle exec --gemfile='/squared/Gemfile' rubocop --parallel -A --cache=true
186
+ }
187
+ end
188
+ .with(:python, editable: false) do # ref=Symbol | group=String
189
+ banner([:name, ": ", :version], "path") # chrome-docs: 0.1.0 | /workspaces/chrome-docs
190
+ doc("make html") # rake rb:doc:python
191
+ run(false) # rake rb:build:python (disabled)
192
+ exclude(%i[base git]) # Project::Git.ref (superclass)
193
+ add("android-docs", "android") # rake rb:android:doc
194
+ add("chrome-docs", "chrome") do # rake rb:chrome:doc
195
+ apply :run, proc {
196
+ pip("wheel", with: "build:dev") # pip wheel -r '/chrome-docs/requirements.txt' --no-deps
197
+ }
198
+ end
136
199
  end
137
200
  .style("inline", "bold")
138
201
  .build
@@ -473,28 +536,28 @@ Non-task:
473
536
 
474
537
  Most project classes will inherit from `Git` which enables these tasks:
475
538
 
476
- | Task | Git | Command |
477
- | :--------- | :--------------- | :-------------------------------------------- |
478
- | branch | branch | create track delete move copy list current |
479
- | checkout | checkout | commit branch track detach path |
480
- | commit | commit | add all amend amend-orig fixup |
481
- | diff | diff | head branch files view between contain |
482
- | fetch | fetch | origin remote all |
483
- | files | ls-files | cached modified deleted others |
484
- | git | | add blame clean mv revert rm status |
485
- | log | log | view between contain |
486
- | merge | merge | commit no-commit send |
487
- | pull | pull | origin remote all |
488
- | rebase | rebase | branch onto send |
489
- | refs | ls-remote --refs | heads tags remote |
490
- | reset | reset | commit index patch mode undo |
491
- | restore | restore | source staged worktree |
492
- | rev | rev | commit build output |
493
- | show | show | format oneline textconv |
494
- | stash | stash | push pop apply branch drop clear list all |
495
- | submodule | submodule | status update branch url sync |
496
- | switch | switch | branch create detach |
497
- | tag | tag | add sign delete list |
539
+ | Task | Git | Command |
540
+ | :--------- | :--------------- | :-------------------------------------------------------- |
541
+ | branch | branch | create track delete move copy list current |
542
+ | checkout | checkout | commit branch track detach path |
543
+ | commit | commit | add all amend amend-orig fixup |
544
+ | diff | diff | head branch files view between contain |
545
+ | fetch | fetch | origin remote all |
546
+ | files | ls-files | cached modified deleted others |
547
+ | git | | add blame clean grep mv revert rm status |
548
+ | log | log | view between contain |
549
+ | merge | merge | commit no-commit send |
550
+ | pull | pull | origin remote all |
551
+ | rebase | rebase | branch onto send |
552
+ | refs | ls-remote --refs | heads tags remote |
553
+ | reset | reset | commit index patch mode undo |
554
+ | restore | restore | source staged worktree |
555
+ | rev | rev | commit build output |
556
+ | show | show | format oneline textconv |
557
+ | stash | stash | push pop apply branch drop clear list all staged worktree |
558
+ | submodule | submodule | status update branch url sync |
559
+ | switch | switch | branch create detach |
560
+ | tag | tag | add sign delete list |
498
561
 
499
562
  You can disable all of them at once using the `exclude` property.
500
563
 
@@ -521,7 +584,7 @@ Commands which use commit hashes are parsed using a ":" prefix as to not be conf
521
584
 
522
585
  ```sh
523
586
  rake squared:log:view[:af012345] # git log af012345
524
- rake squared:log:view[H1,HEAD^5,all,lib,./H12345] # git log --all @~1 @^5 -- "lib" "H12345"
587
+ rake squared:log:view[H1,HEAD^5,all,lib,./H12345] # git log --all @~1 @^5 -- 'lib' 'H12345'
525
588
  ```
526
589
 
527
590
  ## Environment
@@ -609,7 +672,7 @@ LOG_LEVEL # See gem "logger"
609
672
 
610
673
  ### Git
611
674
 
612
- * Version: [2.50](https://github.com/git/git/blob/v2.50.0/Documentation/RelNotes/2.50.0.adoc)
675
+ * Version: [2.51](https://github.com/git/git/blob/v2.51.0/Documentation/RelNotes/2.51.0.adoc)
613
676
 
614
677
  ```sh
615
678
  GIT_OPTIONS=q,strategy=ort # all
@@ -676,7 +739,7 @@ DOCKER_ALL=1 # list every image/container
676
739
  DOCKER_Y=1 # confirm all
677
740
 
678
741
  BUILD_SQUARED_OPTS="NODE_TAG=24 RUBY_VERSION=3.4.0" DOCKER_SQUARED_OPTS="--no-cache --label=v1" rake squared:build
679
- docker build --no-cache --label=v1 --build-arg="NODE_TAG=24" --build-arg="RUBY_VERSION=3.4.0" .
742
+ docker build --no-cache --label=v1 --build-arg='NODE_TAG=24' --build-arg='RUBY_VERSION=3.4.0' .
680
743
  ```
681
744
 
682
745
  | Command | Flag | ENV |
@@ -707,58 +770,27 @@ Workspace::Application
707
770
 
708
771
  ### Repo
709
772
 
710
- * [Repo](https://source.android.com/docs/setup/reference/repo)
711
- * https://github.com/anpham6/squared-repo
712
- * Python 3.6
713
- * Not compatible with Windows
714
-
715
- ```sh
716
- mkdir -p ~/.bin
717
- PATH="${HOME}/.bin:${PATH}"
718
- curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
719
- chmod a+rx ~/.bin/repo
720
- ```
721
-
722
- ```ruby
723
- # REPO_ROOT = /workspaces |
724
- # REPO_HOME = /workspaces/squared | Dir.pwd
725
- # rake = /workspaces/squared/Rakefile | main?
726
- #
727
- # OR
728
- #
729
- # REPO_ROOT = /workspaces | Dir.pwd
730
- # rake = /workspaces/Rakefile |
731
- # REPO_HOME = /workspaces/squared | main: "squared"
732
-
733
- Workspace::Application
734
- .new(main: "squared")
735
- .repo("https://github.com/anpham6/squared-repo", "nightly", script: ["build:dev", "prod"], ref: :node)
736
- .add("squared", script: ["build:stage1", "build:stage2"])
737
- .build
738
- ```
739
-
740
- These global options also can target the application main suffix `${NAME}`. (e.g. *REPO_ROOT_SQUARED*)
773
+ These global options also can target the application main suffix `${NAME}`. (e.g. REPO_ROOT_SQUARED)
741
774
 
742
775
  ```sh
743
776
  REPO_ROOT # parent dir
744
777
  REPO_HOME # project dir (main)
745
- REPO_BUILD # run,script
778
+ REPO_BUILD # script,run (e.g. build:dev | build:dev,make install | make)
746
779
  REPO_GROUP # string
747
780
  REPO_REF # e.g. ruby,node
748
781
  REPO_DEV # pattern,0,1
749
782
  REPO_PROD # pattern,0,1
750
783
  REPO_WARN # 0,1
751
784
  REPO_SYNC # 0,1
752
- REPO_GIT # manifest repository
785
+ REPO_URL # manifest repository
753
786
  REPO_MANIFEST # e.g. latest,nightly,prod
754
787
  REPO_GROUPS # e.g. base,prod,docs
755
788
  REPO_STAGE # 0,1,2,3,4
756
789
  REPO_SUBMODULLES # 0,1
790
+ REPO_Y # 0,1
757
791
  REPO_TIMEOUT # confirm dialog (seconds)
758
792
  ```
759
793
 
760
- Other similarly cloned `Repo` repositories can be managed remotely by setting the `REPO_ROOT` environment variable to the location.
761
-
762
794
  ## LICENSE
763
795
 
764
796
  BSD 3-Clause
@@ -17,6 +17,7 @@ module Squared
17
17
  GRAPH: ['|', '-', '|', '\\', '-'].freeze,
18
18
  BORDER: ['|', '-', '-', '-', '-', '-', '|', '|', '-', '-'].freeze,
19
19
  VIEW: 'view',
20
+ BACKTRACE: $DEBUG || !$VERBOSE.nil?,
20
21
  LEVEL: ENV.fetch('LOG_LEVEL', 0).to_i,
21
22
  COLOR: ENV.fetch('NO_COLOR', '').empty?
22
23
  }
@@ -97,27 +98,5 @@ module Squared
97
98
  VAR[:theme].each_value { |val| val.freeze.each_value(&:freeze) }
98
99
  VAR.freeze
99
100
  end
100
-
101
- module_function
102
-
103
- def as_a(obj, *meth, flat: nil, compact: false, &blk)
104
- return [] if obj.nil?
105
-
106
- unless obj.is_a?(::Array)
107
- obj = if obj.respond_to?(:to_ary)
108
- obj.to_ary
109
- elsif obj.respond_to?(:to_a) && !obj.is_a?(::Hash) && (val = obj.to_a).is_a?(::Array)
110
- val
111
- else
112
- [obj]
113
- end
114
- end
115
- obj = flat.is_a?(::Numeric) ? obj.flatten(flat) : obj.flatten if flat
116
- obj = obj.compact if compact
117
- obj = obj.map(&meth.shift) until meth.empty?
118
- return obj unless block_given?
119
-
120
- obj.select(&blk)
121
- end
122
101
  end
123
102
  end
@@ -39,6 +39,10 @@ module Squared
39
39
  TEXT_STYLE = [:bold, :dim, :italic, :underline, :blinking, nil, :inverse, :hidden, :strikethrough].freeze
40
40
  private_constant :AIX_TERM, :BOX_GRAPH, :BOX_BORDER, :TEXT_STYLE
41
41
 
42
+ String.define_method(:stripstyle) { gsub(/\x1B\[(?:\d+;?)+m/, '') }
43
+ String.define_method(:stripext) { File.basename(self, '.*') }
44
+ String.define_method(:subhint) { |s| s.nil? || (s.is_a?(::String) && s.empty?) ? self : "#{self} (#{s})" }
45
+
42
46
  def enable_aixterm
43
47
  unless (colors = __get__(:colors)).frozen?
44
48
  colors.update(AIX_TERM)
@@ -67,7 +71,6 @@ module Squared
67
71
  end
68
72
  wrap = ->(s, n) { "\x1B[#{n.join(';')}m#{s}\x1B[0m" }
69
73
  code = []
70
- args.clear if args.size == 1 && args.first.nil?
71
74
  args.concat(Array(styles)).flatten.each_with_index do |type, i|
72
75
  next unless type
73
76
 
@@ -86,7 +89,7 @@ module Squared
86
89
  end
87
90
  else
88
91
  t = type.to_sym
89
- if (c = __get__(:colors)[t])
92
+ if (c = __get__(:colors)[t] || __get__(:colors)[t.to_s.sub('bright_', '').to_sym])
90
93
  if index == -1
91
94
  s = wrap.call(s, [c])
92
95
  else
@@ -95,7 +98,7 @@ module Squared
95
98
  else
96
99
  next unless (n = TEXT_STYLE.index(t))
97
100
 
98
- s = "\x1B[#{n + 1}m#{s}\x1B[#{n == 0 ? 22 : n + 21}m"
101
+ s = "\x1B[#{n.succ}m#{s}\x1B[#{n == 0 ? 22 : n + 21}m"
99
102
  end
100
103
  end
101
104
  if index == -1
@@ -127,8 +130,8 @@ module Squared
127
130
  colors = __get__(:colors)
128
131
  Array(args).flatten.compact.each do |val|
129
132
  if !val.is_a?(::Numeric)
130
- val = val.to_sym
131
- ret << val if colors.key?(val) || TEXT_STYLE.include?(val)
133
+ k = val.to_sym
134
+ ret << k if colors.key?(k) || colors.key?(k.to_s.sub('bright_', '').to_sym) || TEXT_STYLE.include?(k)
132
135
  elsif val.between?(0, 256)
133
136
  ret << val
134
137
  elsif val < 0 && (b = val.to_s.split('.')[1])
@@ -152,6 +155,10 @@ module Squared
152
155
  end
153
156
  end
154
157
 
158
+ def opt_style(styles, pat = nil, index = 1)
159
+ { styles: styles, pat: pat, index: index }
160
+ end
161
+
155
162
  def log_sym(level)
156
163
  if level.is_a?(::Numeric)
157
164
  case level
@@ -181,22 +188,23 @@ module Squared
181
188
  args = args.map(&:to_s)
182
189
  if level.is_a?(::Numeric)
183
190
  if append && respond_to?(:log)
184
- ref = log rescue nil
185
- ref.add(level, message(subject, *args, hint: hint, space: ', ')) if ref.is_a?(::Logger)
191
+ (log rescue nil).tap do |ref|
192
+ ref.add(level, message(subject, *args, hint: hint, space: ', ')) if ref.is_a?(::Logger)
193
+ end
186
194
  end
187
- return false unless pass || level >= ARG[:LEVEL]
195
+ return false if !pass && level < ARG[:LEVEL]
188
196
  end
189
- if hint.nil? ? args.size > 1 : !hint
197
+ if (args.size > 1 && !hint) || hint == false
190
198
  title = log_title(level, color: false)
191
- if color
192
- sub = [{ pat: /\A(#{Regexp.escape(title)})(.*)\z/m, styles: __get__(:theme)[:logger][log_sym(level)] }]
193
- end
194
- emphasize(args, title: title + (subject ? " #{subject}" : ''), sub: sub, pipe: -1)
199
+ emphasize(args,
200
+ title: title + (subject ? " #{subject}" : ''),
201
+ pipe: -1,
202
+ sub: if color
203
+ opt_style(__get__(:theme)[:logger][log_sym(level)], /\A(#{Regexp.escape(title)})(.*)\z/m)
204
+ end)
195
205
  else
196
206
  msg = [log_title(level, color: color)]
197
- if subject
198
- msg << (color ? sub_style(subject.to_s, (@theme.is_a?(::Hash) && @theme[:subject]) || :bold) : subject)
199
- end
207
+ msg << (color ? sub_style(subject.to_s, styles: (@theme && @theme[:subject]) || :bold) : subject) if subject
200
208
  msg << args.shift if msg.size == 1
201
209
  message(msg.join(' '), *args, hint: hint)
202
210
  end
@@ -209,7 +217,7 @@ module Squared
209
217
  begin
210
218
  File.open(pipe, 'a') do |f|
211
219
  br = File::SEPARATOR == '\\' ? "\r\n" : "\n"
212
- args.flatten.each { |val| f.write(strip_style(val.chomp) + br) }
220
+ args.flatten.each { |val| f.write(val.chomp.stripstyle + br) }
213
221
  end
214
222
  return
215
223
  rescue StandardError
@@ -219,17 +227,16 @@ module Squared
219
227
  (pipe == 2 ? $stderr : $stdout).puts(*args)
220
228
  end
221
229
 
222
- alias puts_oe log_console
223
-
224
230
  module_function
225
231
 
226
232
  def message(*args, hint: nil, empty: false, space: ARG[:SPACE])
227
233
  (empty ? args.reject { |val| val.nil? || (val.respond_to?(:empty?) && val.empty?) } : args)
228
- .join(space) + (hint ? " (#{hint})" : '')
234
+ .join(space)
235
+ .subhint(hint)
229
236
  end
230
237
 
231
238
  def emphasize(val, title: nil, footer: nil, right: false, cols: nil, sub: nil, pipe: nil,
232
- border: @theme.is_a?(::Hash) && @theme[:border])
239
+ border: @theme && @theme[:border])
233
240
  n = 0
234
241
  max = ->(a) { n = [n, a.max_by(&:size).size].max }
235
242
  set = ->(s) { Array(s).map(&:to_s).tap { |a| max.call(a) } }
@@ -241,9 +248,7 @@ module Squared
241
248
  lines = val.to_s.lines(chomp: true)
242
249
  lines[0] = "#{val.class}: #{lines.first}" if (err = val.is_a?(::StandardError))
243
250
  end
244
- return if lines.empty?
245
-
246
- n = cols || max.call(lines)
251
+ n = (cols.is_a?(::Array) ? cols.map(&:size).max : cols) || max.call(lines)
247
252
  if $stdout.tty?
248
253
  require 'io/console'
249
254
  (n = [n, $stdout.winsize[1] - 4].min) rescue nil
@@ -261,14 +266,14 @@ module Squared
261
266
  sub.each { |h| s = sub_style(s, **h) }
262
267
  s = "#{b0} #{s} #{b0}"
263
268
  if border
264
- s = sub_style(s, pat: /\A(#{Regexp.escape(b0)})(.+)\z/om, styles: border)
265
- s = sub_style(s, pat: /\A(.+)(#{Regexp.escape(b0)})\z/om, styles: border, index: 2)
269
+ s = sub_style(s, **opt_style(border, /\A(#{Regexp.escape(b0)})(.+)\z/m))
270
+ s = sub_style(s, **opt_style(border, /\A(.+)(#{Regexp.escape(b0)})\z/m, 2))
266
271
  end
267
272
  s
268
273
  end
269
274
  out << draw.call(b2, b3)
270
275
  if title
271
- out.concat(title.map { |t| pr.call(t) })
276
+ out.concat(title.map! { |t| pr.call(t) })
272
277
  out << draw.call(b6, b7)
273
278
  end
274
279
  lines.each { |line| out << pr.call(line) }
@@ -296,22 +301,19 @@ module Squared
296
301
  else
297
302
  pipe = $stdout unless pipe.respond_to?(:puts)
298
303
  end
299
- pipe.puts(out)
304
+ pipe.puts out
300
305
  else
301
306
  err ? warn(out) : puts(out)
302
307
  end
303
308
  end
304
309
 
305
- def strip_style(val)
306
- val.gsub(/\x1B\[(\d+;?)+m/, '')
307
- end
308
-
309
- def stripext(val)
310
- File.basename(val, '.*')
311
- end
312
-
313
- def raise_error(*args, hint: nil, kind: ArgumentError)
314
- raise kind, message(*args, hint: hint, empty: true), caller_locations(1).map(&:to_s)
310
+ def raise_error(*args, hint: nil, kind: RuntimeError, start: 0)
311
+ kind = args.shift if args.first.is_a?(::Class) && args.first < ::Exception
312
+ raise kind, message(*args, hint: hint, empty: true), if ARG[:BACKTRACE]
313
+ caller(start.succ)
314
+ else
315
+ caller_locations(start.succ, 1).first&.base_label
316
+ end
315
317
  end
316
318
  end
317
319
  end
@@ -1,12 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ unless defined?(Readline)
4
+ if RUBY_ENGINE == 'ruby' && RUBY_VERSION < '2.7'
5
+ require 'readline'
6
+ else
7
+ begin
8
+ require 'reline'
9
+ Object.send(:remove_const, :Readline) if Object.const_defined?(:Readline)
10
+ Readline = Reline
11
+ rescue LoadError
12
+ require 'readline'
13
+ end
14
+ end
15
+ end
16
+
3
17
  module Squared
4
18
  module Common
5
19
  module Prompt
6
20
  module_function
7
21
 
8
- def confirm(msg, default = nil, agree: 'Y', cancel: 'N', attempts: 5, timeout: 30)
9
- require 'readline'
22
+ def confirm(msg, default = nil, agree: 'Y', cancel: 'N', attempts: 3, timeout: 60)
10
23
  require 'timeout'
11
24
  if agree == 'Y' && cancel == 'N' && !msg.match?(%r{\[(?:Yn|nY|Y/n|y/N)\]})
12
25
  case default
@@ -38,9 +51,8 @@ module Squared
38
51
  end
39
52
  end
40
53
 
41
- def choice(msg, list = nil, min: 1, max: 1, multiple: false, force: true, grep: nil, auto: true,
42
- attempts: 5, timeout: 0)
43
- require 'readline'
54
+ def choice(msg, list = nil, min: 1, max: 1, multiple: false, index: false, grep: nil, border: nil, auto: true,
55
+ force: true, attempts: 3, timeout: 0)
44
56
  require 'timeout'
45
57
  if list
46
58
  grep &&= Array(grep).map { |val| Regexp.new(val) }
@@ -52,40 +64,51 @@ module Squared
52
64
  puts '%2d. %s' % [items.size, val]
53
65
  end
54
66
  max = items.size
55
- raise_error 'empty selection list' if max == 0
67
+ raise ArgumentError, 'empty selection list' if max == 0
68
+
56
69
  min = grep ? 1 : [min, max].min
57
70
  if auto
58
- msg = "#{msg}: [#{min}-#{max}#{if multiple
59
- "|,#{multiple.is_a?(::Numeric) ? "{#{multiple}}" : ''}"
60
- end}] "
71
+ auto.times { puts } if auto.is_a?(::Numeric)
72
+ if border == true
73
+ puts print_footer
74
+ elsif border
75
+ puts print_footer(border: border)
76
+ end
77
+ msg = "#{msg + (force ? ':' : '?')} [#{min}-#{max}#{if (n = multiple)
78
+ "|,#{n.is_a?(::Numeric) ? "{#{n}}" : '*'}"
79
+ end}] "
61
80
  end
62
81
  end
63
- valid = ->(s) { s.match?(/^\d+$/) && s.to_i.between?(min, max) }
82
+ between = ->(s) { s.match?(/^\d+$/) && s.to_i.between?(min, max) }
64
83
  Timeout.timeout(timeout) do
65
84
  while (ch = Readline.readline(msg))
66
85
  unless (ch = ch.strip).empty?
67
86
  if multiple
68
- a = ch.split(',')
69
- b = a.map do |s|
70
- s.strip!
71
- if s =~ /^(\d+)-(\d+)$/
72
- next unless valid.call($1) && valid.call($2)
87
+ k = if ch == '*'
88
+ (min..max).to_a
89
+ else
90
+ ch.split(/\s*,\s*/).map! do |s|
91
+ if s =~ /^(\d+)-(\d+)$/
92
+ next unless between.call($1) && between.call($2)
73
93
 
74
- c = $1.to_i
75
- d = $2.to_i
76
- next (c..d).to_a if c < d
77
- elsif valid.call(s)
78
- s.to_i
94
+ i = $1.to_i
95
+ j = $2.to_i
96
+ next (i..j).to_a if i < j
97
+ elsif between.call(s)
98
+ s.to_i
99
+ end
100
+ end
101
+ end
102
+ unless k.include?(nil)
103
+ k.flatten!
104
+ k.uniq!
105
+ k.sort!
106
+ unless multiple.is_a?(::Numeric) && multiple != k.size
107
+ return index || !items ? k : k.map! { |i| items[i.pred] }
79
108
  end
80
109
  end
81
- unless b.include?(nil)
82
- b.flatten!
83
- b.uniq!
84
- b.sort!
85
- return items ? b.map! { |i| items[i - 1] } : b unless multiple.is_a?(::Numeric) && multiple != b.size
86
- end
87
- elsif valid.call(ch)
88
- return items ? items[ch.to_i - 1] : ch.to_i
110
+ elsif between.call(ch)
111
+ return index || !items ? ch.to_i : items[ch.to_i.pred]
89
112
  end
90
113
  end
91
114
  attempts -= 1
@@ -98,12 +121,11 @@ module Squared
98
121
  puts
99
122
  exit 0
100
123
  else
101
- multiple ? [] : nil
124
+ [] if multiple
102
125
  end
103
126
  end
104
127
 
105
128
  def readline(msg, history = false, force: nil, multiline: nil, &blk)
106
- require 'readline'
107
129
  multiline = if multiline && Readline.respond_to?(:readmultiline)
108
130
  multiline.is_a?(::Enumerable) || block_given? ? multiline : [multiline.to_s]
109
131
  end
@@ -113,20 +135,16 @@ module Squared
113
135
  elsif block_given?
114
136
  Readline.readmultiline(msg, history, &blk)
115
137
  else
116
- Readline.readmultiline(msg, history) do |line|
117
- next if line.strip.empty?
118
-
119
- multiline.any? { |val| line.split.last.end_with?(val.to_s) }
120
- end
138
+ Readline.readmultiline(msg, history) { |line| multiline.any? { |val| line.split.last.end_with?(val.to_s) } }
121
139
  end
122
140
  end
123
141
  case force
124
142
  when ::TrueClass, ::FalseClass
125
- msg = "#{msg} %s " % if multiline
126
- multiline.is_a?(::Enumerable) ? "{#{multiline.to_a.join('|')}}" : multiline
127
- else
128
- "(#{force ? 'required' : 'optional'}):"
129
- end
143
+ msg = "#{msg}%s%s " % if multiline
144
+ [' ', multiline.is_a?(::Enumerable) ? "{#{multiline.to_a.join('|')}}" : multiline]
145
+ else
146
+ [force ? ':' : '?', '']
147
+ end
130
148
  ret = (prompt.call || '').strip
131
149
  multiline.each { |val| break if ret.delete_suffix!(val.to_s) } if multiline.is_a?(::Enumerable)
132
150
  exit 1 if force && ret.empty?