squared 0.0.9 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90258e1d46b51b15fcb414cddf85407968d227b5032797e76ead83befa5d302b
4
- data.tar.gz: a01b153f7c26b7f15a24cc66a159e47e42bc117e7b0810d7146c6ca3766f3cd0
3
+ metadata.gz: d000551aa00185dae3ea1bc7e3a61c64af96dce7d717339483d8cb572ca12d43
4
+ data.tar.gz: 229f06052512ee06d16e43688602753a867025e8cf39b6b1c6dfafa69805fdbc
5
5
  SHA512:
6
- metadata.gz: 484c0af12a637957f3ed9ddc43b6f08bee1f3dbcba50a2acdfdd0dcb7bb929b83b64d40fd8bbf17ceddc5d8177a27773e4193198cbc80f2991651bf55366b2c8
7
- data.tar.gz: a9617a969c41e971235b98bc730fd440f6c913a2ce21b9e8acb1907ae170ac72da1bb5d72df8650c1833827599fb0d95ab4aae19e5c75249dac07c0096efe077
6
+ metadata.gz: 5efe60578369457075929b95ba8ea628d39d7324d40f84c397e07606c8c5fe48f07f797b940d7ca34a80321bb3f1435494c36afcd95432cbe8924e4353099f56
7
+ data.tar.gz: daf832c4fbcf0e80459317be7b91781cfea9b653b3368cdbc090101d67ca59aff8b38940c2803100e322ca14bd25f45ccf37ef27bb348312b724c45babeb3393
data/README.md CHANGED
@@ -16,9 +16,6 @@
16
16
  * [E-mc](https://github.com/anpham6/e-mc#readme)
17
17
  * [Pi-r](https://github.com/anpham6/pi-r#readme)
18
18
 
19
- > [!NOTE]
20
- > `squared-express` originally was intended to be used as a HTTP/1.1 insecure development server for Android projects. `E-mc` was created a few years later to produce static outputs for any kind of development accessible through a simple REST API. Gradually any new development that calls the REST API will use [NGINX Unit](https://unit.nginx.org) as the *production* application runtime to efficiently serve static assets. Middleware and application code can be conducted in the same manner with Express and other programming frameworks (e.g. Ruby). Maintanence updates with subsequent `E-mc` releases supporting legacy development environments will be tested only against `Express 4.19` although most existing applications can be upgraded without any problems. Upgrading to `Express 5` is not recommended as it offers no new noticeable features (e.g. HTTP/2) and possibly compatibility issues with template libraries.
21
-
22
19
  ## Installation
23
20
 
24
21
  * NodeJS 16 LTS
@@ -113,14 +110,13 @@ cd workspaces # REPO_ROOT
113
110
 
114
111
  wget https://raw.githubusercontent.com/anpham6/squared/master/Rakefile
115
112
 
116
- # REPO_DOCS=1 (venv)
117
113
  rake -T # List tasks
118
114
 
119
115
  # REPO_BUILD={dev,prod}
120
116
  # PIPE_FAIL=1
121
117
  rake repo:init # nightly
122
- # OR
123
- rake repo:init[latest] # 0.11.x
118
+ rake repo:init[latest] # squared
119
+ rake repo:init[0.11.x] # e-mc
124
120
  # OR
125
121
  REPO_ROOT=/tmp/123 NODE_INSTALL=pnpm repo:init
126
122
  ```
data/README.ruby.md CHANGED
@@ -4,9 +4,17 @@
4
4
  * [manifest](https://github.com/anpham6/squared-repo)
5
5
  * [docs](https://squared.readthedocs.io)
6
6
 
7
- ## Prerequisites
7
+ ## Version Compatibility
8
8
 
9
- * Ruby 2.4
9
+ | Date | squared | Ruby 2 | Ruby 3 |
10
+ | :------: | ------: | -----: | -----: |
11
+ | 12-07-24 | 0.1.0 | 2.4.0 | 3.0.0 |
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ gem install squared
17
+ ```
10
18
 
11
19
  ### Optional
12
20
 
@@ -20,42 +28,67 @@ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
20
28
  chmod a+rx ~/.bin/repo
21
29
  ```
22
30
 
23
- ## Installation
24
-
25
- ```sh
26
- gem install squared
27
- ```
28
-
29
31
  ## Example - Rakefile
30
32
 
31
33
  Projects from any accessible folder can be added either relative to `REPO_ROOT` or absolutely. The same Rakefile can also manage other similarly cloned repositories remotely by setting the `REPO_ROOT` environment variable to the location. Missing projects will simply be excluded from the task runner.
32
34
 
33
35
  ```ruby
34
36
  require "squared"
35
- require "squared/workspace/repo" # Repo (optional)
37
+
38
+ require "squared/workspace"
39
+ require "squared/workspace/repo" # Optional
40
+ require "squared/workspace/project/node" #
41
+ require "squared/workspace/project/python" #
42
+ require "squared/workspace/project/ruby" #
43
+ # OR
44
+ require "squared/app" # All workspace related modules
36
45
 
37
46
  # NODE_ENV = production
38
47
  # REPO_ROOT = /workspaces
39
- # REPO_HOME = /workspaces/squared
48
+ # REPO_HOME = /workspaces/squared (Dir.pwd)
40
49
  # rake = /workspaces/squared/Rakefile
50
+ # pathname = /workspaces/pathname
51
+ # optparse = /workspaces/optparse
52
+ # log = /workspaces/logger
53
+ # emc = /workspaces/e-mc
54
+ # pir = /workspaces/pi-r
55
+ # squared = /workspaces/squared
56
+ # cli = /workspaces/squared/publish/sqd-cli
57
+ # sqd-serve = /workspaces/squared/publish/sqd-serve
58
+ # sqd = /workspaces/squared/sqd
41
59
 
42
60
  Workspace::Application
43
- .new(main: "squared") # Dir.pwd? (main? is implicitly basename)
44
- .banner("group", "project", styles: %i[yellow black], border: "bold") # name | project | path | ref | group?
45
- .repo("https://github.com/anpham6/squared-repo", "nightly", run: "build", ref: :node) # Repo (optional)
61
+ .new(Dir.pwd, main: "squared") # Dir.pwd? (main? is implicitly basename)
62
+ .banner("group", "project", styles: %i[yellow black], border: "bold") # name | project | path | ref | group? | parent? | version?
63
+ .repo("https://github.com/anpham6/squared-repo", "nightly", script: ["build:dev", "build:prod"], ref: :node) # Repo (optional)
46
64
  .run("rake install", ref: :ruby)
47
65
  .depend(false, group: "default")
48
66
  .clean("rake clean", group: "default")
49
67
  .clean(["build/"], group: "app")
50
- .add("pathname", run: "rake compile", copy: "rake install", test: "rake test", group: "default", ref: :ruby) # Ruby (with C extensions)
68
+ .log({ file: "tmp/%Y-%m-%d.log", level: "debug" }, group: "app")
69
+ .add("pathname", run: "rake compile", copy: "rake install", test: "rake test", group: "default", env: { # Ruby (with C extensions)
70
+ "CFLAGS" => "-fPIC -O1"
71
+ })
51
72
  .add("optparse", doc: "rake rdoc", group: "default") # Uses bundler/gem_tasks (without C extensions)
52
- .add("logger", copy: { from: "lib", glob: "**/*.rb", gemdir: "~/.rvm/gems/ruby-3.3.5/gems/logger-1.6.1" }, clean: ["tmp/"]) # autodetect: true
53
- .add("emc", "e-mc", copy: { from: "publish", into: "@e-mc", also: [:pir, "squared-express/"] }, ref: :node) # Node
54
- .add("pir", "pi-r", copy: { from: "publish", into: "@pi-r" }, clean: ["publish/**/*.js", "tmp/"]) # Trailing slash required for directories
55
- .add("squared", log: { file: "tmp/%Y-%m-%d.log", level: "debug" }, group: "app") # Copy target (main)
56
- .add("sqd", "squared/sqd", script: ["build:sqd", "prod:sqd"], depend: false, clean: ["build/sqd/"], exclude: :git) # NPM workspaces
73
+ .add("logger", copy: { from: "lib", glob: "**/*.rb", into: "~/.rvm/gems/ruby-3.3.5/gems/logger-1.6.1" }, clean: ["tmp/"]) # autodetect: true
74
+ .add("e-mc", "emc", copy: { from: "publish", scope: "@e-mc", also: [:pir, "squared-express/"] }, ref: :node) # Node
75
+ .add("pi-r", "pir", copy: { from: "publish", scope: "@pi-r" }, clean: ["publish/**/*.js", "tmp/"]) # Trailing slash required for directories
76
+ .add("squared", script: ["build:stage1", "build:stage2"], group: "app") do # Copy target (main)
77
+ add("publish/sqd-cli", "cli", exclude: [:git]) # rake cli:build
78
+ add("publish/sqd-serve") # rake sqd-serve:build
79
+ add("publish/sqd-admin", group: "sqd", exclude: [:base])
80
+ # OR
81
+ with(exclude: [:base]) { add("publish/*", "packages") } # rake packages:sqd-serve:build
82
+ # OR
83
+ add(["publish/sqd-cli", "publish/sqd-serve", "publish/sqd-admin"], true, exclude: [:base]) # rake squared:sqd-serve:build
84
+ end
85
+ .add("squared/sqd", exclude: [:git]) do
86
+ variable_set :script, "build:sqd" # Override detection
87
+ variable_set :depend, false
88
+ variable_set :clean, ["build/sqd/"]
89
+ end
57
90
  .style("banner", 255.255) # 256 colors (fg | fg.bg | -0.bg)
58
- .build(default: "status", parallel: ["pull", "fetch", "rebase", "copy", "clean"]) do |workspace|
91
+ .build(default: "status", parallel: ["pull", "fetch", "rebase", "copy", "clean", "outdated:ruby"]) do |workspace|
59
92
  workspace
60
93
  .enable_aixterm
61
94
  .style({
@@ -63,45 +96,40 @@ Workspace::Application
63
96
  border: "bright_white"
64
97
  })
65
98
  end
66
- # pathname = /workspaces/pathname
67
- # optparse = /workspaces/optparse
68
- # log = /workspaces/logger
69
- # emc = /workspaces/e-mc
70
- # pir = /workspaces/pi-r
71
- # squared = /workspaces/squared
72
99
 
73
- Workspace::Application
74
- .new(ENV["SQUARED_DIR"], prefix: "rb", common: false) # Local styles
75
- .group("default", "ruby/", run: "rake build", copy: "rake install", clean: "rake clean", ref: :ruby, override: {
76
- pathname: {
77
- run: "rake compile" # rake rb:pathname:build
78
- }
79
- })
80
- .with(:python) do # ref=Symbol | group=String
81
- banner("path", command: false, styles: "yellow") #
82
- doc("make html") # rake rb:doc:python
83
- run(false) # rake rb:build:python (disabled)
84
- exclude(%i[base git]) # Project::Git.ref (superclass)
85
- add("android", "android-docs") # rake rb:android:doc
86
- add("chrome", "chrome-docs") # rake rb:chrome:doc
87
- end #
88
- .style("inline", "bold")
89
- .build
90
100
  # default = /workspaces/ruby/*
91
101
  # pathname = /workspaces/ruby/pathname
92
102
  # optparse = /workspaces/ruby/optparse
93
103
  # logger = /workspaces/ruby/logger
94
104
  # android = /workspaces/android-docs
95
105
  # chrome = /workspaces/chrome-docs
106
+
107
+ Workspace::Application
108
+ .new(ENV["SQUARED_HOME"], prefix: "rb", common: false) # Local styles
109
+ .group("ruby", "default", run: "rake build", copy: "rake install", clean: "rake clean", ref: :ruby, override: {
110
+ pathname: {
111
+ run: "rake compile" # rake rb:pathname:build
112
+ }
113
+ })
114
+ .with(:python) do # ref=Symbol | group=String
115
+ banner([:name, ': ', :version], "path") # chrome-docs: 0.1.0 | /workspaces/chrome-docs
116
+ doc("make html") # rake rb:doc:python
117
+ run(false) # rake rb:build:python (disabled)
118
+ exclude(%i[base git]) # Project::Git.ref (superclass)
119
+ add("android-docs", "android") # rake rb:android:doc
120
+ add("chrome-docs", "chrome") # rake rb:chrome:doc
121
+ end #
122
+ .style("inline", "bold")
123
+ .build
96
124
  ```
97
125
 
98
- **NOTE**: The use of "**ref**" (class name) is only necessary when running `repo:init` for the first time into an empty directory.
126
+ **NOTE**: The use of "**ref**" (class name) is only necessary when initializing an empty directory (e.g. *rake repo:init*).
99
127
 
100
128
  ## Usage
101
129
 
102
130
  ```sh
103
- rake -T # List tasks
104
- rake # rake status (usually "build")
131
+ rake -T # List tasks
132
+ rake # rake status (usually "build")
105
133
 
106
134
  # GIT_OPTIONS=rebase
107
135
  rake pull # All except "default" + "app"
@@ -123,7 +151,17 @@ rake clean:app # none + skip + ["build/"]
123
151
  rake clean:node # none + ["publish/**/*.js", "tmp/"] + ["build/"]
124
152
  ```
125
153
 
126
- ## Commands
154
+ ```sh
155
+ rake build:app # squared + cli + sqd-serve
156
+ rake squared:build:workspace # cli + sqd-serve
157
+ rake pull:sqd # sqd-admin
158
+ rake squared:pull:workspace # sqd-serve + sqd-admin
159
+ rake squared:outdated:workspace # cli + sqd-serve + sqd-admin
160
+ ```
161
+
162
+ ## Methods
163
+
164
+ Task:
127
165
 
128
166
  * run
129
167
  * depend
@@ -131,6 +169,11 @@ rake clean:node # none + ["publish/**/*.js", "tmp/"] + ["build/"]
131
169
  * doc
132
170
  * clean
133
171
 
172
+ Non-task:
173
+
174
+ * log
175
+ * exclude
176
+
134
177
  ## Styles
135
178
 
136
179
  * banner
@@ -139,6 +182,61 @@ rake clean:node # none + ["publish/**/*.js", "tmp/"] + ["build/"]
139
182
  * active
140
183
  * inline
141
184
  * major
185
+ * red
186
+ * yellow
187
+ * green
188
+
189
+ ## Environment
190
+
191
+ ### Build
192
+
193
+ ```ruby
194
+ # :env :run :opts
195
+ # LD_LIBRARY_PATH="path/to/lib" CFLAGS="-Wall" gcc a.c -o a.o -c
196
+ BUILD_${NAME} # gcc a.c -o a.o
197
+ BUILD_OPTS_${NAME} # -c
198
+ BUILD_ENV_${NAME} # {"LD_LIBRARY_PATH":"path/to/lib","CFLAGS":"-Wall"} (hash/json)
199
+
200
+ # :env :opts :script
201
+ # NODE_ENV="production" NO_COLOR="1" npm run --loglevel=error --workspaces=false build:dev
202
+ BUILD_${NAME} # build:dev
203
+ BUILD_OPTS_${NAME} # --loglevel=error --workspaces=false
204
+ BUILD_ENV_${NAME} # {"NODE_ENV":"production","NO_COLOR":"1"} (hash/json)
205
+ BUILD_DEV_${NAME} # pattern,0,1 (:dev)
206
+ BUILD_PROD_${NAME} # pattern,0,1 (:prod)
207
+
208
+ BUILD_${NAME}=0 # skip project
209
+ ```
210
+
211
+ These options also support the project specific suffix `${NAME}`. (e.g. LOG_FILE_SQUARED)
212
+
213
+ ### Logger
214
+
215
+ ```ruby
216
+ LOG_FILE # %Y-%m-%d.log
217
+ # OR
218
+ LOG_AUTO # year,y,month,m,day,d,1
219
+ # Optional
220
+ LOG_DIR # exist?
221
+ LOG_LEVEL # See gem "logger"
222
+ LOG_COLUMNS # terminal width (default: 80)
223
+ ```
224
+
225
+ ### Repo
226
+
227
+ ```ruby
228
+ REPO_ROOT # parent dir
229
+ REPO_HOME # project dir (main)
230
+ REPO_BUILD # run,script
231
+ REPO_GROUP # string
232
+ REPO_REF # e.g. ruby,node
233
+ REPO_DEV # pattern,0,1
234
+ REPO_PROD # pattern,0,1
235
+ REPO_WARN # 0,1
236
+ REPO_SYNC # 0,1
237
+ REPO_MANIFEST # e.g. latest,nightly,prod
238
+ REPO_TIMEOUT # confirm dialog (seconds)
239
+ ```
142
240
 
143
241
  ## LICENSE
144
242
 
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'workspace'
4
+ require_relative 'workspace/repo'
5
+ require_relative 'workspace/project/node'
6
+ require_relative 'workspace/project/python'
7
+ require_relative 'workspace/project/ruby'
8
+ require_relative 'config'
9
+
10
+ Common = Squared::Common
@@ -2,6 +2,15 @@
2
2
 
3
3
  module Squared
4
4
  module Common
5
+ ARG = {
6
+ PIPE: 1,
7
+ OUT: nil,
8
+ FAIL: false,
9
+ COMMON: true,
10
+ BANNER: true,
11
+ SPACE: ' => ',
12
+ COLOR: ENV.fetch('NO_COLOR', '').empty?
13
+ }
5
14
  VAR = {
6
15
  project: {},
7
16
  colors: {
@@ -65,6 +74,7 @@ module Squared
65
74
  end
66
75
 
67
76
  def __freeze__
77
+ ARG.freeze
68
78
  VAR.each_value(&:freeze)
69
79
  VAR[:theme].each_value(&:freeze)
70
80
  VAR.freeze
@@ -72,25 +82,17 @@ module Squared
72
82
 
73
83
  module_function
74
84
 
75
- def env(key, default = nil, equals: nil, ignore: nil, **)
76
- ret = ENV.fetch(key, '')
77
- return ret == equals.to_s unless equals.nil?
78
-
79
- ret.empty? || (ignore && as_a(ignore).any? { |val| ret == val.to_s }) ? default : ret
80
- end
81
-
82
- def message(*args, hint: nil)
83
- args.reject(&:empty?).join(' => ') + (hint ? " (#{hint})" : '')
84
- end
85
-
86
85
  def as_a(obj, meth = nil, flat: nil, compact: false)
87
86
  return [] if obj.nil?
88
87
 
89
- if !obj.is_a?(::Array)
90
- obj = [obj]
91
- elsif flat
92
- obj = obj.flatten(flat == true ? nil : flat)
88
+ unless obj.is_a?(::Array)
89
+ obj = if !obj.is_a?(::Hash) && obj.respond_to?(:to_a) && (val = obj.to_a).is_a?(::Array)
90
+ val
91
+ else
92
+ [obj]
93
+ end
93
94
  end
95
+ obj = obj.flatten(flat.is_a?(::Numeric) ? flat : nil) if flat
94
96
  obj = obj.map(&meth) if meth
95
97
  compact ? obj.compact : obj
96
98
  end
@@ -1,14 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require 'forwardable'
4
5
 
5
6
  module Squared
6
7
  module Common
7
- class JoinSet < ::Set
8
- def self.to_s
9
- super.to_s.match(/[^:]+$/)[0]
8
+ class SymSet
9
+ extend Forwardable
10
+
11
+ def_delegators :@data, :each, :each_with_index, :entries, :to_a, :include?
12
+
13
+ def initialize(data = [])
14
+ @data = Set.new(data)
10
15
  end
11
16
 
17
+ def add(val)
18
+ @data.add(val.to_sym)
19
+ end
20
+
21
+ def to_s
22
+ @data.to_s.sub('Set', SymSet.to_s)
23
+ end
24
+
25
+ alias inspect to_s
26
+ end
27
+
28
+ class JoinSet < ::Set
12
29
  def initialize(data = [], delim: ' ')
13
30
  super(data.compact)
14
31
  @delim = delim
@@ -40,7 +40,7 @@ module Squared
40
40
  private
41
41
 
42
42
  def sub_style(val, *args, styles: nil, pat: nil, index: 1)
43
- return val unless ENV.fetch('NO_COLOR', '').empty? && !__get__(:no_color)
43
+ return val unless ARG[:COLOR]
44
44
 
45
45
  if pat && index != 0
46
46
  return val unless (data = pat.match(val))
@@ -61,7 +61,7 @@ module Squared
61
61
  else
62
62
  s = ret
63
63
  end
64
- if type.is_a?(Numeric)
64
+ if type.is_a?(::Numeric)
65
65
  f, b = type.to_s.split('.')
66
66
  s = wrap.(s, ['38', '5', f]) if f[0] != '-' && f.to_i <= 255
67
67
  if b
@@ -77,7 +77,7 @@ module Squared
77
77
  code << c
78
78
  end
79
79
  else
80
- next unless (n = TEXT_STYLE.index { |style| style == t })
80
+ next unless (n = TEXT_STYLE.index(t))
81
81
 
82
82
  s = "\x1B[#{n + 1}m#{s}\x1B[#{n == 0 ? 22 : n + 21}m"
83
83
  end
@@ -106,11 +106,11 @@ module Squared
106
106
  out
107
107
  end
108
108
 
109
- def check_style(*args, empty: true)
109
+ def check_style(args, empty: true)
110
110
  ret = []
111
111
  colors = __get__(:colors)
112
112
  as_a(args, flat: true, compact: true).each do |val|
113
- if !val.is_a?(Numeric)
113
+ if !val.is_a?(::Numeric)
114
114
  val = val.to_sym
115
115
  ret << val if colors.key?(val) || TEXT_STYLE.include?(val)
116
116
  elsif val >= 0 && val <= 256
@@ -123,39 +123,41 @@ module Squared
123
123
  ret if empty || !ret.empty?
124
124
  end
125
125
 
126
- def apply_style(data, key, *args, empty: true)
127
- data = __get__(:theme)[data] if data.is_a?(Symbol)
128
- return unless data
126
+ def apply_style(data, key, args, empty: true)
127
+ return if data.is_a?(::Symbol) && (data = __get__(:theme)[data]).nil?
129
128
 
130
- set = ->(k, v) { data[k.to_sym] = check_style(v, empty: empty) }
131
- if key.is_a?(Hash)
132
- key.each { |k, v| set.(k, v || args.flatten) }
129
+ set = ->(k, v) { data[k] = check_style(v, empty: empty) }
130
+ if key.is_a?(::Hash)
131
+ key.each { |k, v| set.(k, v || args) }
133
132
  else
134
- set.(key, args.flatten)
133
+ set.(key.to_sym, args)
135
134
  end
136
135
  end
137
136
 
138
- def log_title(level, color: true)
139
- level = if level.is_a?(::Numeric)
140
- case level
141
- when Logger::DEBUG
142
- :debug
143
- when Logger::INFO
144
- :info
145
- when Logger::WARN
146
- :warn
147
- when Logger::ERROR
148
- :error
149
- when Logger::FATAL
150
- :fatal
151
- else
152
- :unknown
153
- end
154
- else
155
- level.to_s.downcase.to_sym
156
- end
137
+ def log_sym(level)
138
+ if level.is_a?(::Numeric)
139
+ case level
140
+ when Logger::DEBUG
141
+ :debug
142
+ when Logger::INFO
143
+ :info
144
+ when Logger::WARN
145
+ :warn
146
+ when Logger::ERROR
147
+ :error
148
+ when Logger::FATAL
149
+ :fatal
150
+ else
151
+ :unknown
152
+ end
153
+ else
154
+ level.to_s.downcase.to_sym
155
+ end
156
+ end
157
+
158
+ def log_title(level, color: ARG[:COLOR])
157
159
  theme = __get__(:theme)[:logger]
158
- styles = theme[level] || theme[level = :unknown]
160
+ styles = theme[level = log_sym(level)] || theme[level = :unknown]
159
161
  case (ret = +level.to_s.upcase)
160
162
  when 'WARN', 'ERROR', 'FATAL'
161
163
  ret += '!'
@@ -163,26 +165,56 @@ module Squared
163
165
  color ? sub_style(ret, *styles) : ret
164
166
  end
165
167
 
166
- def log_message(level, *args, subject: nil, hint: nil, color: true)
167
- msg = [log_title(level, color: color)]
168
- msg << (color ? sub_style(subject, :underline) : subject) if subject
169
- message(msg.join(' '), *args, hint: hint)
168
+ def log_message(level, *args, subject: nil, hint: nil, color: ARG[:COLOR])
169
+ args = args.map(&:to_s)
170
+ if args.size > 1
171
+ title = log_title(level, color: false)
172
+ sub = { pat: /^(#{title})(.+)$/, styles: __get__(:theme)[:logger][log_sym(level)] } if color
173
+ emphasize(args, title: title + (subject ? " #{subject}" : ''), sub: sub)
174
+ else
175
+ msg = [log_title(level, color: color)]
176
+ if subject
177
+ msg << (color ? sub_style(subject, :underline) : subject)
178
+ else
179
+ msg += args
180
+ args.clear
181
+ end
182
+ message(msg.join(' '), *args, hint: hint)
183
+ end
170
184
  end
171
185
 
172
- def raise_error(*args, hint: nil, kind: ArgumentError)
173
- raise kind, message(*args, hint: hint)
186
+ def puts_oe(*args, pipe: 1)
187
+ if pipe.is_a?(::Pathname)
188
+ begin
189
+ File.open(pipe, 'a') do |f|
190
+ br = File::SEPARATOR == '\\' ? "\r\n" : "\n"
191
+ args.flatten.each { |val| f.write("#{strip_style(val.chomp)}#{br}") }
192
+ end
193
+ return
194
+ rescue StandardError
195
+ pipe = 2
196
+ end
197
+ end
198
+ (pipe == 2 ? $stderr : $stdout).puts(*args)
174
199
  end
175
200
 
176
201
  module_function
177
202
 
178
- def emphasize(val, title: nil, cols: nil, sub: nil, pipe: nil)
203
+ def message(*args, hint: nil, empty: false)
204
+ (empty ? args.reject { |val| val.nil? || val.empty? } : args).join(ARG[:SPACE]) + (hint ? " (#{hint})" : '')
205
+ end
206
+
207
+ def emphasize(val, title: nil, footer: nil, cols: nil, sub: nil, border: nil, pipe: nil)
179
208
  n = 0
180
- if title
181
- title = title.to_s
182
- n = title.size
209
+ set = lambda do |l|
210
+ ret = as_a(l, :to_s)
211
+ n = ret.max_by(&:size).size
212
+ ret
183
213
  end
214
+ title = set.(title) if title
215
+ footer = set.(footer) if footer
184
216
  if val.is_a?(::Array)
185
- lines = val
217
+ lines = val.map(&:to_s)
186
218
  else
187
219
  lines = val.to_s.lines.map(&:chomp)
188
220
  lines[0] = "#{val.class}: #{lines.first}" if (err = val.is_a?(::StandardError))
@@ -194,24 +226,50 @@ module Squared
194
226
  end
195
227
  out = []
196
228
  bord = '-' * (n + 4)
229
+ bord = sub_style(bord, styles: border) if border
197
230
  sub = [sub] if sub && !sub.is_a?(::Array)
198
231
  pr = lambda do |line|
199
232
  s = line.ljust(n)
200
233
  sub&.each { |h| s = sub_style(s, **h) }
201
- "| #{s} |"
234
+ s = "| #{s} |"
235
+ if border
236
+ s = sub_style(s, pat: /^(\|)(.+)$/m, styles: border)
237
+ s = sub_style(s, pat: /^(.+)(\|)$/m, styles: border, index: 2)
238
+ end
239
+ s
202
240
  end
203
241
  out << bord
204
- out << pr.(title) << bord if title
242
+ if title
243
+ out += title.map { |t| pr.(t) }
244
+ out << bord
245
+ end
205
246
  lines.each { |line| out << pr.(line) }
206
247
  out << bord
248
+ out += footer if footer
207
249
  if block_given?
208
250
  yield out
209
- elsif pipe.respond_to?(:puts)
251
+ elsif pipe
252
+ case pipe
253
+ when 0
254
+ pipe = $stdin
255
+ when 2
256
+ pipe = $stderr
257
+ else
258
+ pipe = $stdout unless pipe.respond_to?(:puts)
259
+ end
210
260
  pipe.puts out
211
261
  else
212
262
  err ? warn(out) : puts(out)
213
263
  end
214
264
  end
265
+
266
+ def strip_style(val)
267
+ val.gsub(/\x1B\[(\d+;?)+m/, '')
268
+ end
269
+
270
+ def raise_error(*args, hint: nil, kind: ArgumentError)
271
+ raise kind, message(*args, hint: hint, empty: true)
272
+ end
215
273
  end
216
274
  end
217
275
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'readline'
4
+
5
+ module Squared
6
+ module Common
7
+ module Prompt
8
+ module_function
9
+
10
+ def confirm(msg, default = nil, agree: 'Y', cancel: 'N', attempts: 5, timeout: 15)
11
+ require 'timeout'
12
+ agree = /^#{agree}$/i if agree.is_a?(::String)
13
+ cancel = /^#{cancel}$/i if cancel.is_a?(::String)
14
+ Timeout.timeout(timeout) do
15
+ begin
16
+ while (ch = Readline.readline(msg, true))
17
+ ch = ch.chomp
18
+ ch = default if ch.empty?
19
+ case ch
20
+ when agree
21
+ return true
22
+ when cancel
23
+ return false
24
+ end
25
+ attempts -= 1
26
+ exit 1 unless attempts >= 0
27
+ end
28
+ rescue Interrupt
29
+ puts
30
+ exit 0
31
+ else
32
+ false
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end