charming 0.1.2 → 0.1.3

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +3 -3
  3. data/lib/charming/controller/class_methods.rb +2 -2
  4. data/lib/charming/controller/command_palette.rb +2 -2
  5. data/lib/charming/controller/rendering.rb +2 -2
  6. data/lib/charming/controller/session_state.rb +1 -1
  7. data/lib/charming/generators/component_generator.rb +1 -1
  8. data/lib/charming/generators/templates/app/application.template +1 -1
  9. data/lib/charming/generators/templates/app/layout.template +3 -6
  10. data/lib/charming/generators/templates/app/view.template +1 -1
  11. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  12. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  13. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  14. data/lib/charming/internal/renderer/differential.rb +13 -5
  15. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  16. data/lib/charming/presentation/component.rb +3 -5
  17. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  18. data/lib/charming/presentation/components/command_palette.rb +94 -96
  19. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  20. data/lib/charming/presentation/components/empty_state.rb +47 -49
  21. data/lib/charming/presentation/components/form/builder.rb +52 -54
  22. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  23. data/lib/charming/presentation/components/form/field.rb +94 -96
  24. data/lib/charming/presentation/components/form/input.rb +53 -55
  25. data/lib/charming/presentation/components/form/note.rb +27 -29
  26. data/lib/charming/presentation/components/form/select.rb +84 -86
  27. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  28. data/lib/charming/presentation/components/form.rb +120 -122
  29. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  30. data/lib/charming/presentation/components/list.rb +123 -125
  31. data/lib/charming/presentation/components/markdown.rb +21 -23
  32. data/lib/charming/presentation/components/modal.rb +46 -48
  33. data/lib/charming/presentation/components/progressbar.rb +51 -53
  34. data/lib/charming/presentation/components/spinner.rb +40 -42
  35. data/lib/charming/presentation/components/table.rb +109 -111
  36. data/lib/charming/presentation/components/text_area.rb +219 -221
  37. data/lib/charming/presentation/components/text_input.rb +120 -122
  38. data/lib/charming/presentation/components/viewport.rb +218 -220
  39. data/lib/charming/presentation/layout/builder.rb +64 -66
  40. data/lib/charming/presentation/layout/overlay.rb +48 -50
  41. data/lib/charming/presentation/layout/pane.rb +122 -118
  42. data/lib/charming/presentation/layout/rect.rb +14 -16
  43. data/lib/charming/presentation/layout/screen_layout.rb +40 -42
  44. data/lib/charming/presentation/layout/split.rb +101 -103
  45. data/lib/charming/presentation/layout.rb +28 -30
  46. data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
  47. data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
  48. data/lib/charming/presentation/markdown/render_context.rb +12 -14
  49. data/lib/charming/presentation/markdown/renderer.rb +84 -86
  50. data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
  51. data/lib/charming/presentation/markdown.rb +4 -6
  52. data/lib/charming/presentation/template_view.rb +22 -24
  53. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  54. data/lib/charming/presentation/templates.rb +47 -49
  55. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  56. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  57. data/lib/charming/presentation/ui/border.rb +24 -26
  58. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  59. data/lib/charming/presentation/ui/canvas.rb +59 -61
  60. data/lib/charming/presentation/ui/style.rb +173 -175
  61. data/lib/charming/presentation/ui/theme.rb +133 -135
  62. data/lib/charming/presentation/ui/width.rb +12 -14
  63. data/lib/charming/presentation/ui.rb +69 -71
  64. data/lib/charming/presentation/view.rb +103 -105
  65. data/lib/charming/runtime.rb +23 -10
  66. data/lib/charming/version.rb +1 -1
  67. data/lib/charming.rb +3 -2
  68. metadata +2 -1
@@ -1,67 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- # Templates resolves and renders view templates by name. Template handlers are registered
6
- # for file extensions (e.g., `.tui.erb`) and the resolver searches `app/views/<name><ext>`
7
- # under the application root, falling back through registered extensions when the first
8
- # match is not found.
9
- module Templates
10
- # A resolved template: an on-disk *path* paired with the *handler* responsible for rendering it.
11
- ResolvedTemplate = Data.define(:path, :handler) do
12
- # Renders the template against *view* by delegating to the registered handler.
13
- def render(view)
14
- handler.render(path, view)
15
- end
4
+ # Templates resolves and renders view templates by name. Template handlers are registered
5
+ # for file extensions (e.g., `.tui.erb`) and the resolver searches `app/views/<name><ext>`
6
+ # under the application root, falling back through registered extensions when the first
7
+ # match is not found.
8
+ module Templates
9
+ # A resolved template: an on-disk *path* paired with the *handler* responsible for rendering it.
10
+ ResolvedTemplate = Data.define(:path, :handler) do
11
+ # Renders the template against *view* by delegating to the registered handler.
12
+ def render(view)
13
+ handler.render(path, view)
16
14
  end
