sevgi 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/LICENSE +674 -0
  4. data/README.md +15 -0
  5. data/bin/sevgi +136 -0
  6. data/lib/sevgi/errors.rb +11 -0
  7. data/lib/sevgi/external.rb +8 -0
  8. data/lib/sevgi/function.rb +35 -0
  9. data/lib/sevgi/geometry/elements/rect.rb +133 -0
  10. data/lib/sevgi/geometry/elements/segment.rb +102 -0
  11. data/lib/sevgi/geometry/elements.rb +41 -0
  12. data/lib/sevgi/geometry/equation/line/diagonal.rb +90 -0
  13. data/lib/sevgi/geometry/equation/line/horizontal.rb +15 -0
  14. data/lib/sevgi/geometry/equation/line/vertical.rb +52 -0
  15. data/lib/sevgi/geometry/equation/line.rb +38 -0
  16. data/lib/sevgi/geometry/equation.rb +3 -0
  17. data/lib/sevgi/geometry/errors.rb +5 -0
  18. data/lib/sevgi/geometry/external.rb +15 -0
  19. data/lib/sevgi/geometry/operation/align.rb +40 -0
  20. data/lib/sevgi/geometry/operation/sweep.rb +52 -0
  21. data/lib/sevgi/geometry/operation.rb +28 -0
  22. data/lib/sevgi/geometry/point.rb +62 -0
  23. data/lib/sevgi/geometry.rb +10 -0
  24. data/lib/sevgi/graphics/attribute.rb +125 -0
  25. data/lib/sevgi/graphics/canvas.rb +51 -0
  26. data/lib/sevgi/graphics/content.rb +72 -0
  27. data/lib/sevgi/graphics/document/base.rb +51 -0
  28. data/lib/sevgi/graphics/document/default.rb +19 -0
  29. data/lib/sevgi/graphics/document/html.rb +11 -0
  30. data/lib/sevgi/graphics/document/inkscape.rb +17 -0
  31. data/lib/sevgi/graphics/document/minimal.rb +11 -0
  32. data/lib/sevgi/graphics/document.rb +53 -0
  33. data/lib/sevgi/graphics/element.rb +85 -0
  34. data/lib/sevgi/graphics/external.rb +15 -0
  35. data/lib/sevgi/graphics/mixtures/core.rb +81 -0
  36. data/lib/sevgi/graphics/mixtures/duplicate.rb +36 -0
  37. data/lib/sevgi/graphics/mixtures/hatch.rb +21 -0
  38. data/lib/sevgi/graphics/mixtures/identify.rb +93 -0
  39. data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
  40. data/lib/sevgi/graphics/mixtures/lint.rb +33 -0
  41. data/lib/sevgi/graphics/mixtures/render.rb +149 -0
  42. data/lib/sevgi/graphics/mixtures/replicate.rb +125 -0
  43. data/lib/sevgi/graphics/mixtures/save.rb +23 -0
  44. data/lib/sevgi/graphics/mixtures/transform.rb +78 -0
  45. data/lib/sevgi/graphics/mixtures/underscore.rb +21 -0
  46. data/lib/sevgi/graphics/mixtures/validate.rb +27 -0
  47. data/lib/sevgi/graphics/mixtures/wrappers.rb +63 -0
  48. data/lib/sevgi/graphics/mixtures.rb +16 -0
  49. data/lib/sevgi/graphics.rb +10 -0
  50. data/lib/sevgi/internal/constant.rb +5 -0
  51. data/lib/sevgi/internal/dim.rb +13 -0
  52. data/lib/sevgi/internal/function/file.rb +30 -0
  53. data/lib/sevgi/internal/function/float.rb +35 -0
  54. data/lib/sevgi/internal/function/formula.rb +71 -0
  55. data/lib/sevgi/internal/function/math.rb +43 -0
  56. data/lib/sevgi/internal/function/string.rb +13 -0
  57. data/lib/sevgi/internal/function.rb +7 -0
  58. data/lib/sevgi/internal/list.rb +27 -0
  59. data/lib/sevgi/internal/locate.rb +47 -0
  60. data/lib/sevgi/internal/margin.rb +23 -0
  61. data/lib/sevgi/internal/minitest/script.rb +50 -0
  62. data/lib/sevgi/internal/minitest/shell.rb +71 -0
  63. data/lib/sevgi/internal/minitest/suite.rb +35 -0
  64. data/lib/sevgi/internal/minitest.rb +5 -0
  65. data/lib/sevgi/internal/paper.rb +26 -0
  66. data/lib/sevgi/internal.rb +10 -0
  67. data/lib/sevgi/standard/conform.rb +55 -0
  68. data/lib/sevgi/standard/data/attribute.rb +496 -0
  69. data/lib/sevgi/standard/data/element.rb +269 -0
  70. data/lib/sevgi/standard/data/specification.rb +1841 -0
  71. data/lib/sevgi/standard/data.rb +79 -0
  72. data/lib/sevgi/standard/errors.rb +28 -0
  73. data/lib/sevgi/standard/model.rb +82 -0
  74. data/lib/sevgi/standard.rb +26 -0
  75. data/lib/sevgi/utensils/external.rb +22 -0
  76. data/lib/sevgi/utensils/grid.rb +49 -0
  77. data/lib/sevgi/utensils/ruler.rb +47 -0
  78. data/lib/sevgi/utensils/tsquare.rb +28 -0
  79. data/lib/sevgi/utensils.rb +7 -0
  80. data/lib/sevgi/version.rb +5 -0
  81. data/lib/sevgi.rb +10 -0
  82. metadata +127 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Graphics
