architext 0.2.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.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Architext
4
+ class SelectionParser
5
+ def initialize(paths)
6
+ @paths = paths
7
+ end
8
+
9
+ def parse(input)
10
+ normalized = input.to_s.strip.downcase
11
+ return @paths if %w[a all *].include?(normalized)
12
+ return [] if normalized.empty?
13
+
14
+ indexes = normalized.split(',').flat_map { |part| expand_part(part.strip) }
15
+ indexes.uniq.filter_map { |index| @paths[index - 1] }
16
+ end
17
+
18
+ private
19
+
20
+ def expand_part(part)
21
+ if part.match?(/\A\d+\z/)
22
+ [part.to_i]
23
+ elsif (match = part.match(/\A(\d+)\s*-\s*(\d+)\z/))
24
+ first = match[1].to_i
25
+ last = match[2].to_i
26
+ first <= last ? (first..last).to_a : (last..first).to_a
27
+ else
28
+ []
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Architext
6
+ class Settings
7
+ attr_reader :config_path
8
+
9
+ def initialize(config_path: default_config_path)
10
+ @config_path = config_path
11
+ end
12
+
13
+ def default_vault
14
+ return nil unless File.file?(@config_path)
15
+
16
+ value = File.read(@config_path).strip
17
+ value.empty? ? nil : value
18
+ rescue StandardError
19
+ nil
20
+ end
21
+
22
+ def default_vault=(vault)
23
+ value = vault.to_s.strip
24
+ raise ArgumentError, 'default vault cannot be blank' if value.empty?
25
+
26
+ FileUtils.mkdir_p(File.dirname(@config_path))
27
+ File.write(@config_path, "#{value}\n")
28
+ end
29
+
30
+ def clear_default_vault
31
+ File.delete(@config_path) if File.file?(@config_path)
32
+ end
33
+
34
+ private
35
+
36
+ def default_config_path
37
+ File.join(Dir.home, '.config', 'architext', 'default_vault')
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Architext
4
+ module Terminal
5
+ RESET = "\e[0m"
6
+ HIDE_CURSOR = "\e[?25l"
7
+ SHOW_CURSOR = "\e[?25h"
8
+ CLEAR = "\e[2J"
9
+ CLEAR_TO_END = "\e[J"
10
+ CLEAR_LINE = "\e[2K"
11
+ HOME = "\e[H"
12
+ ALT_SCREEN = "\e[?1049h"
13
+ MAIN_SCREEN = "\e[?1049l"
14
+
15
+ PALETTE = {
16
+ ink: "\e[38;2;221;231;239m",
17
+ white: "\e[38;2;255;255;255m",
18
+ dim: "\e[38;2;126;140;155m",
19
+ faint: "\e[38;2;83;96;112m",
20
+ cyan: "\e[38;2;91;220;255m",
21
+ blue: "\e[38;2;98;151;255m",
22
+ green: "\e[38;2;91;232;184m",
23
+ amber: "\e[38;2;245;196;94m",
24
+ red: "\e[38;2;255;104;112m",
25
+ violet: "\e[38;2;190;142;255m",
26
+ bold: "\e[1m",
27
+ inverse: "\e[7m"
28
+ }.freeze
29
+
30
+ TAGS = PALETTE.transform_keys(&:to_s).merge('/' => RESET).freeze
31
+
32
+ module_function
33
+
34
+ def enabled?(io = $stdout)
35
+ io.respond_to?(:tty?) && io.tty? && ENV['NO_COLOR'].nil? && !dumb_terminal?
36
+ end
37
+
38
+ def alt_screen_supported?(io = $stdout)
39
+ enabled?(io) && ENV['ARCHITEXT_NO_ALT_SCREEN'].nil?
40
+ end
41
+
42
+ def paint(text, *styles, enabled: true)
43
+ return text.to_s unless enabled
44
+
45
+ styles.map { |style| PALETTE.fetch(style) }.join + text.to_s + RESET
46
+ end
47
+
48
+ # Tiny pretext-style renderer: "[cyan]text[/]" keeps visual markup readable.
49
+ def render(markup, enabled: true)
50
+ return strip_markup(markup) unless enabled
51
+
52
+ markup.to_s.gsub(%r{\[(/|[a-z_]+)\]}) { TAGS.fetch(Regexp.last_match(1), Regexp.last_match(0)) }
53
+ end
54
+
55
+ def strip_markup(markup)
56
+ markup.to_s.gsub(%r{\[(/|[a-z_]+)\]}, '')
57
+ end
58
+
59
+ def visible_length(text)
60
+ display_width(strip_ansi(text))
61
+ end
62
+
63
+ def truncate(text, width)
64
+ plain = text.to_s
65
+ return '' if width <= 0
66
+ return plain if visible_length(plain) <= width
67
+
68
+ truncate_plain(strip_ansi(plain), width)
69
+ end
70
+
71
+ def strip_ansi(text)
72
+ text.to_s.gsub(/\e\[[0-9;?]*[A-Za-z]/, '')
73
+ end
74
+
75
+ def truncate_plain(text, width)
76
+ return '' if width <= 0
77
+ return text.to_s if display_width(text) <= width
78
+ return text.to_s.each_char.first.to_s if width <= 1
79
+
80
+ out = +''
81
+ used = 0
82
+ text.to_s.each_char do |char|
83
+ char_width = char_display_width(char)
84
+ break if used + char_width > width - 3
85
+
86
+ out << char
87
+ used += char_width
88
+ end
89
+ "#{out}..."
90
+ end
91
+
92
+ def display_width(text)
93
+ text.to_s.each_char.sum { |char| char_display_width(char) }
94
+ end
95
+
96
+ def char_display_width(char)
97
+ codepoint = char.ord
98
+ return 0 if codepoint < 32 || codepoint == 0x7F
99
+ return 0 if combining_mark?(codepoint)
100
+ return 2 if wide_codepoint?(codepoint)
101
+
102
+ 1
103
+ end
104
+
105
+ def combining_mark?(codepoint)
106
+ (0x0300..0x036F).cover?(codepoint) ||
107
+ (0x1AB0..0x1AFF).cover?(codepoint) ||
108
+ (0x1DC0..0x1DFF).cover?(codepoint) ||
109
+ (0x20D0..0x20FF).cover?(codepoint) ||
110
+ (0xFE00..0xFE0F).cover?(codepoint)
111
+ end
112
+
113
+ def wide_codepoint?(codepoint)
114
+ (0x1100..0x115F).cover?(codepoint) ||
115
+ (0x2329..0x232A).cover?(codepoint) ||
116
+ (0x2E80..0xA4CF).cover?(codepoint) ||
117
+ (0xAC00..0xD7A3).cover?(codepoint) ||
118
+ (0xF900..0xFAFF).cover?(codepoint) ||
119
+ (0xFE10..0xFE19).cover?(codepoint) ||
120
+ (0xFE30..0xFE6F).cover?(codepoint) ||
121
+ (0xFF00..0xFF60).cover?(codepoint) ||
122
+ (0xFFE0..0xFFE6).cover?(codepoint) ||
123
+ (0x1F000..0x1FAFF).cover?(codepoint)
124
+ end
125
+
126
+ def dumb_terminal?
127
+ ENV.fetch('TERM', '').downcase == 'dumb'
128
+ end
129
+ end
130
+ end