15
+ end
17
16
 
18
- # Raised when no template file matches the given name under the application root.
19
- MissingTemplateError = Class.new(Error)
20
-
21
- class << self
22
- # Registers a template *handler* for a file *extension* (e.g., ".tui.erb" => ErbHandler).
23
- # The handler responds to `.render(path, view)`.
24
- def register(extension, handler)
25
- handlers[extension] = handler
26
- end
17
+ # Raised when no template file matches the given name under the application root.
18
+ MissingTemplateError = Class.new(Error)
27
19
 
28
- # Resolves a template by *name* under `app/views` of *root* (defaults to the current
29
- # working directory). Raises MissingTemplateError when no matching file exists.
30
- def resolve(name, root: nil)
31
- views_root = File.join(root || Dir.pwd, "app", "views")
32
- searched_paths = candidate_paths(views_root, name.to_s)
20
+ class << self
21
+ # Registers a template *handler* for a file *extension* (e.g., ".tui.erb" => ErbHandler).
22
+ # The handler responds to `.render(path, view)`.
23
+ def register(extension, handler)
24
+ handlers[extension] = handler
25
+ end
33
26
 
34
- searched_paths.each do |path|
35
- next unless File.file?(path)
27
+ # Resolves a template by *name* under `app/views` of *root* (defaults to the current
28
+ # working directory). Raises MissingTemplateError when no matching file exists.
29
+ def resolve(name, root: nil)
30
+ views_root = File.join(root || Dir.pwd, "app", "views")
31
+ searched_paths = candidate_paths(views_root, name.to_s)
36
32
 
37
- return ResolvedTemplate.new(path: path, handler: handler_for(path))
38
- end
33
+ searched_paths.each do |path|
34
+ next unless File.file?(path)
39
35
 
40
- raise MissingTemplateError, "Missing template #{name.inspect}. Searched: #{searched_paths.join(", ")}"
36
+ return ResolvedTemplate.new(path: path, handler: handler_for(path))
41
37
  end
42
38
 
43
- # Hash of registered handlers keyed by extension. Populated by `register`.
44
- def handlers
45
- @handlers ||= {}
46
- end
39
+ raise MissingTemplateError, "Missing template #{name.inspect}. Searched: #{searched_paths.join(", ")}"
40
+ end
47
41
 
48
- private
42
+ # Hash of registered handlers keyed by extension. Populated by `register`.
43
+ def handlers
44
+ @handlers ||= {}
45
+ end
49
46
 
50
- # Returns candidate paths under *views_root* for *name*. When the bare path has a known
51
- # extension, returns it directly; otherwise returns the path with each registered extension
52
- # appended (in registration order).
53
- def candidate_paths(views_root, name)
54
- path = File.expand_path(name, views_root)
55
- return [path] if handler_for(path)
47
+ private
56
48
 
57
- handlers.keys.map { |extension| "#{path}#{extension}" }
58
- end
49
+ # Returns candidate paths under *views_root* for *name*. When the bare path has a known
50
+ # extension, returns it directly; otherwise returns the path with each registered extension
51
+ # appended (in registration order).
52
+ def candidate_paths(views_root, name)
53
+ path = File.expand_path(name, views_root)
54
+ return [path] if handler_for(path)
59
55
 
60
- # Looks up the handler whose registered extension matches the end of *path*. Returns nil
61
- # when no handler matches.
62
- def handler_for(path)
63
- handlers.find { |extension, _handler| path.end_with?(extension) }&.last
64
- end
56
+ handlers.keys.map { |extension| "#{path}#{extension}" }
57
+ end
58
+
59
+ # Looks up the handler whose registered extension matches the end of *path*. Returns nil
60
+ # when no handler matches.
61
+ def handler_for(path)
62
+ handlers.find { |extension, _handler| path.end_with?(extension) }&.last
65
63
  end
