gum 0.2.0 → 0.3.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 (64) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +435 -16
  4. data/exe/gum +11 -0
  5. data/gum.gemspec +28 -32
  6. data/lib/gum/command.rb +159 -0
  7. data/lib/gum/commands/choose.rb +95 -0
  8. data/lib/gum/commands/confirm.rb +57 -0
  9. data/lib/gum/commands/file.rb +84 -0
  10. data/lib/gum/commands/filter.rb +119 -0
  11. data/lib/gum/commands/format.rb +74 -0
  12. data/lib/gum/commands/input.rb +68 -0
  13. data/lib/gum/commands/join.rb +41 -0
  14. data/lib/gum/commands/log.rb +98 -0
  15. data/lib/gum/commands/pager.rb +55 -0
  16. data/lib/gum/commands/spin.rb +167 -0
  17. data/lib/gum/commands/style.rb +93 -0
  18. data/lib/gum/commands/table.rb +93 -0
  19. data/lib/gum/commands/write.rb +84 -0
  20. data/lib/gum/upstream.rb +17 -0
  21. data/lib/gum/version.rb +5 -1
  22. data/lib/gum.rb +170 -10
  23. data/sig/gum/command.rbs +23 -0
  24. data/sig/gum/commands/choose.rbs +42 -0
  25. data/sig/gum/commands/confirm.rbs +30 -0
  26. data/sig/gum/commands/file.rbs +38 -0
  27. data/sig/gum/commands/filter.rbs +48 -0
  28. data/sig/gum/commands/format.rbs +47 -0
  29. data/sig/gum/commands/input.rbs +32 -0
  30. data/sig/gum/commands/join.rbs +24 -0
  31. data/sig/gum/commands/log.rbs +56 -0
  32. data/sig/gum/commands/pager.rbs +28 -0
  33. data/sig/gum/commands/spin.rbs +55 -0
  34. data/sig/gum/commands/style.rbs +44 -0
  35. data/sig/gum/commands/table.rbs +35 -0
  36. data/sig/gum/commands/write.rbs +35 -0
  37. data/sig/gum/upstream.rbs +9 -0
  38. data/sig/gum/version.rbs +5 -0
  39. data/sig/gum.rbs +79 -0
  40. metadata +49 -144
  41. data/.gitignore +0 -9
  42. data/.rspec +0 -2
  43. data/.travis.yml +0 -5
  44. data/Gemfile +0 -4
  45. data/Rakefile +0 -6
  46. data/bin/console +0 -14
  47. data/bin/setup +0 -8
  48. data/lib/gum/factory.rb +0 -33
  49. data/lib/gum/filter.rb +0 -28
  50. data/lib/gum/filters/exists.rb +0 -25
  51. data/lib/gum/filters/fuzzy.rb +0 -11
  52. data/lib/gum/filters/geo/bbox.rb +0 -70
  53. data/lib/gum/filters/geo/distance.rb +0 -26
  54. data/lib/gum/filters/geo/range.rb +0 -33
  55. data/lib/gum/filters/geo.rb +0 -10
  56. data/lib/gum/filters/prefix.rb +0 -15
  57. data/lib/gum/filters/range.rb +0 -22
  58. data/lib/gum/filters/regexp.rb +0 -11
  59. data/lib/gum/filters/term.rb +0 -15
  60. data/lib/gum/filters/terms.rb +0 -11
  61. data/lib/gum/filters/wildcard.rb +0 -15
  62. data/lib/gum/filters.rb +0 -35
  63. data/lib/gum/order.rb +0 -18
  64. data/lib/gum/search.rb +0 -83
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ require "English"
6
+ require "open3"
7
+
8
+ module Gum
9
+ module Command
10
+ def self.run(*, input: nil, interactive: true)
11
+ if input && interactive
12
+ run_interactive_with_input(*, input: input)
13
+ elsif input
14
+ run_non_interactive(*, input: input)
15
+ else
16
+ run_interactive(*)
17
+ end
18
+ end
19
+
20
+ def self.run_non_interactive(*args, input:)
21
+ stdout, stderr, status = Open3.capture3(Gum.executable, *args.map(&:to_s), stdin_data: input)
22
+
23
+ unless status.success?
24
+ return nil if status.exitstatus == 130 # User cancelled (Ctrl+C)
25
+
26
+ raise Error, "gum #{args.first} failed: #{stderr}" unless stderr.empty?
27
+ end
28
+
29
+ stdout.chomp
30
+ end
31
+
32
+ def self.run_interactive(*args)
33
+ tty = File.open("/dev/tty", "r+")
34
+
35
+ stdout, wait_thread = Open3.pipeline_r(
36
+ [Gum.executable, *args.map(&:to_s)],
37
+ in: tty,
38
+ err: tty
39
+ )
40
+
41
+ output = stdout.read.chomp
42
+ stdout.close
43
+ tty.close
44
+
45
+ status = wait_thread.last.value
46
+ return nil if status.exitstatus == 130 # User cancelled
47
+ return nil unless status.success?
48
+
49
+ output
50
+ rescue Errno::ENOENT, Errno::ENXIO, Errno::EIO
51
+ stdout, stderr, status = Open3.capture3(Gum.executable, *args.map(&:to_s))
52
+
53
+ unless status.success?
54
+ return nil if status.exitstatus == 130
55
+ raise Error, "gum #{args.first} failed: #{stderr}" unless stderr.empty?
56
+ end
57
+
58
+ stdout.chomp
59
+ end
60
+
61
+ def self.run_interactive_with_input(*args, input:)
62
+ tty = File.open("/dev/tty", "r+")
63
+ stdin_read, stdin_write = IO.pipe
64
+ stdout_read, stdout_write = IO.pipe
65
+
66
+ pid = Process.spawn(
67
+ Gum.executable, *args.map(&:to_s),
68
+ in: stdin_read,
69
+ out: stdout_write,
70
+ err: tty
71
+ )
72
+
73
+ stdin_read.close
74
+ stdout_write.close
75
+
76
+ stdin_write.write(input)
77
+ stdin_write.close
78
+
79
+ output = stdout_read.read.chomp
80
+ stdout_read.close
81
+
82
+ _, status = Process.wait2(pid)
83
+ tty.close
84
+
85
+ return nil if status.exitstatus == 130 # User cancelled
86
+ return nil unless status.success?
87
+
88
+ output
89
+ rescue Errno::ENOENT, Errno::ENXIO, Errno::EIO
90
+ run_non_interactive(*args, input: input)
91
+ end
92
+
93
+ def self.run_display_only(*args, input:)
94
+ IO.popen([Gum.executable, *args.map(&:to_s)], "w") do |io|
95
+ io.write(input)
96
+ end
97
+
98
+ $CHILD_STATUS.success? || nil
99
+ rescue Errno::ENOENT
100
+ raise Error, "gum executable not found"
101
+ end
102
+
103
+ def self.run_with_status(*args, input: nil)
104
+ if input
105
+ _stdout, _stderr, status = Open3.capture3(Gum.executable, *args.map(&:to_s), stdin_data: input)
106
+ status.success?
107
+ else
108
+ system(Gum.executable, *args.map(&:to_s))
109
+ end
110
+ end
111
+
112
+ def self.build_args(command, *positional, **options)
113
+ args = [command]
114
+ args.concat(positional.flatten.compact)
115
+
116
+ options.each do |key, value|
117
+ next if value.nil?
118
+
119
+ flag = key.to_s.tr("_", "-")
120
+
121
+ case value
122
+ when true
123
+ args << "--#{flag}"
124
+ when false
125
+ args << "--no-#{flag}" if flag_supports_negation?(command, flag)
126
+ when Array
127
+ value.each { |v| args << "--#{flag}=#{v}" }
128
+ when Hash
129
+ value.each { |k, v| args << "--#{flag}.#{k}=#{v}" }
130
+ else
131
+ args << "--#{flag}=#{value}"
132
+ end
133
+ end
134
+
135
+ args
136
+ end
137
+
138
+ def self.add_style_args(args, flag, style_hash)
139
+ return unless style_hash
140
+
141
+ style_hash.each do |key, value|
142
+ args << "--#{flag}.#{key}" << value.to_s
143
+ end
144
+ end
145
+
146
+ def self.flag_supports_negation?(command, flag)
147
+ negatable = {
148
+ "filter" => ["fuzzy", "sort", "strict", "reverse", "indicator"],
149
+ "choose" => ["limit"],
150
+ "input" => ["echo-mode"],
151
+ "file" => ["all", "file", "directory"],
152
+ "pager" => ["soft-wrap"],
153
+ "table" => ["border", "print"],
154
+ }
155
+
156
+ negatable.fetch(command, []).include?(flag)
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ module Gum
6
+ # Choose an option from a list of choices
7
+ #
8
+ # @example Single selection with array
9
+ # color = Gum.choose(%w[red green blue])
10
+ #
11
+ # @example Single selection with splat
12
+ # color = Gum.choose("red", "green", "blue")
13
+ #
14
+ # @example Multiple selection
15
+ # colors = Gum.choose(%w[red green blue], limit: 2)
16
+ #
17
+ # @example Unlimited selection
18
+ # colors = Gum.choose(%w[red green blue], no_limit: true)
19
+ #
20
+ # @example With header and custom height
21
+ # choice = Gum.choose(options, header: "Pick one:", height: 10)
22
+ #
23
+ class Choose
24
+ # Choose from a list of options
25
+ #
26
+ # @rbs *items: Array[String] | String -- list of choices to display (array or splat)
27
+ # @rbs limit: Integer? -- maximum number of items that can be selected (1 = single select)
28
+ # @rbs no_limit: bool -- allow unlimited selections (use tab/ctrl+space to select)
29
+ # @rbs ordered: bool? -- maintain selection order
30
+ # @rbs height: Integer? -- height of the list
31
+ # @rbs cursor: String? -- cursor character (default: ">")
32
+ # @rbs header: String? -- header text displayed above choices
33
+ # @rbs cursor_prefix: String? -- prefix for the cursor line
34
+ # @rbs selected_prefix: String? -- prefix for selected items (default: "✓")
35
+ # @rbs unselected_prefix: String? -- prefix for unselected items (default: "○")
36
+ # @rbs selected: Array[String]? -- items to pre-select
37
+ # @rbs timeout: Integer? -- timeout in seconds (0 = no timeout)
38
+ # @rbs cursor_style: Hash[Symbol, untyped]? -- cursor style options
39
+ # @rbs header_style: Hash[Symbol, untyped]? -- header text style
40
+ # @rbs item_style: Hash[Symbol, untyped]? -- item text style
41
+ # @rbs selected_style: Hash[Symbol, untyped]? -- selected item style
42
+ # @rbs return: String | Array[String] | nil -- selected item(s), or nil if cancelled
43
+ def self.call(
44
+ *items,
45
+ limit: nil,
46
+ no_limit: false,
47
+ ordered: nil,
48
+ height: nil,
49
+ cursor: nil,
50
+ header: nil,
51
+ cursor_prefix: nil,
52
+ selected_prefix: nil,
53
+ unselected_prefix: nil,
54
+ selected: nil,
55
+ timeout: nil,
56
+ cursor_style: nil,
57
+ header_style: nil,
58
+ item_style: nil,
59
+ selected_style: nil
60
+ )
61
+ items = items.flatten
62
+
63
+ options = {
64
+ limit: no_limit ? nil : limit,
65
+ "no-limit": no_limit || nil,
66
+ ordered: ordered,
67
+ height: height,
68
+ cursor: cursor,
69
+ header: header,
70
+ "cursor-prefix": cursor_prefix,
71
+ "selected-prefix": selected_prefix,
72
+ "unselected-prefix": unselected_prefix,
73
+ timeout: timeout ? "#{timeout}s" : nil,
74
+ item: item_style,
75
+ selected: selected,
76
+ }
77
+
78
+ args = Command.build_args("choose", *items, **options.compact)
79
+
80
+ Command.add_style_args(args, :cursor, cursor_style)
81
+ Command.add_style_args(args, :header, header_style)
82
+ Command.add_style_args(args, :selected, selected_style)
83
+
84
+ result = Command.run(*args)
85
+
86
+ return nil if result.nil?
87
+
88
+ if no_limit || (limit && limit > 1)
89
+ result.split("\n")
90
+ else
91
+ result
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ module Gum
6
+ # Ask for user confirmation
7
+ #
8
+ # @example Basic confirmation
9
+ # if Gum.confirm("Delete file?")
10
+ # File.delete(path)
11
+ # end
12
+ #
13
+ # @example With default value
14
+ # proceed = Gum.confirm("Continue?", default: true)
15
+ #
16
+ # @example With custom button labels
17
+ # Gum.confirm("Save changes?", affirmative: "Save", negative: "Discard")
18
+ #
19
+ class Confirm
20
+ # Prompt for yes/no confirmation
21
+ #
22
+ # @rbs prompt: String -- the confirmation prompt
23
+ # @rbs default: bool? -- default value (true = yes, false = no)
24
+ # @rbs affirmative: String? -- text for affirmative button (default: "Yes")
25
+ # @rbs negative: String? -- text for negative button (default: "No")
26
+ # @rbs timeout: Integer? -- timeout in seconds (0 = no timeout)
27
+ # @rbs prompt_style: Hash[Symbol, untyped]? -- prompt text style options
28
+ # @rbs selected_style: Hash[Symbol, untyped]? -- selected button style options
29
+ # @rbs unselected_style: Hash[Symbol, untyped]? -- unselected button style options
30
+ # @rbs return: bool -- true if confirmed, false if rejected
31
+ def self.call(
32
+ prompt = "Are you sure?",
33
+ default: nil,
34
+ affirmative: nil,
35
+ negative: nil,
36
+ timeout: nil,
37
+ prompt_style: nil,
38
+ selected_style: nil,
39
+ unselected_style: nil
40
+ )
41
+ options = {
42
+ default: default.nil? ? nil : default,
43
+ affirmative: affirmative,
44
+ negative: negative,
45
+ timeout: timeout ? "#{timeout}s" : nil,
46
+ }
47
+
48
+ args = Command.build_args("confirm", prompt, **options.compact)
49
+
50
+ Command.add_style_args(args, :prompt, prompt_style)
51
+ Command.add_style_args(args, :selected, selected_style)
52
+ Command.add_style_args(args, :unselected, unselected_style)
53
+
54
+ Command.run_with_status(*args)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ module Gum
6
+ # Pick a file from a folder
7
+ #
8
+ # @example Basic file picker
9
+ # path = Gum.file
10
+ #
11
+ # @example Start from specific directory
12
+ # path = Gum.file(directory: "~/Documents")
13
+ #
14
+ # @example Show hidden files
15
+ # path = Gum.file(all: true)
16
+ #
17
+ # @example Only show directories
18
+ # dir = Gum.file(directory_only: true)
19
+ #
20
+ class FilePicker
21
+ # Pick a file from the filesystem
22
+ #
23
+ # @rbs path: String? -- starting directory path
24
+ # @rbs cursor: String? -- cursor character
25
+ # @rbs all: bool? -- show hidden files and directories
26
+ # @rbs file: bool? -- allow file selection (default: true)
27
+ # @rbs directory: bool? -- allow directory selection (default: false)
28
+ # @rbs directory_only: bool -- only allow directory selection (convenience option)
29
+ # @rbs height: Integer? -- height of the file picker
30
+ # @rbs timeout: Integer? -- timeout in seconds (0 = no timeout)
31
+ # @rbs cursor_style: Hash[Symbol, untyped]? -- cursor style options
32
+ # @rbs symlink_style: Hash[Symbol, untyped]? -- symlink text style
33
+ # @rbs directory_style: Hash[Symbol, untyped]? -- directory text style
34
+ # @rbs file_style: Hash[Symbol, untyped]? -- file text style
35
+ # @rbs permissions_style: Hash[Symbol, untyped]? -- permissions text style
36
+ # @rbs selected_style: Hash[Symbol, untyped]? -- selected item style
37
+ # @rbs file_size_style: Hash[Symbol, untyped]? -- file size text style
38
+ # @rbs return: String? -- selected file path, or nil if cancelled
39
+ def self.call(
40
+ path = nil,
41
+ cursor: nil,
42
+ all: nil,
43
+ file: nil,
44
+ directory: nil,
45
+ directory_only: false,
46
+ height: nil,
47
+ timeout: nil,
48
+ cursor_style: nil,
49
+ symlink_style: nil,
50
+ directory_style: nil,
51
+ file_style: nil,
52
+ permissions_style: nil,
53
+ selected_style: nil,
54
+ file_size_style: nil
55
+ )
56
+ if directory_only
57
+ file = false
58
+ directory = true
59
+ end
60
+
61
+ options = {
62
+ cursor: cursor,
63
+ all: all,
64
+ file: file,
65
+ directory: directory,
66
+ height: height,
67
+ timeout: timeout ? "#{timeout}s" : nil,
68
+ }
69
+
70
+ positional = path ? [path] : [] #: Array[String]
71
+ args = Command.build_args("file", *positional, **options.compact)
72
+
73
+ Command.add_style_args(args, :cursor, cursor_style)
74
+ Command.add_style_args(args, :symlink, symlink_style)
75
+ Command.add_style_args(args, :directory, directory_style)
76
+ Command.add_style_args(args, :file, file_style)
77
+ Command.add_style_args(args, :permissions, permissions_style)
78
+ Command.add_style_args(args, :selected, selected_style)
79
+ Command.add_style_args(args, :"file-size", file_size_style)
80
+
81
+ Command.run(*args)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ module Gum
6
+ # Filter items from a list with fuzzy matching
7
+ #
8
+ # @example Single selection filter with array
9
+ # file = Gum.filter(Dir.glob("*"))
10
+ #
11
+ # @example Single selection filter with splat
12
+ # file = Gum.filter("file1.rb", "file2.rb", "file3.rb")
13
+ #
14
+ # @example Multiple selection filter
15
+ # files = Gum.filter(Dir.glob("*"), limit: 5)
16
+ #
17
+ # @example With placeholder and height
18
+ # selection = Gum.filter(items, placeholder: "Search...", height: 20)
19
+ #
20
+ class Filter
21
+ # Filter a list with fuzzy matching
22
+ #
23
+ # @rbs *items: Array[String] | String -- list of items to filter (array or splat)
24
+ # @rbs limit: Integer? -- maximum number of items that can be selected
25
+ # @rbs no_limit: bool -- allow unlimited selections (use tab/ctrl+space to select)
26
+ # @rbs height: Integer? -- height of the list
27
+ # @rbs width: Integer? -- width of the filter input
28
+ # @rbs placeholder: String? -- placeholder text for the filter input
29
+ # @rbs prompt: String? -- prompt string shown before input
30
+ # @rbs value: String? -- initial filter value
31
+ # @rbs header: String? -- header text displayed above the list
32
+ # @rbs indicator: String? -- character for selected item indicator
33
+ # @rbs selected_prefix: String? -- prefix for selected items
34
+ # @rbs unselected_prefix: String? -- prefix for unselected items
35
+ # @rbs match_prefix: String? -- prefix for matched text
36
+ # @rbs fuzzy: bool? -- enable fuzzy matching (default: true)
37
+ # @rbs sort: bool? -- sort results by match score (default: true)
38
+ # @rbs strict: bool? -- require exact match
39
+ # @rbs reverse: bool? -- reverse the order of results
40
+ # @rbs timeout: Integer? -- timeout in seconds (0 = no timeout)
41
+ # @rbs header_style: Hash[Symbol, untyped]? -- header text style
42
+ # @rbs text_style: Hash[Symbol, untyped]? -- text style for items
43
+ # @rbs cursor_text_style: Hash[Symbol, untyped]? -- style for cursor line text
44
+ # @rbs match_style: Hash[Symbol, untyped]? -- style for matched characters
45
+ # @rbs placeholder_style: Hash[Symbol, untyped]? -- placeholder text style
46
+ # @rbs prompt_style: Hash[Symbol, untyped]? -- prompt text style
47
+ # @rbs indicator_style: Hash[Symbol, untyped]? -- indicator character style
48
+ # @rbs return: String | Array[String] | nil -- selected item(s), or nil if cancelled
49
+ def self.call(
50
+ *items,
51
+ limit: nil,
52
+ no_limit: false,
53
+ height: nil,
54
+ width: nil,
55
+ placeholder: nil,
56
+ prompt: nil,
57
+ value: nil,
58
+ header: nil,
59
+ indicator: nil,
60
+ selected_prefix: nil,
61
+ unselected_prefix: nil,
62
+ match_prefix: nil,
63
+ fuzzy: nil,
64
+ sort: nil,
65
+ strict: nil,
66
+ reverse: nil,
67
+ timeout: nil,
68
+ header_style: nil,
69
+ text_style: nil,
70
+ cursor_text_style: nil,
71
+ match_style: nil,
72
+ placeholder_style: nil,
73
+ prompt_style: nil,
74
+ indicator_style: nil
75
+ )
76
+ items = items.flatten
77
+
78
+ options = {
79
+ limit: no_limit ? 0 : limit,
80
+ height: height,
81
+ width: width,
82
+ placeholder: placeholder,
83
+ prompt: prompt,
84
+ value: value,
85
+ header: header,
86
+ indicator: indicator,
87
+ selected_prefix: selected_prefix,
88
+ unselected_prefix: unselected_prefix,
89
+ match_prefix: match_prefix,
90
+ fuzzy: fuzzy,
91
+ sort: sort,
92
+ strict: strict,
93
+ reverse: reverse,
94
+ timeout: timeout ? "#{timeout}s" : nil,
95
+ text: text_style,
96
+ cursor_text: cursor_text_style,
97
+ match: match_style,
98
+ }
99
+
100
+ args = Command.build_args("filter", **options.compact)
101
+
102
+ Command.add_style_args(args, :header, header_style)
103
+ Command.add_style_args(args, :placeholder, placeholder_style)
104
+ Command.add_style_args(args, :prompt, prompt_style)
105
+ Command.add_style_args(args, :indicator, indicator_style)
106
+
107
+ input_data = items.join("\n")
108
+ result = Command.run(*args, input: input_data)
109
+
110
+ return nil if result.nil?
111
+
112
+ if no_limit || (limit && limit > 1)
113
+ result.split("\n")
114
+ else
115
+ result
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ module Gum
6
+ # Format text with markdown, code highlighting, templates, or emojis
7
+ #
8
+ # @example Markdown formatting
9
+ # Gum.format("# Hello\n- Item 1\n- Item 2")
10
+ #
11
+ # @example Code highlighting
12
+ # Gum.format(code, type: :code, language: "ruby")
13
+ #
14
+ # @example Template formatting
15
+ # Gum.format('{{ Bold "Hello" }} {{ Color "99" "0" " World " }}', type: :template)
16
+ #
17
+ # @example Emoji formatting
18
+ # Gum.format("I :heart: Ruby :gem:", type: :emoji)
19
+ #
20
+ class Format
21
+ TYPES = [:markdown, :code, :template, :emoji].freeze #: Array[Symbol]
22
+
23
+ # Format and render text
24
+ #
25
+ # @rbs *text: String -- text content to format (multiple strings joined with newlines)
26
+ # @rbs type: Symbol | String | nil -- format type (:markdown, :code, :template, :emoji)
27
+ # @rbs language: String? -- programming language for code highlighting
28
+ # @rbs theme: String? -- syntax highlighting theme
29
+ # @rbs return: String? -- formatted text output
30
+ def self.call(*text, type: nil, language: nil, theme: nil)
31
+ options = {
32
+ type: type&.to_s,
33
+ language: language,
34
+ theme: theme,
35
+ }
36
+
37
+ content = text.join("\n")
38
+
39
+ args = Command.build_args("format", **options.compact)
40
+
41
+ if content.empty?
42
+ Command.run(*args, interactive: false)
43
+ else
44
+ Command.run(*args, input: content, interactive: false)
45
+ end
46
+ end
47
+
48
+ # @rbs text: String -- markdown text to format
49
+ # @rbs return: String? -- rendered markdown output
50
+ def self.markdown(text)
51
+ call(text, type: :markdown)
52
+ end
53
+
54
+ # @rbs text: String -- source code to highlight
55
+ # @rbs language: String? -- programming language for highlighting
56
+ # @rbs theme: String? -- syntax highlighting theme
57
+ # @rbs return: String? -- syntax-highlighted code output
58
+ def self.code(text, language: nil, theme: nil)
59
+ call(text, type: :code, language: language, theme: theme)
60
+ end
61
+
62
+ # @rbs text: String -- template string with Termenv helpers
63
+ # @rbs return: String? -- rendered template output
64
+ def self.template(text)
65
+ call(text, type: :template)
66
+ end
67
+
68
+ # @rbs text: String -- text with :emoji_name: codes
69
+ # @rbs return: String? -- text with emojis rendered
70
+ def self.emoji(text)
71
+ call(text, type: :emoji)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ module Gum
6
+ # Prompt for single-line input
7
+ #
8
+ # @example Basic input
9
+ # name = Gum.input(placeholder: "Enter your name")
10
+ #
11
+ # @example Password input
12
+ # password = Gum.input(password: true)
13
+ #
14
+ # @example With default value and custom prompt
15
+ # email = Gum.input(value: "user@", prompt: "> ", placeholder: "email")
16
+ #
17
+ class Input
18
+ # Prompt for single-line input
19
+ #
20
+ # @rbs placeholder: String? -- placeholder text shown when input is empty
21
+ # @rbs prompt: String? -- prompt string shown before input
22
+ # @rbs value: String? -- initial value for the input
23
+ # @rbs char_limit: Integer? -- maximum number of characters allowed
24
+ # @rbs width: Integer? -- width of the input field
25
+ # @rbs password: bool -- mask input characters for password entry
26
+ # @rbs header: String? -- header text displayed above the input
27
+ # @rbs timeout: Integer? -- timeout in seconds (0 = no timeout)
28
+ # @rbs cursor: Hash[Symbol, untyped]? -- cursor style options (foreground, background)
29
+ # @rbs prompt_style: Hash[Symbol, untyped]? -- prompt text style options
30
+ # @rbs placeholder_style: Hash[Symbol, untyped]? -- placeholder text style options
31
+ # @rbs header_style: Hash[Symbol, untyped]? -- header text style options
32
+ # @rbs return: String? -- the entered text, or nil if cancelled
33
+ def self.call(
34
+ placeholder: nil,
35
+ prompt: nil,
36
+ value: nil,
37
+ char_limit: nil,
38
+ width: nil,
39
+ password: false,
40
+ header: nil,
41
+ timeout: nil,
42
+ cursor: nil,
43
+ prompt_style: nil,
44
+ placeholder_style: nil,
45
+ header_style: nil
46
+ )
47
+ options = {
48
+ placeholder: placeholder,
49
+ prompt: prompt,
50
+ value: value,
51
+ char_limit: char_limit,
52
+ width: width,
53
+ password: password || nil,
54
+ header: header,
55
+ timeout: timeout ? "#{timeout}s" : nil,
56
+ cursor: cursor,
57
+ }
58
+
59
+ args = Command.build_args("input", **options.compact)
60
+
61
+ Command.add_style_args(args, :prompt, prompt_style)
62
+ Command.add_style_args(args, :placeholder, placeholder_style)
63
+ Command.add_style_args(args, :header, header_style)
64
+
65
+ Command.run(*args)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+ # rbs_inline: enabled
4
+
5
+ module Gum
6
+ # Join text blocks horizontally or vertically
7
+ #
8
+ # @example Horizontal join (default)
9
+ # combined = Gum.join(box1, box2)
10
+ #
11
+ # @example Vertical join
12
+ # stacked = Gum.join(box1, box2, vertical: true)
13
+ #
14
+ # @example With alignment
15
+ # aligned = Gum.join(box1, box2, vertical: true, align: :center)
16
+ #
17
+ class Join
18
+ # Join text blocks together
19
+ #
20
+ # @rbs *texts: String -- text blocks to join (usually styled with Gum.style)
21
+ # @rbs vertical: bool -- stack blocks vertically (default: false, horizontal)
22
+ # @rbs horizontal: bool -- place blocks side by side (default)
23
+ # @rbs align: Symbol | String | nil -- alignment (:left, :center, :right for vertical; :top, :middle, :bottom for horizontal)
24
+ # @rbs return: String? -- combined text output
25
+ def self.call(*texts, vertical: false, horizontal: false, align: nil)
26
+ options = {} #: Hash[Symbol, untyped]
27
+
28
+ if vertical
29
+ options[:vertical] = true
30
+ elsif horizontal
31
+ options[:horizontal] = true
32
+ end
33
+
34
+ options[:align] = align.to_s if align
35
+
36
+ args = Command.build_args("join", *texts, **options.compact)
37
+
38
+ Command.run(*args, interactive: false)
39
+ end
40
+ end
41
+ end