samovar 2.4.1 → 2.5.1
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/completion.md +206 -0
- data/context/index.yaml +4 -0
- data/lib/samovar/command.rb +24 -9
- data/lib/samovar/completion/context.rb +111 -0
- data/lib/samovar/completion/provider.rb +75 -0
- data/lib/samovar/completion/result.rb +54 -0
- data/lib/samovar/completion/suggestion.rb +94 -0
- data/lib/samovar/completion.rb +24 -0
- data/lib/samovar/failure.rb +1 -1
- data/lib/samovar/flags.rb +32 -0
- data/lib/samovar/many.rb +33 -2
- data/lib/samovar/nested.rb +46 -3
- data/lib/samovar/one.rb +28 -2
- data/lib/samovar/option.rb +63 -5
- data/lib/samovar/options.rb +90 -6
- data/lib/samovar/output/columns.rb +1 -1
- data/lib/samovar/output/header.rb +1 -1
- data/lib/samovar/output/usage_formatter.rb +41 -35
- data/lib/samovar/split.rb +45 -1
- data/lib/samovar/version.rb +1 -1
- data/license.md +2 -1
- data/readme.md +30 -8
- data/releases.md +11 -1
- data.tar.gz.sig +0 -0
- metadata +10 -17
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8bdded07aeaa6cc737bf9c08155dcd2c954521f8cd066dbeb6786c5ba325b02
|
|
4
|
+
data.tar.gz: a469ff20b8426b570700cb6cc2daaba39e689aa12f8df58ed9701a516f2be4f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e48219e31bda0451b3c59752077f470e55794621f2f1b011d60b73caae7bf0695ba7c4a90c7f956e02ff066e5ca12102e571fcc543632da64d1edaa1a69b1a44
|
|
7
|
+
data.tar.gz: 230b563dd99c38945c8ea041ed399eaea8f245bf6476295ec097cebdd818c5f1cd66473603a37f51c741a0bdb3a991f94595e145a0714ed7f3715e92b5c131b2
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Completion
|
|
2
|
+
|
|
3
|
+
This guide explains how to add shell completion to commands built with `samovar`.
|
|
4
|
+
|
|
5
|
+
Samovar can complete command lines using the same grammar used for parsing. It can complete option flags, boolean flag variants, nested command names, option values, positional arguments, and split arguments.
|
|
6
|
+
|
|
7
|
+
## Command Entry Point
|
|
8
|
+
|
|
9
|
+
Commands expose a completion entry point alongside the normal execution entry point:
|
|
10
|
+
|
|
11
|
+
~~~ ruby
|
|
12
|
+
Application.call(ARGV) # Parse and execute the command.
|
|
13
|
+
Application.complete # Print completion candidates.
|
|
14
|
+
~~~
|
|
15
|
+
|
|
16
|
+
`complete` expects the command-line arguments to be truncated to the cursor. The final argument is the token being completed. When completing after a space, pass an empty string as the final argument:
|
|
17
|
+
|
|
18
|
+
~~~ ruby
|
|
19
|
+
Application.complete(["serve", "--bind", ""])
|
|
20
|
+
~~~
|
|
21
|
+
|
|
22
|
+
Completion candidates are printed as tab-separated values:
|
|
23
|
+
|
|
24
|
+
~~~ text
|
|
25
|
+
type value description key=value
|
|
26
|
+
~~~
|
|
27
|
+
|
|
28
|
+
The first three fields are always the completion type, value, and description. Additional fields are optional metadata entries encoded as `key=value`.
|
|
29
|
+
|
|
30
|
+
## Static Completions
|
|
31
|
+
|
|
32
|
+
Option flags and nested command names are completed automatically. You can add static completions for option values and positional arguments with `completions:`.
|
|
33
|
+
|
|
34
|
+
~~~ ruby
|
|
35
|
+
require "samovar"
|
|
36
|
+
|
|
37
|
+
class Serve < Samovar::Command
|
|
38
|
+
self.description = "Run the server."
|
|
39
|
+
|
|
40
|
+
options do
|
|
41
|
+
option "--format <name>", "The output format.", default: "text", completions: ["json", "text", "yaml"]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Application < Samovar::Command
|
|
46
|
+
options do
|
|
47
|
+
option "-h/--help", "Print help."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
nested :command, {
|
|
51
|
+
"serve" => Serve
|
|
52
|
+
}, default: "serve"
|
|
53
|
+
end
|
|
54
|
+
~~~
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
|
|
58
|
+
~~~ ruby
|
|
59
|
+
Application.complete(["ser"])
|
|
60
|
+
# command serve Run the server.
|
|
61
|
+
|
|
62
|
+
Application.complete(["serve", "--format", "j"])
|
|
63
|
+
# value json
|
|
64
|
+
~~~
|
|
65
|
+
|
|
66
|
+
If an option has a default value, the default is offered before other value completions.
|
|
67
|
+
|
|
68
|
+
## Dynamic Completions
|
|
69
|
+
|
|
70
|
+
Use a callable provider when completions depend on runtime state.
|
|
71
|
+
|
|
72
|
+
~~~ ruby
|
|
73
|
+
class Serve < Samovar::Command
|
|
74
|
+
def self.host_completions(context)
|
|
75
|
+
["localhost", "0.0.0.0"].select do |host|
|
|
76
|
+
host.start_with?(context.current)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
options do
|
|
81
|
+
option "--bind <host>", "The bind address.", completions: method(:host_completions)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
~~~
|
|
85
|
+
|
|
86
|
+
The provider receives a `Samovar::Completion::Context` with:
|
|
87
|
+
|
|
88
|
+
- `current`: The token being completed.
|
|
89
|
+
- `arguments`: The full truncated argument list.
|
|
90
|
+
- `environment`: The environment hash passed to `complete`.
|
|
91
|
+
- `row`: The parser row whose value is being completed. This can be an option or a positional argument.
|
|
92
|
+
|
|
93
|
+
Providers can return strings, hashes, or `Samovar::Completion::Suggestion` instances:
|
|
94
|
+
|
|
95
|
+
~~~ ruby
|
|
96
|
+
option "--mode <name>", "The mode.",
|
|
97
|
+
completions: [
|
|
98
|
+
{value: "development", description: "Local development", type: :value, suffix: " "},
|
|
99
|
+
{value: "production", description: "Production", type: :value}
|
|
100
|
+
]
|
|
101
|
+
~~~
|
|
102
|
+
|
|
103
|
+
## Path Completion
|
|
104
|
+
|
|
105
|
+
For path-like arguments, let the shell do native path expansion by using one of the native completion providers:
|
|
106
|
+
|
|
107
|
+
~~~ ruby
|
|
108
|
+
class Process < Samovar::Command
|
|
109
|
+
options do
|
|
110
|
+
option "--output <path>", "The output path.", completions: :path
|
|
111
|
+
option "--root <path>", "The root directory.", completions: :directory
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
one :input, "The input path.", completions: :file
|
|
115
|
+
end
|
|
116
|
+
~~~
|
|
117
|
+
|
|
118
|
+
Supported native providers:
|
|
119
|
+
|
|
120
|
+
- `:path`: Complete files and directories using the shell.
|
|
121
|
+
- `:file`: Alias for `:path`.
|
|
122
|
+
- `:directory`: Complete directories using the shell.
|
|
123
|
+
- `:executable`: Complete executable commands using the shell.
|
|
124
|
+
|
|
125
|
+
Samovar does not inspect the filesystem for these providers. It emits a typed completion request, and the shell adapter translates it to native shell path completion.
|
|
126
|
+
|
|
127
|
+
For split arguments, `:executable` completes the command immediately after the split marker. Once a command is present, Samovar emits a `delegate` completion with an `index` metadata field. The index is the zero-based argument index where delegated completion begins.
|
|
128
|
+
|
|
129
|
+
## Dedicated Completion Executable
|
|
130
|
+
|
|
131
|
+
Shell adapters call a dedicated completion executable named `completion-<command>`. This avoids running the normal command during completion.
|
|
132
|
+
|
|
133
|
+
For a command named `falcon`, provide:
|
|
134
|
+
|
|
135
|
+
~~~ text
|
|
136
|
+
bin/falcon
|
|
137
|
+
bin/completion-falcon
|
|
138
|
+
~~~
|
|
139
|
+
|
|
140
|
+
The completion executable can be very small:
|
|
141
|
+
|
|
142
|
+
~~~ ruby
|
|
143
|
+
#!/usr/bin/env ruby
|
|
144
|
+
# frozen_string_literal: true
|
|
145
|
+
|
|
146
|
+
require_relative "../lib/my/application"
|
|
147
|
+
|
|
148
|
+
My::Application.complete
|
|
149
|
+
~~~
|
|
150
|
+
|
|
151
|
+
When the user completes a command by path, the shell adapter resolves the completion executable next to that command:
|
|
152
|
+
|
|
153
|
+
~~~ text
|
|
154
|
+
falcon -> completion-falcon
|
|
155
|
+
bin/falcon -> bin/completion-falcon
|
|
156
|
+
./bin/falcon -> ./bin/completion-falcon
|
|
157
|
+
/path/falcon -> /path/completion-falcon
|
|
158
|
+
~~~
|
|
159
|
+
|
|
160
|
+
## Installing Shell Adapters
|
|
161
|
+
|
|
162
|
+
Shell adapter generation and installation is provided by the `completion` gem.
|
|
163
|
+
|
|
164
|
+
Generate an adapter script:
|
|
165
|
+
|
|
166
|
+
~~~ bash
|
|
167
|
+
$ completion generate --shell zsh --command falcon
|
|
168
|
+
~~~
|
|
169
|
+
|
|
170
|
+
Install a generic adapter script into the default directory for the current shell:
|
|
171
|
+
|
|
172
|
+
~~~ bash
|
|
173
|
+
$ completion install
|
|
174
|
+
~~~
|
|
175
|
+
|
|
176
|
+
The generic adapter checks whether a matching `completion-<command>` executable exists before handling a command. You can install an adapter for a specific command instead:
|
|
177
|
+
|
|
178
|
+
~~~ bash
|
|
179
|
+
$ completion install --command falcon
|
|
180
|
+
~~~
|
|
181
|
+
|
|
182
|
+
You can specify the shell and directory explicitly:
|
|
183
|
+
|
|
184
|
+
~~~ bash
|
|
185
|
+
$ completion install --shell fish --directory ~/.config/fish/completions --command falcon
|
|
186
|
+
~~~
|
|
187
|
+
|
|
188
|
+
The installed adapter calls the matching `completion-<command>` executable when completion is requested.
|
|
189
|
+
|
|
190
|
+
## Testing Completion
|
|
191
|
+
|
|
192
|
+
You can test completion directly without involving a shell:
|
|
193
|
+
|
|
194
|
+
~~~ ruby
|
|
195
|
+
output = StringIO.new
|
|
196
|
+
|
|
197
|
+
Application.complete(["serve", "--format", "j"], output: output)
|
|
198
|
+
|
|
199
|
+
expect(output.string).to be == "value\tjson\t\n"
|
|
200
|
+
~~~
|
|
201
|
+
|
|
202
|
+
For a trailing-space completion, pass an empty final token:
|
|
203
|
+
|
|
204
|
+
~~~ ruby
|
|
205
|
+
Application.complete(["serve", "--format", ""], output: output)
|
|
206
|
+
~~~
|
data/context/index.yaml
CHANGED
|
@@ -12,3 +12,7 @@ files:
|
|
|
12
12
|
title: Getting Started
|
|
13
13
|
description: This guide explains how to use `samovar` to build command-line tools
|
|
14
14
|
and applications.
|
|
15
|
+
- path: completion.md
|
|
16
|
+
title: Completion
|
|
17
|
+
description: This guide explains how to add shell completion to commands built with
|
|
18
|
+
`samovar`.
|
data/lib/samovar/command.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "split"
|
|
|
14
14
|
require_relative "output"
|
|
15
15
|
|
|
16
16
|
require_relative "error"
|
|
17
|
+
require_relative "completion"
|
|
17
18
|
|
|
18
19
|
module Samovar
|
|
19
20
|
# Represents a command in the command-line interface.
|
|
@@ -23,12 +24,13 @@ module Samovar
|
|
|
23
24
|
# Parse and execute the command with the given input.
|
|
24
25
|
#
|
|
25
26
|
# This is the high-level entry point for CLI applications. It handles errors gracefully by printing usage and returning nil.
|
|
27
|
+
# The given arguments are passed to the parser, which consumes them as mutable input.
|
|
26
28
|
#
|
|
27
|
-
# @parameter
|
|
29
|
+
# @parameter arguments [Array(String)] The command-line arguments to parse.
|
|
28
30
|
# @parameter output [IO] The output stream for error messages.
|
|
29
31
|
# @returns [Object | Nil] The result of the command's call method, or nil if parsing/execution failed.
|
|
30
|
-
def self.call(
|
|
31
|
-
self.parse(
|
|
32
|
+
def self.call(arguments = ARGV, output: $stderr)
|
|
33
|
+
self.parse(arguments).call
|
|
32
34
|
rescue Error => error
|
|
33
35
|
error.command.print_usage(output: output) do |formatter|
|
|
34
36
|
formatter.map(error)
|
|
@@ -37,27 +39,40 @@ module Samovar
|
|
|
37
39
|
return nil
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
# Complete the command-line input without executing the command.
|
|
43
|
+
# The given arguments are treated as the stable command-line boundary; completion internals consume derived input arrays.
|
|
44
|
+
#
|
|
45
|
+
# @parameter arguments [Array(String)] The command-line arguments to complete.
|
|
46
|
+
# @parameter environment [Hash] The environment for completion callbacks.
|
|
47
|
+
# @parameter output [IO] The output stream for printing completion results.
|
|
48
|
+
# @returns [Completion::Result] The completion result.
|
|
49
|
+
def self.complete(arguments = ARGV, environment: ENV, output: $stdout)
|
|
50
|
+
Completion.complete(self, arguments, environment: environment).tap do |result|
|
|
51
|
+
result.print(output)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
40
55
|
# Parse the command-line input and create a command instance.
|
|
41
56
|
#
|
|
42
57
|
# This is the low-level parsing primitive. It raises {Error} exceptions on parsing failures.
|
|
43
58
|
# For CLI applications, use {call} instead which handles errors gracefully.
|
|
44
59
|
#
|
|
45
|
-
# @parameter
|
|
60
|
+
# @parameter arguments [Array(String)] The command-line arguments to parse.
|
|
46
61
|
# @returns [Command] The parsed command instance.
|
|
47
62
|
# @raises [Error] If parsing fails.
|
|
48
|
-
def self.parse(
|
|
49
|
-
self.new(
|
|
63
|
+
def self.parse(arguments)
|
|
64
|
+
self.new(arguments)
|
|
50
65
|
end
|
|
51
66
|
|
|
52
67
|
# Create a new command instance with the given arguments.
|
|
53
68
|
#
|
|
54
69
|
# This is a convenience method for creating command instances with explicit arguments.
|
|
55
70
|
#
|
|
56
|
-
# @parameter
|
|
71
|
+
# @parameter arguments [Array(String)] The command-line arguments to parse.
|
|
57
72
|
# @parameter options [Hash] Additional options to pass to the command.
|
|
58
73
|
# @returns [Command] The command instance.
|
|
59
|
-
def self.[](*
|
|
60
|
-
self.new(
|
|
74
|
+
def self.[](*arguments, **options)
|
|
75
|
+
self.new(arguments, **options)
|
|
61
76
|
end
|
|
62
77
|
|
|
63
78
|
class << self
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "result"
|
|
7
|
+
|
|
8
|
+
module Samovar
|
|
9
|
+
module Completion
|
|
10
|
+
# The context provided to dynamic completion callbacks.
|
|
11
|
+
class Context
|
|
12
|
+
# Build a context for a command class and argument list.
|
|
13
|
+
#
|
|
14
|
+
# @parameter command_class [Class] The command class to complete.
|
|
15
|
+
# @parameter arguments [Array(String)] The truncated command-line arguments.
|
|
16
|
+
# @parameter environment [Hash] The environment for completion callbacks.
|
|
17
|
+
# @returns [Context] The completion context.
|
|
18
|
+
def self.for(command_class, arguments, environment: ENV)
|
|
19
|
+
arguments = arguments.dup
|
|
20
|
+
current = arguments.pop || ""
|
|
21
|
+
|
|
22
|
+
return self.new(
|
|
23
|
+
command_class.table.merged,
|
|
24
|
+
arguments,
|
|
25
|
+
current,
|
|
26
|
+
environment: environment,
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Initialize a new completion context.
|
|
31
|
+
#
|
|
32
|
+
# @parameter table [Table] The command table to complete.
|
|
33
|
+
# @parameter arguments [Array(String)] The completed words before the current token.
|
|
34
|
+
# @parameter current [String] The token being completed.
|
|
35
|
+
# @parameter row [Object | Nil] The parser row whose value is being completed.
|
|
36
|
+
# @parameter environment [Hash] The environment for completion callbacks.
|
|
37
|
+
def initialize(table, arguments, current, row = nil, environment: ENV)
|
|
38
|
+
@table = table
|
|
39
|
+
@arguments = arguments
|
|
40
|
+
@current = current
|
|
41
|
+
@row = row
|
|
42
|
+
@environment = environment
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @attribute [Table] The command table to complete.
|
|
46
|
+
attr :table
|
|
47
|
+
|
|
48
|
+
# @attribute [Array(String)] The completed words before the current token.
|
|
49
|
+
attr :arguments
|
|
50
|
+
|
|
51
|
+
# @attribute [String] The token being completed.
|
|
52
|
+
attr :current
|
|
53
|
+
|
|
54
|
+
# @attribute [Object | Nil] The parser row whose value is being completed.
|
|
55
|
+
attr :row
|
|
56
|
+
|
|
57
|
+
# @attribute [Hash] The environment for completion callbacks.
|
|
58
|
+
attr :environment
|
|
59
|
+
|
|
60
|
+
# Create a context for completing the given parser row.
|
|
61
|
+
#
|
|
62
|
+
# @parameter row [Object] The parser row whose value is being completed.
|
|
63
|
+
# @returns [Context] The specialized completion context.
|
|
64
|
+
def with_row(row)
|
|
65
|
+
return self.class.new(
|
|
66
|
+
@table,
|
|
67
|
+
@arguments,
|
|
68
|
+
@current,
|
|
69
|
+
row,
|
|
70
|
+
environment: @environment,
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Complete the current command class.
|
|
75
|
+
#
|
|
76
|
+
# @returns [Result] The completion result.
|
|
77
|
+
def complete
|
|
78
|
+
complete_rows(@table, @arguments.dup)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Complete the given command class with completed words.
|
|
82
|
+
#
|
|
83
|
+
# @parameter command_class [Class] The command class to complete.
|
|
84
|
+
# @parameter words [Array(String)] The completed words before the current token.
|
|
85
|
+
# @returns [Result] The completion result.
|
|
86
|
+
def complete_command(command_class, words = [])
|
|
87
|
+
complete_rows(command_class.table.merged, words)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Complete the rows in a command table.
|
|
91
|
+
# The input array is mutable and may be consumed by parser rows.
|
|
92
|
+
#
|
|
93
|
+
# @parameter table [Table] The command table to complete.
|
|
94
|
+
# @parameter input [Array(String)] The mutable completed words to consume.
|
|
95
|
+
# @returns [Result] The completion result.
|
|
96
|
+
def complete_rows(table, input)
|
|
97
|
+
collected = []
|
|
98
|
+
|
|
99
|
+
table.each do |row|
|
|
100
|
+
if row.respond_to?(:complete)
|
|
101
|
+
if result = row.complete(input, self, collected)
|
|
102
|
+
return result
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
return Result.new(collected)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "result"
|
|
7
|
+
require_relative "suggestion"
|
|
8
|
+
|
|
9
|
+
module Samovar
|
|
10
|
+
module Completion
|
|
11
|
+
# Expands static, dynamic, and native completion providers.
|
|
12
|
+
class Provider
|
|
13
|
+
# Initialize a new completion provider.
|
|
14
|
+
#
|
|
15
|
+
# @parameter context [Context] The completion context.
|
|
16
|
+
# @parameter completions [Array | Proc | Symbol | Nil] The static, dynamic, or native completions.
|
|
17
|
+
def initialize(context, completions)
|
|
18
|
+
@context = context
|
|
19
|
+
@completions = completions
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Generate suggestions from the provider.
|
|
23
|
+
#
|
|
24
|
+
# @returns [Result] The matching completion suggestions.
|
|
25
|
+
def suggestions
|
|
26
|
+
case @completions
|
|
27
|
+
when nil
|
|
28
|
+
Result.new
|
|
29
|
+
when Symbol
|
|
30
|
+
native_suggestions
|
|
31
|
+
else
|
|
32
|
+
matching_suggestions
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
protected
|
|
37
|
+
|
|
38
|
+
# Generate matching suggestions from static or dynamic completions.
|
|
39
|
+
#
|
|
40
|
+
# @returns [Result] The matching completion suggestions.
|
|
41
|
+
def matching_suggestions
|
|
42
|
+
values = @completions
|
|
43
|
+
|
|
44
|
+
if values.respond_to?(:call)
|
|
45
|
+
values = values.call(@context)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
values = Array(values).filter_map do |value|
|
|
49
|
+
suggestion = Suggestion.wrap(value)
|
|
50
|
+
|
|
51
|
+
suggestion if suggestion.start_with?(@context.current)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return Result.new(values)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Generate native shell completion requests.
|
|
58
|
+
#
|
|
59
|
+
# @returns [Result] The native completion request suggestions.
|
|
60
|
+
def native_suggestions
|
|
61
|
+
case @completions
|
|
62
|
+
when :path, :file
|
|
63
|
+
Result.new([Suggestion.new(@context.current, description: "Path", type: :path)])
|
|
64
|
+
when :directory
|
|
65
|
+
Result.new([Suggestion.new(@context.current, description: "Directory", type: :directory)])
|
|
66
|
+
when :executable
|
|
67
|
+
Result.new([Suggestion.new(@context.current, description: "Executable", type: :executable)])
|
|
68
|
+
else
|
|
69
|
+
Result.new
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Samovar
|
|
7
|
+
module Completion
|
|
8
|
+
# A collection of completion suggestions.
|
|
9
|
+
class Result
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
# Initialize a new completion result.
|
|
13
|
+
#
|
|
14
|
+
# @parameter suggestions [Array(Suggestion)] The suggestions in this result.
|
|
15
|
+
def initialize(suggestions = [])
|
|
16
|
+
@suggestions = suggestions
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @attribute [Array(Suggestion)] The suggestions in this result.
|
|
20
|
+
attr :suggestions
|
|
21
|
+
|
|
22
|
+
# Iterate over each suggestion.
|
|
23
|
+
#
|
|
24
|
+
# @yields {|suggestion| ...} The block to call for each suggestion.
|
|
25
|
+
def each(&block)
|
|
26
|
+
@suggestions.each(&block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Whether this result contains no suggestions.
|
|
30
|
+
#
|
|
31
|
+
# @returns [Boolean] True if there are no suggestions.
|
|
32
|
+
def empty?
|
|
33
|
+
@suggestions.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Combine this result with another result.
|
|
37
|
+
#
|
|
38
|
+
# @parameter other [Result] The other result to append.
|
|
39
|
+
# @returns [Result] The combined result.
|
|
40
|
+
def +(other)
|
|
41
|
+
self.class.new(@suggestions + other.suggestions)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Print suggestions as tab-separated completion records.
|
|
45
|
+
#
|
|
46
|
+
# @parameter output [IO] The output stream to write to.
|
|
47
|
+
def print(output = $stdout)
|
|
48
|
+
each do |suggestion|
|
|
49
|
+
output.puts suggestion.to_record
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Samovar
|
|
7
|
+
module Completion
|
|
8
|
+
# A single completion suggestion.
|
|
9
|
+
class Suggestion
|
|
10
|
+
# Wrap a raw completion value in a suggestion.
|
|
11
|
+
#
|
|
12
|
+
# @parameter value [Suggestion | Hash | Object] The value to wrap.
|
|
13
|
+
# @returns [Suggestion] The normalized suggestion.
|
|
14
|
+
def self.wrap(value)
|
|
15
|
+
case value
|
|
16
|
+
when self
|
|
17
|
+
return value
|
|
18
|
+
when Hash
|
|
19
|
+
value = value.dup
|
|
20
|
+
suggestion = value.fetch(:value)
|
|
21
|
+
value.delete(:value)
|
|
22
|
+
|
|
23
|
+
return self.new(suggestion, **value)
|
|
24
|
+
else
|
|
25
|
+
return self.new(value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Initialize a new completion suggestion.
|
|
30
|
+
#
|
|
31
|
+
# @parameter value [Object] The completion value.
|
|
32
|
+
# @parameter type [Symbol | String | Nil] The completion type.
|
|
33
|
+
# @parameter description [String | Nil] The completion description.
|
|
34
|
+
# @parameter options [Hash] Additional completion metadata.
|
|
35
|
+
def initialize(value, type: nil, description: nil, **options)
|
|
36
|
+
@type = type
|
|
37
|
+
@value = value
|
|
38
|
+
@description = description
|
|
39
|
+
@options = options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @attribute [Symbol | String | Nil] The completion type.
|
|
43
|
+
attr :type
|
|
44
|
+
|
|
45
|
+
# @attribute [Object] The completion value.
|
|
46
|
+
attr :value
|
|
47
|
+
|
|
48
|
+
# @attribute [String | Nil] The completion description.
|
|
49
|
+
attr :description
|
|
50
|
+
|
|
51
|
+
# @attribute [Hash] Additional completion metadata.
|
|
52
|
+
attr :options
|
|
53
|
+
|
|
54
|
+
# Whether this suggestion starts with the given prefix.
|
|
55
|
+
#
|
|
56
|
+
# @parameter prefix [String] The prefix to check.
|
|
57
|
+
# @returns [Boolean] True if the suggestion starts with the given prefix.
|
|
58
|
+
def start_with?(prefix)
|
|
59
|
+
to_s.start_with?(prefix)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Convert the suggestion to a tab-separated completion record.
|
|
63
|
+
#
|
|
64
|
+
# @returns [String] The escaped completion record.
|
|
65
|
+
def to_record
|
|
66
|
+
fields = [
|
|
67
|
+
escape(@type),
|
|
68
|
+
escape(@value),
|
|
69
|
+
escape(@description),
|
|
70
|
+
]
|
|
71
|
+
@options.each do |key, value|
|
|
72
|
+
next if value.nil?
|
|
73
|
+
|
|
74
|
+
fields << "#{escape(key)}=#{escape(value)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return fields.join("\t")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Convert the suggestion to its value.
|
|
81
|
+
#
|
|
82
|
+
# @returns [String] The suggestion value.
|
|
83
|
+
def to_s
|
|
84
|
+
@value.to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def escape(value)
|
|
90
|
+
value.to_s.gsub(/[\t\r\n]/, " ")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "completion/context"
|
|
7
|
+
require_relative "completion/provider"
|
|
8
|
+
require_relative "completion/result"
|
|
9
|
+
require_relative "completion/suggestion"
|
|
10
|
+
|
|
11
|
+
module Samovar
|
|
12
|
+
# Shell completion support for Samovar commands.
|
|
13
|
+
module Completion
|
|
14
|
+
# Complete the command line for the given command class.
|
|
15
|
+
#
|
|
16
|
+
# @parameter command_class [Class] The command class to complete.
|
|
17
|
+
# @parameter arguments [Array(String)] The application arguments.
|
|
18
|
+
# @parameter environment [Hash] The environment for completion callbacks.
|
|
19
|
+
# @returns [Result] The completion result.
|
|
20
|
+
def self.complete(command_class, arguments, environment: ENV)
|
|
21
|
+
Context.for(command_class, arguments, environment: environment).complete
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/samovar/failure.rb
CHANGED