66
64
  end
67
65
  end
@@ -1,88 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module UI
6
- class ANSICodes
7
- ATTRIBUTES = {
8
- bold: 1,
9
- faint: 2,
10
- italic: 3,
11
- underline: 4,
12
- reverse: 7,
13
- strikethrough: 9
14
- }.freeze
4
+ module UI
5
+ class ANSICodes
6
+ ATTRIBUTES = {
7
+ bold: 1,
8
+ faint: 2,
9
+ italic: 3,
10
+ underline: 4,
11
+ reverse: 7,
12
+ strikethrough: 9
13
+ }.freeze
15
14
 
16
- COLORS = {
17
- black: 30,
18
- red: 31,
19
- green: 32,
20
- yellow: 33,
21
- blue: 34,
22
- magenta: 35,
23
- cyan: 36,
24
- white: 37,
25
- bright_black: 90,
26
- bright_red: 91,
27
- bright_green: 92,
28
- bright_yellow: 93,
29
- bright_blue: 94,
30
- bright_magenta: 95,
31
- bright_cyan: 96,
32
- bright_white: 97
33
- }.freeze
15
+ COLORS = {
16
+ black: 30,
17
+ red: 31,
18
+ green: 32,
19
+ yellow: 33,
20
+ blue: 34,
21
+ magenta: 35,
22
+ cyan: 36,
23
+ white: 37,
24
+ bright_black: 90,
25
+ bright_red: 91,
26
+ bright_green: 92,
27
+ bright_yellow: 93,
28
+ bright_blue: 94,
29
+ bright_magenta: 95,
30
+ bright_cyan: 96,
31
+ bright_white: 97
32
+ }.freeze
34
33
 
35
- def initialize(attributes:, foreground:, background:)
36
- @attributes = attributes
37
- @foreground = foreground
38
- @background = background
39
- end
34
+ def initialize(attributes:, foreground:, background:)
35
+ @attributes = attributes
36
+ @foreground = foreground
37
+ @background = background
38
+ end
40
39
 
41
- def codes
42
- @codes ||= attribute_codes +
43
- color_codes(@foreground, foreground: true) +
44
- color_codes(@background, foreground: false)
45
- end
40
+ def codes
41
+ @codes ||= attribute_codes +
42
+ color_codes(@foreground, foreground: true) +
43
+ color_codes(@background, foreground: false)
44
+ end
46
45
 
47
- def apply(value)
48
- return value if codes.empty?
46
+ def apply(value)
47
+ return value if codes.empty?
49
48
 
