sevgi 0.0.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.
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