5
+ module Mixtures
6
+ module Transform
7
+ module InstanceMethods
8
+ def Translate(x, y = nil)
9
+ tap do
10
+ next if x.to_f == 0.0 && (y.nil? || y.to_f == 0.0)
11
+
12
+ attributes[:"transform+"] = "translate(#{(y ? [x, y] : [x]).join(" ")})"
13
+ end
14
+ end
15
+
16
+ def Scale(x, y = nil)
17
+ tap do
18
+ next if x.to_f == 0.0 && (y.nil? || y.to_f == 0.0)
19
+
20
+ attributes[:"transform+"] = "scale(#{(y ? [x, y] : [x]).join(", ")})"
21
+ end
22
+ end
23
+
24
+ def Scale!(...)
25
+ Scale(...).tap do
26
+ attributes[:"vector-effect"] = "non-scaling-stroke" unless attributes[:"vector-effect"]
27
+ end
28
+ end
29
+
30
+ def SkewX(a)
31
+ tap do
32
+ next if a.to_f == 0.0
33
+
34
+ attributes[:"transform+"] = "skewX(#{a})"
35
+ end
36
+ end
37
+
38
+ def SkewY(a)
39
+ tap do
40
+ next if a.to_f == 0.0
41
+
42
+ attributes[:"transform+"] = "skewY(#{a})"
43
+ end
44
+ end
45
+
46
+ def Rotate(a, *origin)
47
+ tap do
48
+ ArgumentError.("Incorrect origin (two coordinates required): #{origin}") if !origin.empty? && origin.size != 2
49
+
50
+ next if a.to_f == 0.0
51
+
52
+ attributes[:"transform+"] = "rotate(#{[a, *origin].join(", ")})"
53
+ end
54
+ end
55
+
56
+ def Matrix(*values)
57
+ tap do
58
+ ArgumentError.("Incorrect transform matrix (six values required): #{values}") if values.size != 6
59
+
60
+ next if values.map(&:to_f).all? { _1 == 0.0 }
61
+
62
+ attributes[:"transform+"] = "matrix(#{values.join(" ")})"
63
+ end
64
+ end
65
+
66
+ def Align(position, inner:, outer:)
67
+ return self unless position && inner && outer
68
+
69
+ case position.to_sym
70
+ when :center then Translate((outer.width - inner.width) / 2.0, (outer.height - inner.height) / 2.0)
71
+ else ArgumentError.("Unsupported alignment: #{position}")
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Graphics
5
+ module Mixtures
6
+ module Underscore
7
+ module InstanceMethods
8
+ def _(*contents)
9
+ self.class.call(:_, parent: self, contents:)
10
+ end
11
+
12
+ def Ancestral
13
+ {}.tap do |result|
14
+ Root.Traverse { |element| result.merge!(element[:_]) if element.has?(:_) }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sevgi/standard"
4
+
5
+ module Sevgi
6
+ module Graphics
7
+ module Mixtures
8
+ module Validate
9
+ module InstanceMethods
10
+ def CData
11
+ return if !contents || contents.empty?
12
+
13
+ Graphics.Text(contents)
14
+ end
15
+
16
+ def Validate
17
+ Traverse do |element|
18
+ Standard.conform(
19
+ element.name, attributes: element.attributes.list, cdata: element.CData, elements: element.children.map(&:name)
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Graphics
5
+ module Mixtures
6
+ module Wrappers
7
+ module InstanceMethods
8
+ def Cline(x1: 0, y1: 0, x2:, y2:, **)
9
+ path(d: "M #{x1} #{y1} L #{x2} #{y2}", **)
10
+ end
11
+
12
+ def Hline(x1: 0, y1: 0, x2:, **)
13
+ path(d: "M #{x1} #{y1} H #{x2}", **)
14
+ end
15
+
16
+ def Vline(x1: 0, y1: 0, y2:, **)
17
+ path(d: "M #{x1} #{y1} V #{y2}", **)
18
+ end
19
+
20
+ def cline(x1: 0, y1: 0, angle:, length:, **)
21
+ dx = length * ::Math.cos(angle.to_f / 180 * ::Math::PI)
22
+ dy = length * ::Math.sin(angle.to_f / 180 * ::Math::PI)
23
+ path(d: "M #{x1} #{y1} l #{dx} #{dy}", **)
24
+ end
25
+
26
+ def css(hash, **)
27
+ style(Content::CSS.new(hash), type: "text/css", **)
28
+ end
29
+
30
+ def cxline(x1: 0, y1: 0, angle:, dx:, **)
31
+ dy = dx * ::Math.tan(angle.to_f / 180 * ::Math::PI)
32
+ path(d: "M #{x1} #{y1} l #{dx} #{dy}", **)
33
+ end
34
+
35
+ def cyline(x1: 0, y1: 0, angle:, dy:, **)
36
+ dx = dy / ::Math.tan(angle.to_f / 180 * ::Math::PI)
37
+ path(d: "M #{x1} #{y1} l #{dx} #{dy}", **)
38
+ end
39
+
40
+ def hline(x1: 0, y1: 0, length:, **)
41
+ path(d: "M #{x1} #{y1} h #{length}", **)
42
+ end
43
+
44
+ def layer(...)
45
+ g(...)
46
+ end
47
+
48
+ def segment(...)
49
+ Cline(...)
50
+ end
51
+
52
+ def square(length:, **)
53
+ rect(width: length, height: length, **)
54
+ end
55
+
56
+ def vline(x1: 0, y1: 0, length:, **)
57
+ path(d: "M #{x1} #{y1} v #{length}", **)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mixtures/core"
4
+
5
+ require_relative "mixtures/duplicate"
6
+ require_relative "mixtures/hatch"
7
+ require_relative "mixtures/identify"
8
+ require_relative "mixtures/inkscape"
9
+ require_relative "mixtures/lint"
10
+ require_relative "mixtures/render"
11
+ require_relative "mixtures/replicate"
12
+ require_relative "mixtures/save"
13
+ require_relative "mixtures/transform"
14
+ require_relative "mixtures/underscore"
15
+ require_relative "mixtures/validate"
16
+ require_relative "mixtures/wrappers"
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "graphics/attribute"
4
+ require_relative "graphics/content"
5
+ require_relative "graphics/canvas"
6
+ require_relative "graphics/element"
7
+ require_relative "graphics/mixtures"
8
+ require_relative "graphics/document"
9
+
10
+ require_relative "graphics/external"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ Undefined = Object.new.freeze
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ Dim = Data.define(:width, :height, :unit) do
5
+ def initialize(width:, height:, unit: "mm") = super(width: Float(width), height: Float(height), unit:)
6
+
7
+ def longest = deconstruct.max
8
+
9
+ def rect = Geometry::Rect[width, height]
10
+
11
+ def shortest = deconstruct.min
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Sevgi
6
+ module Function
7
+ module File
8
+ def changed?(file, content)
9
+ ::File.exist?(file) ? Digest::SHA1.digest(::File.read(file)) != Digest::SHA1.digest(content) : true
10
+ end
11
+
12
+ def out(content, *paths, smart: false)
13
+ if paths.empty?
14
+ ::Kernel.puts(content)
15
+ else
16
+ file = ::File.expand_path(::File.join(*paths))
17
+ output = "#{content.chomp}\n"
18
+
19
+ ::File.write(file, output) if !smart || changed?(file, output)
20
+ end
21
+ end
22
+
23
+ def variations(name, dirs, exts)
24
+ dirs.product(exts).map { |dir, ext| ::File.join(dir, "#{name}.#{ext}") }
25
+ end
26
+ end
27
+
28
+ extend File
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Function
5
+ module Float
6
+ @precision = PRECISION = 6
7
+
8
+ class << self
9
+ attr_accessor :precision
10
+ end
11
+
12
+ def approx(float, precision = nil) = float.round(precision || Function::Float.precision)
13
+
14
+ def eq?(left, right, precision: nil) = approx(left, precision) == approx(right, precision)
15
+
16
+ def ge?(left, right, precision: nil) = approx(left, precision) >= approx(right, precision)
17
+
18
+ def gt?(left, right, precision: nil) = approx(left, precision) > approx(right, precision)
19
+
20
+ def le?(left, right, precision: nil) = approx(left, precision) <= approx(right, precision)
21
+
22
+ def lt?(left, right, precision: nil) = approx(left, precision) < approx(right, precision)
23
+
24
+ def nonzero?(...) = !zero?(...)
25
+
26
+ def prettify(*args) = args.map { (i = _1.to_i) == _1.to_f ? i : _1 }
27
+
28
+ def round(float, precision) = precision ? float.round(precision) : float
29
+
30
+ def zero?(value, precision: nil) = eq?(value, 0.0, precision:)
31
+ end
32
+
33
+ extend Float
34
+ end
35
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Function
5
+ module Formula
6
+ # Y
7
+ # ^
8
+ # | slope (Y/X)
9
+ # | .
10
+ # | .
11
+ # | pb +
12
+ # | . . coangle°
13
+ # | . .
14
+ # | . .
15
+ # | . .
16
+ # | (hypothenus) d dy
17
+ # | . .
18
+ # | . .
19
+ # | pa . angle° . pc
20
+ # | + . . dx . . +
21
+ # | 90° . ..
22
+ # | . . .
23
+ # | . . r
24
+ # | . ry .
25
+ # | . .
26
+ # | . + . rx . +
27
+ # | . . . noangle°
28
+ # +--------------+------------+---------------------> X
29
+ # | intercept (Y)
30
+ # |
31
+ # |
32
+ #
33
+
34
+ def coangle(angle) = 90.0 - angle # complementary angle
35
+
36
+ def angler(dx, dy) = atan(dy / dx)
37
+
38
+ def angles(slope) = atan(slope)
39
+
40
+ def distance(p, q) = ::Math.sqrt(dxp(p, q)**2 + dyp(p, q)**2)
41
+
42
+ def height(pa, pb) = dyp(pa, pb).abs
43
+
44
+ def hypothenus(dx, dy) = ::Math.sqrt(dx**2 + dy**2)
45
+
46
+ def intercept(point, angle, slope) = point.y - (slope * point.x) # point is pa or pb
47
+
48
+ def noangle(angle) = 90.0 + angle # normal angle
49
+
50
+ def rx(r, angle) = r * sin(angle)
51
+
52
+ def ry(r, angle) = r * cos(angle)
53
+
54
+ def slopea(angle) = tan(angle)
55
+
56
+ def sloper(dx, dy) = dy / dx
57
+
58
+ def width(pa, pb) = dxp(pa, pb).abs
59
+
60
+ def dxp(pa, pb) = pb.x - pa.x
61
+
62
+ def dyp(pa, pb) = pb.y - pa.y
63
+
64
+ def dxa(d, angle) = d * cos(angle)
65
+
66
+ def dya(d, angle) = d * sin(angle)
67
+ end
68
+
69
+ extend Formula
70
+ end
71
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Function
5
+ module Math
6
+ def acos(value) = to_degrees(::Math.acos(value))
7
+
8
+ def acot(value) = 90.0 - to_degrees(::Math.atan(value))
9
+
10
+ def asin(value) = to_degrees(::Math.asin(value))
11
+
12
+ def atan(value) = to_degrees(::Math.atan(value))
13
+
14
+ def complement(degrees) = 90.0 - degrees
15
+
16
+ def cos(degrees) = ::Math.cos(to_radians(degrees))
17
+
18
+ def cot(degrees) = 1.0 / ::Math.tan(to_radians(degrees))
19
+
20
+ def golden = @golden ||= ((1.0 + ::Math.sqrt(5)) / 2.0)
21
+
22
+ def horizontal?(degrees, precision = nil) = zero?(degrees % 180.0, precision:)
23
+
24
+ def nangle(degrees) = degrees - 90.0
25
+
26
+ def sin(degrees) = ::Math.sin(to_radians(degrees))
27
+
28
+ def sqrt2 = @sqrt2 ||= ::Math.sqrt(2)
29
+
30
+ def sqrt2h = @sqrt2h ||= (sqrt2 / 2.0)
31
+
32
+ def tan(degrees) = ::Math.tan(to_radians(degrees))
33
+
34
+ def to_degrees(radians) = radians.to_f * 180 / ::Math::PI
35
+
36
+ def to_radians(degrees) = degrees.to_f / 180 * ::Math::PI
37
+
38
+ def vertical?(degrees, precision = nil) = zero?(degrees % 90.0, precision:)
39
+ end
40
+
41
+ extend Math
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Function
5
+ module String
6
+ def start_with_upper?(string) = upper?(string[0])
7
+
8
+ def upper?(char) = /[[:upper:]]/.match?(char)
9
+ end
10
+
11
+ extend String
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "function/file"
4
+ require_relative "function/float"
5
+ require_relative "function/formula"
6
+ require_relative "function/math"
7
+ require_relative "function/string"
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module List
5
+ def [](name) = data[name]
6
+
7
+ def import(**kwargs) = data.merge!(kwargs.reject { |key, _| data.key?(key) })
8
+
9
+ def valid?(name) = data.key?(name)
10
+
11
+ class << self
12
+ def extended(base)
13
+ super
14
+
15
+ base.class_exec do
16
+ @data = {}
17
+
18
+ class << self
19
+ attr_reader :data
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ private_constant :List
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ class Locate
5
+ Location = Data.define(:file, :slug, :dir)
6
+
7
+ private_constant :Location
8
+
9
+ attr_reader :slugs, :start
10
+
11
+ def initialize(slugs, start = Dir.pwd)
12
+ @slugs = slugs
13
+ @start = start
14
+ end
15
+
16
+ def call(&block)
17
+ origin = Dir.pwd
18
+ Dir.chdir(start)
19
+
20
+ here = Dir.pwd
21
+ until (found = match(&block))
22
+ Dir.chdir("..")
23
+ return if Dir.pwd == here
24
+
25
+ here = Dir.pwd
26
+ end
27
+
28
+ Location[::File.expand_path(found, here), found, here]
29
+ ensure
30
+ Dir.chdir(origin)
31
+ end
32
+
33
+ private
34
+
35
+ def match(&block)
36
+ finder = block || proc { |path| ::File.exist?(path) }
37
+
38
+ slugs.find { finder.call(_1) }
39
+ end
40
+
41
+ class << self
42
+ def call(*, &block) = new(*).call(&block)
43
+ end
44
+ end
45
+
46
+ private_constant :Locate
47
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ Margin = Data.define(:top, :right, :bottom, :left) do
5
+ def initialize(top: nil, right: nil, bottom: nil, left: nil)
6
+ case [top, right, bottom, left]
7
+ in Numeric, Numeric, Numeric, Numeric then # nop
8
+ in Numeric, Numeric, Numeric, NilClass then left = right
9
+ in Numeric, Numeric, NilClass, NilClass then bottom, left = top, right
10
+ in Numeric, NilClass, NilClass, NilClass then bottom, left, right = top, top, top
11
+ in NilClass, NilClass, NilClass, NilClass then top, bottom, left, right = 0, 0, 0, 0
12
+ end
13
+
14
+ super(top: Float(top), right: Float(right), bottom: Float(bottom), left: Float(left))
15
+ end
16
+
17
+ alias_method :to_a, :deconstruct
18
+
19
+ class << self
20
+ def zero = (@zero ||= self[0.0, 0.0, 0.0, 0.0])
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sevgi
4
+ module Test
5
+ class Script
6
+ attr_reader :file
7
+
8
+ def initialize(file) = @file = sanitize(file)
9
+
10
+ def dir = @dir ||= ::File.dirname(file)
11
+
12
+ def name = @name ||= ::File.basename(file, ".*")
13
+
14
+ def run(*) = Shell.run(file, *)
15
+
16
+ def suite = @suite ||= ::File.basename(dir)
17
+
18
+ def svg? = ::File.exist?(svg)
19
+
20
+ def svg = @svg ||= "#{dir}/#{name}.svg"
21
+
22
+ # A gross hack to avoid touching filesystem during testing. Intercept the Save methods to display output in stdout
23
+ # instead of saving an actual file. This hack meets the following criteria:
24
+ #
25
+ # - Does not make an invasive change to the library just for the sake of testing (hence the interception technique)
26
+ # - Do not create a separate file for the interceptor (hence feeding the interceptor through /dev/stdin)
27
+
28
+ INTERCEPTOR = <<~LIB
29
+ class Sevgi::Graphics::Document::Base
30
+ def Save(*, **) = Out(**)
31
+ def Save!(...) = Save(...)
32
+ end
33
+ LIB
34
+
35
+ def run_passive(*)
36
+ warn(" ==> #{file}")
37
+ Shell.run("sevgi", "-l", "/dev/stdin", file, *) { puts INTERCEPTOR }
38
+ end
39
+
40
+ private
41
+
42
+ def sanitize(file)
43
+ file.tap do
44
+ ArgumentError.("No such file: #{file}") unless ::File.exist?(file)
45
+ ArgumentError.("Not an executable: #{file}") unless ::File.executable?(file)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Sevgi
6
+ module Test
7
+ module Shell
8
+ Result = Data.define(:args, :out, :err, :exit_code) do
9
+ def cmd = args.join(" ")
10
+
11
+ def notok? = !ok?
12
+
13
+ def ok? = exit_code&.zero?
14
+
15
+ def outline = out.first
16
+
17
+ def to_s = out.join("\n")
18
+ end
19
+
20
+ # Adapted to popen3 from github.com/mina-deploy/mina
21
+ class Runner
22
+ def initialize
23
+ @coathooks = 0
24
+ end
25
+
26
+ def run(*args, &block)
27
+ out, err, status = Open3.popen3(*args) do |stdin, stdout, stderr, thread|
28
+ inputs(stdin, thread, &block) if block
29
+ outputs(stdout, stderr, thread)
30
+ end
31
+
32
+ Result[args, out, err, status.exitstatus]
33
+ end
34
+
35
+ private
36
+
37
+ def inputs(stdin, thread, &block)
38
+ stdin.instance_exec(thread, &block)
39
+ stdin.close unless stdin.closed?
40
+ end
41
+
42
+ def outputs(stdout, stderr, thread)
43
+ trap("INT") { handle_sigint(thread.pid) } # handle `^C`
44
+
45
+ out = stdout.readlines.map(&:chomp)
46
+ err = stderr.readlines.map(&:chomp)
47
+
48
+ [out, err, thread.value]
49
+ end
50
+
51
+ def handle_sigint(pid)
52
+ message, signal = if @coathooks > 1
53
+ ["SIGINT received again. Force quitting...", "KILL"]
54
+ else
55
+ ["SIGINT received.", "TERM"]
56
+ end
57
+
58
+ warn("\n#{message}")
59
+ ::Process.kill(signal, pid)
60
+ @coathooks += 1
61
+ rescue Errno::ESRCH
62
+ warn("No process to kill.")
63
+ end
64
+ end
65
+
66
+ extend self
67
+
68
+ def run(...) = Runner.new.run(...)
69
+ end
70
+ end
71
+ end