50
- start = "\e[#{codes.join(";")}m"
51
- value.split("\n", -1).map { |line| "#{start}#{line.gsub("\e[0m", "\e[0m#{start}")}\e[0m" }.join("\n")
52
- end
49
+ start = "\e[#{codes.join(";")}m"
50
+ value.split("\n", -1).map { |line| "#{start}#{line.gsub("\e[0m", "\e[0m#{start}")}\e[0m" }.join("\n")
51
+ end
53
52
 
54
- private
53
+ private
55
54
 
56
- def attribute_codes
57
- @attributes.map { |attribute| ATTRIBUTES.fetch(attribute) }
58
- end
55
+ def attribute_codes
56
+ @attributes.map { |attribute| ATTRIBUTES.fetch(attribute) }
57
+ end
59
58
 
60
- def color_codes(color, foreground:)
61
- return [] unless color
62
- return indexed_color_code(color, foreground: foreground) if color.is_a?(Integer)
63
- return named_color_code(color, foreground: foreground) if COLORS.key?(color.to_sym)
64
- return truecolor_codes(color, foreground: foreground) if color.to_s.start_with?("#")
59
+ def color_codes(color, foreground:)
60
+ return [] unless color
61
+ return indexed_color_code(color, foreground: foreground) if color.is_a?(Integer)
62
+ return named_color_code(color, foreground: foreground) if COLORS.key?(color.to_sym)
63
+ return truecolor_codes(color, foreground: foreground) if color.to_s.start_with?("#")
65
64
 
66
- raise ArgumentError, "unknown color: #{color.inspect}"
67
- end
65
+ raise ArgumentError, "unknown color: #{color.inspect}"
66
+ end
68
67
 
69
- def named_color_code(color, foreground:)
70
- code = COLORS.fetch(color.to_sym)
71
- [foreground ? code : code + 10]
72
- end
68
+ def named_color_code(color, foreground:)
69
+ code = COLORS.fetch(color.to_sym)
70
+ [foreground ? code : code + 10]
71
+ end
73
72
 
74
- def indexed_color_code(color, foreground:)
75
- raise ArgumentError, "indexed color must be between 0 and 255" unless color.between?(0, 255)
73
+ def indexed_color_code(color, foreground:)
74
+ raise ArgumentError, "indexed color must be between 0 and 255" unless color.between?(0, 255)
76
75
 
77
- [foreground ? 38 : 48, 5, color]
78
- end
76
+ [foreground ? 38 : 48, 5, color]
77
+ end
79
78
 
80
- def truecolor_codes(color, foreground:)
81
- hex = color.to_s.delete_prefix("#")
82
- raise ArgumentError, "truecolor must be #rrggbb" unless hex.match?(/\A[0-9a-fA-F]{6}\z/)
79
+ def truecolor_codes(color, foreground:)
80
+ hex = color.to_s.delete_prefix("#")
81
+ raise ArgumentError, "truecolor must be #rrggbb" unless hex.match?(/\A[0-9a-fA-F]{6}\z/)
83
82
 
84
- [foreground ? 38 : 48, 2, hex[0..1].to_i(16), hex[2..3].to_i(16), hex[4..5].to_i(16)]
85
- end
83
+ [foreground ? 38 : 48, 2, hex[0..1].to_i(16), hex[2..3].to_i(16), hex[4..5].to_i(16)]
86
84
  end
87
85
  end
88
86
  end
@@ -1,94 +1,92 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module UI
6
- # ANSISlicer extracts a visible substring from a string that may contain ANSI
7
- # escape sequences, preserving the styling that is active at the start of
8
- # the slice and emitting a trailing reset if any styled content was copied.
9
- class ANSISlicer
10
- def self.slice(line, start_column, width)
11
- return "" unless width.positive?
12
-
13
- slice_range(line.to_s, start_column, start_column + width)
14
- end
4
+ module UI
5
+ # ANSISlicer extracts a visible substring from a string that may contain ANSI
6
+ # escape sequences, preserving the styling that is active at the start of
7
+ # the slice and emitting a trailing reset if any styled content was copied.
8
+ class ANSISlicer
9
+ def self.slice(line, start_column, width)
10
+ return "" unless width.positive?
11
+
12
+ slice_range(line.to_s, start_column, start_column + width)
13
+ end
15
14
 
16
- def self.slice_range(line, start_column, end_column)
17
- state = {column: 0, output: +"", active: [], started: false, styled: false}
15
+ def self.slice_range(line, start_column, end_column)
16
+ state = {column: 0, output: +"", active: [], started: false, styled: false}
18
17
 
19
- each_ansi_or_char(line) do |token, ansi|
20
- if ansi
21
- slice_ansi_token(token, state, start_column, end_column)
22
- else
23
- slice_char(token, state, start_column, end_column)
24
- end
18
+ each_ansi_or_char(line) do |token, ansi|
19
+ if ansi
20
+ slice_ansi_token(token, state, start_column, end_column)
21
+ else
22
+ slice_char(token, state, start_column, end_column)
25
23
  end
26
-
27
- terminate_slice(state)
28
24
  end
29
25
 
30
- def self.each_ansi_or_char(line)
31
- index = 0
32
- while index < line.length
33
- match = line.match(Width::ANSI_PATTERN, index)
34
- if match&.begin(0) == index
35
- yield match[0], true
36
- index = match.end(0)
37
- else
38
- yield line[index], false
39
- index += 1
40
- end
26
+ terminate_slice(state)
27
+ end
28
+
29
+ def self.each_ansi_or_char(line)
30
+ index = 0
31
+ while index < line.length
32
+ match = line.match(Width::ANSI_PATTERN, index)
33
+ if match&.begin(0) == index
34
+ yield match[0], true
35
+ index = match.end(0)
36
+ else
37
+ yield line[index], false
38
+ index += 1
41
39
  end
42
40
  end
41
+ end
43
42
 
44
- def self.slice_ansi_token(token, state, start_column, end_column)
45
- started = state[:started]
46
- update_active_styles(state[:active], token)
47
- return unless state[:column].between?(start_column, end_column - 1)
43
+ def self.slice_ansi_token(token, state, start_column, end_column)
44
+ started = state[:started]
45
+ update_active_styles(state[:active], token)
46
+ return unless state[:column].between?(start_column, end_column - 1)
48
47
 
49
- start_slice(state)
50
- if started
51
- state[:output] << token
52
- state[:styled] = !token.include?("[0m")
53
- end
48
+ start_slice(state)
49
+ if started
50
+ state[:output] << token
51
+ state[:styled] = !token.include?("[0m")
54
52
  end
53
+ end
55
54
 
56
- def self.slice_char(char, state, start_column, end_column)
57
- char_width = Width.measure(char)
58
- char_start = state[:column]
59
- char_end = char_start + char_width
60
- state[:column] = char_end
61
- return unless char_end > start_column && char_start < end_column
55
+ def self.slice_char(char, state, start_column, end_column)
56
+ char_width = Width.measure(char)
57
+ char_start = state[:column]
58
+ char_end = char_start + char_width
59
+ state[:column] = char_end
60
+ return unless char_end > start_column && char_start < end_column
62
61
 
63
- start_slice(state)
64
- state[:output] << char
65
- end
62
+ start_slice(state)
63
+ state[:output] << char
64
+ end
66
65
 
67
- def self.start_slice(state)
68
- return if state[:started]
66
+ def self.start_slice(state)
67
+ return if state[:started]
69
68
 
70
- state[:output] << state[:active].join
71
- state[:styled] = true unless state[:active].empty?
72
- state[:started] = true
73
- end
69
+ state[:output] << state[:active].join
70
+ state[:styled] = true unless state[:active].empty?
71
+ state[:started] = true
72
+ end
74
73
 
75
- def self.terminate_slice(state)
76
- return state[:output] if !state[:styled] || state[:output].empty?
74
+ def self.terminate_slice(state)
75
+ return state[:output] if !state[:styled] || state[:output].empty?
77
76
 
78
- "#{state[:output]}\e[0m"
79
- end
77
+ "#{state[:output]}\e[0m"
78
+ end
80
79
 
81
- def self.update_active_styles(active, token)
82
- if token.include?("[0m")
83
- active.clear
84
- else
85
- active << token
86
- end
80
+ def self.update_active_styles(active, token)
81
+ if token.include?("[0m")
82
+ active.clear
83
+ else
84
+ active << token
87
85
  end
88
-
89
- private_class_method :each_ansi_or_char, :slice_ansi_token, :slice_char,
90
- :start_slice, :terminate_slice, :update_active_styles
91
86
  end
87
+
88
+ private_class_method :each_ansi_or_char, :slice_ansi_token, :slice_char,
89
+ :start_slice, :terminate_slice, :update_active_styles
92
90
  end
93
91
  end
94
92
  end
@@ -1,35 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module UI
6
- class Border
7
- attr_reader :top_left, :top_right, :bottom_left, :bottom_right, :horizontal, :vertical
4
+ module UI
5
+ class Border
6
+ attr_reader :top_left, :top_right, :bottom_left, :bottom_right, :horizontal, :vertical
8
7
 
9
- def initialize(corners:, edges:)
10
- @top_left, @top_right, @bottom_left, @bottom_right = corners
11
- @horizontal, @vertical = edges
12
- end
13
-
14
- def self.fetch(name)
15
- STYLES.fetch(name.to_sym)
16
- end
8
+ def initialize(corners:, edges:)
9
+ @top_left, @top_right, @bottom_left, @bottom_right = corners
10
+ @horizontal, @vertical = edges
17
11
  end
18
12
 
19
- Border::STYLES = {
20
- normal: Border.new(
21
- corners: ["+", "+", "+", "+"], edges: ["-", "|"]
22
- ),
23
- rounded: Border.new(
24
- corners: ["╭", "╮", "╰", "╯"], edges: ["─", "│"]
25
- ),
26
- thick: Border.new(
27
- corners: ["┏", "┓", "┗", "┛"], edges: ["━", "┃"]
28
- ),
29
- double: Border.new(
30
- corners: ["╔", "╗", "╚", "╝"], edges: ["═", "║"]
31
- )
32
- }.freeze
13
+ def self.fetch(name)
14
+ STYLES.fetch(name.to_sym)
15
+ end
33
16
  end
17
+
18
+ Border::STYLES = {
19
+ normal: Border.new(
20
+ corners: ["+", "+", "+", "+"], edges: ["-", "|"]
21
+ ),
22
+ rounded: Border.new(
23
+ corners: ["╭", "╮", "╰", "╯"], edges: ["─", "│"]
24
+ ),
25
+ thick: Border.new(
26
+ corners: ["┏", "┓", "┗", "┛"], edges: ["━", "┃"]
27
+ ),
28
+ double: Border.new(
29
+ corners: ["╔", "╗", "╚", "╝"], edges: ["═", "║"]
30
+ )
31
+ }.freeze
34
32
  end
35
33
  end
@@ -1,57 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module UI
6
- class BorderPainter
7
- DEFAULT_SIDES = %i[top right bottom left].freeze
8
-
9
- def initialize(border:, sides: nil, foreground: nil, background: nil)
10
- @border = border
11
- @sides = Array(sides || DEFAULT_SIDES).map(&:to_sym)
12
- @foreground = foreground
13
- @background = background
14
- end
4
+ module UI
5
+ class BorderPainter
6
+ DEFAULT_SIDES = %i[top right bottom left].freeze
7
+
8
+ def initialize(border:, sides: nil, foreground: nil, background: nil)
9
+ @border = border
10
+ @sides = Array(sides || DEFAULT_SIDES).map(&:to_sym)
11
+ @foreground = foreground
12
+ @background = background
13
+ end
15
14
 
16
- def paint(lines, inner_width)
17
- horizontal = @border.horizontal * inner_width
18
- body = lines.map { |line| border_line(line, inner_width) }
15
+ def paint(lines, inner_width)
16
+ horizontal = @border.horizontal * inner_width
17
+ body = lines.map { |line| border_line(line, inner_width) }
19
18
 
20
- [top_border(horizontal), *body, bottom_border(horizontal)].compact
21
- end
19
+ [top_border(horizontal), *body, bottom_border(horizontal)].compact
20
+ end
22
21
 
23
- private
22
+ private
24
23
 
25
- def border_line(line, width)
26
- left = @sides.include?(:left) ? render_border(@border.vertical) : ""
27
- right = @sides.include?(:right) ? render_border(@border.vertical) : ""
24
+ def border_line(line, width)
25
+ left = @sides.include?(:left) ? render_border(@border.vertical) : ""
26
+ right = @sides.include?(:right) ? render_border(@border.vertical) : ""
28
27
 
29
- "#{left}#{line}#{" " * (width - Width.measure(line))}#{right}"
30
- end
28
+ "#{left}#{line}#{" " * (width - Width.measure(line))}#{right}"
29
+ end
31
30
 
32
- def top_border(horizontal)
33
- return unless @sides.include?(:top)
34
- return render_border(horizontal) unless full_horizontal?
31
+ def top_border(horizontal)
32
+ return unless @sides.include?(:top)
33
+ return render_border(horizontal) unless full_horizontal?
35
34
 
36
- render_border("#{@border.top_left}#{horizontal}#{@border.top_right}")
37
- end
35
+ render_border("#{@border.top_left}#{horizontal}#{@border.top_right}")
36
+ end
38
37
 
39
- def bottom_border(horizontal)
40
- return unless @sides.include?(:bottom)
41
- return render_border(horizontal) unless full_horizontal?
38
+ def bottom_border(horizontal)
39
+ return unless @sides.include?(:bottom)
40
+ return render_border(horizontal) unless full_horizontal?
42
41
 
43
- render_border("#{@border.bottom_left}#{horizontal}#{@border.bottom_right}")
44
- end
42
+ render_border("#{@border.bottom_left}#{horizontal}#{@border.bottom_right}")
43
+ end
45
44
 
46
- def full_horizontal?
47
- @sides.include?(:left) && @sides.include?(:right)
48
- end
45
+ def full_horizontal?
46
+ @sides.include?(:left) && @sides.include?(:right)
47
+ end
49
48
 
50
- def render_border(value)
51
- return value unless @foreground
49
+ def render_border(value)
50
+ return value unless @foreground
52
51
 
53
- Style.new(foreground: @foreground, background: @background).render(value)
54
- end
52
+ Style.new(foreground: @foreground, background: @background).render(value)
55
53
  end
56
54
  end
57
55
  end