r2ui 0.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +79 -0
- data/lib/r2ui/app.rb +311 -34
- data/lib/r2ui/canvas.rb +135 -14
- data/lib/r2ui/cli/builder.rb +72 -0
- data/lib/r2ui/cli/context.rb +45 -0
- data/lib/r2ui/cli/definition.rb +174 -0
- data/lib/r2ui/cli/ext/ask.rb +133 -0
- data/lib/r2ui/cli/ext/box.rb +112 -0
- data/lib/r2ui/cli/ext/choose.rb +162 -0
- data/lib/r2ui/cli/ext/choose_many.rb +241 -0
- data/lib/r2ui/cli/ext/color_option.rb +64 -0
- data/lib/r2ui/cli/ext/completion.rb +520 -0
- data/lib/r2ui/cli/ext/config.rb +179 -0
- data/lib/r2ui/cli/ext/confirm.rb +110 -0
- data/lib/r2ui/cli/ext/dashboard.rb +105 -0
- data/lib/r2ui/cli/ext/diff.rb +195 -0
- data/lib/r2ui/cli/ext/editor.rb +94 -0
- data/lib/r2ui/cli/ext/env.rb +108 -0
- data/lib/r2ui/cli/ext/examples.rb +62 -0
- data/lib/r2ui/cli/ext/filter.rb +269 -0
- data/lib/r2ui/cli/ext/format.rb +104 -0
- data/lib/r2ui/cli/ext/list.rb +117 -0
- data/lib/r2ui/cli/ext/log.rb +89 -0
- data/lib/r2ui/cli/ext/pager.rb +97 -0
- data/lib/r2ui/cli/ext/pairs.rb +62 -0
- data/lib/r2ui/cli/ext/parallel.rb +274 -0
- data/lib/r2ui/cli/ext/password.rb +158 -0
- data/lib/r2ui/cli/ext/progress.rb +330 -0
- data/lib/r2ui/cli/ext/reference.rb +129 -0
- data/lib/r2ui/cli/ext/spinner.rb +165 -0
- data/lib/r2ui/cli/ext/summary.rb +63 -0
- data/lib/r2ui/cli/ext/table.rb +141 -0
- data/lib/r2ui/cli/ext/tasks.rb +216 -0
- data/lib/r2ui/cli/ext/trace.rb +64 -0
- data/lib/r2ui/cli/ext/tree.rb +114 -0
- data/lib/r2ui/cli/ext/version.rb +67 -0
- data/lib/r2ui/cli/extension.rb +129 -0
- data/lib/r2ui/cli/help.rb +115 -0
- data/lib/r2ui/cli/helpers.rb +20 -0
- data/lib/r2ui/cli/live.rb +150 -0
- data/lib/r2ui/cli/parser.rb +165 -0
- data/lib/r2ui/cli/prompt.rb +133 -0
- data/lib/r2ui/cli/runner.rb +86 -0
- data/lib/r2ui/cli/shell.rb +185 -0
- data/lib/r2ui/cli/testing.rb +49 -0
- data/lib/r2ui/cli.rb +129 -0
- data/lib/r2ui/compat/bubbletea/LICENSE-bubbletea.txt +21 -0
- data/lib/r2ui/compat/bubbletea/ansi.rb +424 -0
- data/lib/r2ui/compat/bubbletea/commands.rb +143 -0
- data/lib/r2ui/compat/bubbletea/input.rb +269 -0
- data/lib/r2ui/compat/bubbletea/input_reader.rb +66 -0
- data/lib/r2ui/compat/bubbletea/messages.rb +251 -0
- data/lib/r2ui/compat/bubbletea/model.rb +65 -0
- data/lib/r2ui/compat/bubbletea/native.rb +254 -0
- data/lib/r2ui/compat/bubbletea/renderer.rb +192 -0
- data/lib/r2ui/compat/bubbletea/runner.rb +425 -0
- data/lib/r2ui/compat/bubbletea/terminal.rb +230 -0
- data/lib/r2ui/compat/bubbletea/version.rb +8 -0
- data/lib/r2ui/compat/bubbletea/width_table.rb +228 -0
- data/lib/r2ui/compat/bubbletea.rb +16 -0
- data/lib/r2ui/compat/lipgloss/border.rb +17 -0
- data/lib/r2ui/compat/lipgloss/color.rb +78 -0
- data/lib/r2ui/compat/lipgloss/color_blend.rb +166 -0
- data/lib/r2ui/compat/lipgloss/colorful.rb +585 -0
- data/lib/r2ui/compat/lipgloss/layout.rb +461 -0
- data/lib/r2ui/compat/lipgloss/list.rb +85 -0
- data/lib/r2ui/compat/lipgloss/position.rb +30 -0
- data/lib/r2ui/compat/lipgloss/style/border.rb +134 -0
- data/lib/r2ui/compat/lipgloss/style.rb +1062 -0
- data/lib/r2ui/compat/lipgloss/table/render.rb +518 -0
- data/lib/r2ui/compat/lipgloss/table.rb +185 -0
- data/lib/r2ui/compat/lipgloss/termenv.rb +768 -0
- data/lib/r2ui/compat/lipgloss/text/unicode_tables.rb +428 -0
- data/lib/r2ui/compat/lipgloss/text.rb +1555 -0
- data/lib/r2ui/compat/lipgloss/tree/go_tree.rb +243 -0
- data/lib/r2ui/compat/lipgloss/tree.rb +105 -0
- data/lib/r2ui/compat/lipgloss/version.rb +15 -0
- data/lib/r2ui/compat/lipgloss.rb +64 -0
- data/lib/r2ui/compat/load_path/bubbletea.rb +4 -0
- data/lib/r2ui/compat/load_path/lipgloss.rb +4 -0
- data/lib/r2ui/component.rb +67 -0
- data/lib/r2ui/context.rb +87 -0
- data/lib/r2ui/drop_in.rb +16 -0
- data/lib/r2ui/dsl/column.rb +11 -3
- data/lib/r2ui/dsl/dashboard.rb +45 -9
- data/lib/r2ui/dsl/resource.rb +12 -0
- data/lib/r2ui/ext/after.rb +54 -0
- data/lib/r2ui/ext/async.rb +81 -0
- data/lib/r2ui/ext/batch_sequence.rb +54 -0
- data/lib/r2ui/ext/countdown.rb +52 -0
- data/lib/r2ui/ext/data_table.rb +71 -0
- data/lib/r2ui/ext/every.rb +66 -0
- data/lib/r2ui/ext/exec.rb +116 -0
- data/lib/r2ui/ext/file_picker.rb +78 -0
- data/lib/r2ui/ext/focus_report.rb +69 -0
- data/lib/r2ui/ext/help.rb +62 -0
- data/lib/r2ui/ext/keys.rb +64 -0
- data/lib/r2ui/ext/list.rb +145 -0
- data/lib/r2ui/ext/messages.rb +87 -0
- data/lib/r2ui/ext/mouse.rb +134 -0
- data/lib/r2ui/ext/paginator.rb +86 -0
- data/lib/r2ui/ext/paste.rb +86 -0
- data/lib/r2ui/ext/println.rb +21 -0
- data/lib/r2ui/ext/progress.rb +85 -0
- data/lib/r2ui/ext/refresh.rb +56 -0
- data/lib/r2ui/ext/resize.rb +62 -0
- data/lib/r2ui/ext/screen.rb +47 -0
- data/lib/r2ui/ext/screen_mode.rb +63 -0
- data/lib/r2ui/ext/spinner.rb +59 -0
- data/lib/r2ui/ext/state.rb +45 -0
- data/lib/r2ui/ext/stopwatch.rb +67 -0
- data/lib/r2ui/ext/suspend.rb +56 -0
- data/lib/r2ui/ext/text_area.rb +73 -0
- data/lib/r2ui/ext/text_input.rb +83 -0
- data/lib/r2ui/ext/theme.rb +112 -0
- data/lib/r2ui/ext/view.rb +37 -0
- data/lib/r2ui/ext/viewport.rb +92 -0
- data/lib/r2ui/ext/window_title.rb +51 -0
- data/lib/r2ui/extension.rb +199 -0
- data/lib/r2ui/history.rb +4 -1
- data/lib/r2ui/keys.rb +35 -8
- data/lib/r2ui/motion.rb +169 -0
- data/lib/r2ui/panel_state.rb +46 -2
- data/lib/r2ui/renderer.rb +87 -22
- data/lib/r2ui/version.rb +1 -1
- data/lib/r2ui/widgets/box.rb +6 -3
- data/lib/r2ui/widgets/glyphs.rb +149 -0
- data/lib/r2ui/widgets/table.rb +109 -25
- data/lib/r2ui.rb +12 -1
- metadata +119 -4
- data/lib/r2ui/terminal.rb +0 -53
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# c24-completion: shell completion scripts, generated from the command tree.
|
|
4
|
+
#
|
|
5
|
+
# R2UI.cli "deployer" do
|
|
6
|
+
# completion # adds `deployer completion <bash|zsh|fish>`
|
|
7
|
+
# command :deploy do
|
|
8
|
+
# aliases :d
|
|
9
|
+
# option :env, short: "e", in: %w[staging production]
|
|
10
|
+
# end
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# `deployer completion bash` (or zsh, fish) prints a static script that completes, at every level
|
|
14
|
+
# of the tree, the commands and their aliases, the options a command accepts (its own and its
|
|
15
|
+
# groups', `--no-x` for default-true flags, `--help`) and the `in:` choices of an option's value
|
|
16
|
+
# (`--env <TAB>`, `-e <TAB>`, `--env=<TAB>`); `:path` options complete files. Hidden commands and
|
|
17
|
+
# options are left out. zsh and fish show each command's summary and each option's `desc:`.
|
|
18
|
+
#
|
|
19
|
+
# eval "$(deployer completion bash)" # in ~/.bashrc
|
|
20
|
+
# eval "$(deployer completion zsh)" # in ~/.zshrc, after compinit
|
|
21
|
+
# deployer completion fish | source # in ~/.config/fish/config.fish
|
|
22
|
+
#
|
|
23
|
+
# `deployer completion --help` shows those lines. The script is plain text on a terminal and off
|
|
24
|
+
# it alike (it's data); on a terminal a hint on stderr says how to load it. An unknown shell is a
|
|
25
|
+
# usage error with a "did you mean".
|
|
26
|
+
module R2UI
|
|
27
|
+
module CLI
|
|
28
|
+
module Ext
|
|
29
|
+
module Completion
|
|
30
|
+
SHELLS = { "bash" => "Bourne Again SHell", "zsh" => "Z shell", "fish" => "Friendly interactive shell" }.freeze
|
|
31
|
+
SETUP = {
|
|
32
|
+
"bash" => ['eval "$(%s completion bash)"', "in ~/.bashrc"],
|
|
33
|
+
"zsh" => ['eval "$(%s completion zsh)"', "in ~/.zshrc, after compinit"],
|
|
34
|
+
"fish" => ["%s completion fish | source", "in ~/.config/fish/config.fish"]
|
|
35
|
+
}.freeze
|
|
36
|
+
HELP = { long: "--help", short: "-h", negation: nil, desc: "Show help", value: false, choices: nil, path: false }.freeze
|
|
37
|
+
|
|
38
|
+
# One command the completion knows: its number in the script, the words that may follow it
|
|
39
|
+
# (subcommands and aliases, or the completion command's shells) and the options it takes.
|
|
40
|
+
Node = Struct.new(:id, :command, :words, :options)
|
|
41
|
+
|
|
42
|
+
# The visible tree as numbered nodes and links ("from word" → to).
|
|
43
|
+
class Tree
|
|
44
|
+
attr_reader :nodes, :links
|
|
45
|
+
|
|
46
|
+
def initialize(program)
|
|
47
|
+
@nodes = []
|
|
48
|
+
@links = []
|
|
49
|
+
add(program)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def add(command)
|
|
55
|
+
node = Node.new(@nodes.size, command, [], options(command))
|
|
56
|
+
@nodes << node
|
|
57
|
+
if command.declared(:completion_command).any?
|
|
58
|
+
node.words = SHELLS.to_a
|
|
59
|
+
return node
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
visible = command.commands.reject(&:hidden)
|
|
63
|
+
visible.each do |sub|
|
|
64
|
+
child = add(sub)
|
|
65
|
+
[sub.name, *sub.aliases].each do |word|
|
|
66
|
+
node.words << [word, sub.summary]
|
|
67
|
+
@links << [node.id, word, child.id]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
# `tool help deploy`: offered at the root only (like cobra), understood in every group.
|
|
71
|
+
if visible.any? && !command.find("help")
|
|
72
|
+
node.words << ["help", "Show help for a command"] if command.root?
|
|
73
|
+
@links << [node.id, "help", node.id]
|
|
74
|
+
end
|
|
75
|
+
node
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def options(command)
|
|
79
|
+
command.all_options.reject(&:hidden).map do |o|
|
|
80
|
+
negation = o.boolean? && o.default == true ? "--no-#{o.long.delete_prefix("--")}" : nil
|
|
81
|
+
{ long: o.long, short: o.short && "-#{o.short}", negation:, desc: o.desc, value: !o.boolean?,
|
|
82
|
+
choices: o.choices&.map(&:to_s), path: o.type == :path }
|
|
83
|
+
end + [HELP]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
module_function
|
|
88
|
+
|
|
89
|
+
def script(program, kind)
|
|
90
|
+
Script.new(program).public_send(kind)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def unknown(kind)
|
|
94
|
+
message = "unknown shell '#{kind}' (expected #{SHELLS.keys[0..-2].join(", ")} or #{SHELLS.keys.last})"
|
|
95
|
+
guess = DidYouMean::SpellChecker.new(dictionary: SHELLS.keys).correct(kind).first if defined?(DidYouMean)
|
|
96
|
+
guess ? "#{message}. Did you mean '#{guess}'?" : message
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def setup_lines(shell, name)
|
|
100
|
+
rows = SETUP.map { |kind, (line, where)| [kind, format(line, name), where] }
|
|
101
|
+
kind_width = rows.map { |r| r[0].size }.max
|
|
102
|
+
line_width = rows.map { |r| r[1].size }.max
|
|
103
|
+
rows.map do |kind, line, where|
|
|
104
|
+
"#{kind.ljust(kind_width)} #{shell.paint(line, :accent)}#{" " * (line_width - line.size)} #{shell.paint(where, :muted)}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The three scripts. Every table is a `case` on node numbers, so names never become
|
|
109
|
+
# code; words are single-quoted, and choices also backslash-escaped for the shells that
|
|
110
|
+
# expand them.
|
|
111
|
+
class Script
|
|
112
|
+
def initialize(program)
|
|
113
|
+
@name = program.name
|
|
114
|
+
@id = program.name.gsub(/[^A-Za-z0-9_]/, "_")
|
|
115
|
+
@tree = Tree.new(program)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def bash
|
|
119
|
+
<<~BASH
|
|
120
|
+
# bash completion for #{@name}
|
|
121
|
+
# Generated by `#{@name} completion bash`; regenerate it when the tool changes.
|
|
122
|
+
# Load it in this shell: eval "$(#{@name} completion bash)"
|
|
123
|
+
# For every new shell, add that line to ~/.bashrc.
|
|
124
|
+
|
|
125
|
+
#{next_function}
|
|
126
|
+
|
|
127
|
+
#{takes_value_function}
|
|
128
|
+
|
|
129
|
+
__#{@id}_complete_choices() {
|
|
130
|
+
case "$1 $2" in
|
|
131
|
+
#{indent(choice_cases { |words| "REPLY=#{sq(words.map { |w| escape(w) }.join(" "))}" }, 4)}
|
|
132
|
+
*) return 1 ;;
|
|
133
|
+
esac
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
__#{@id}_complete_options() {
|
|
137
|
+
case "$1" in
|
|
138
|
+
#{indent(@tree.nodes.map { |n| "#{n.id}) REPLY=#{sq(option_words(n).join(" "))} ;; # #{n.command.full_name}" }, 4)}
|
|
139
|
+
*) return 1 ;;
|
|
140
|
+
esac
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
__#{@id}_complete_words() {
|
|
144
|
+
case "$1" in
|
|
145
|
+
#{indent(word_nodes.map { |n| "#{n.id}) REPLY=#{sq(n.words.map { |w, _| escape(w) }.join(" "))} ;; # #{n.command.full_name}" }, 4)}
|
|
146
|
+
*) return 1 ;;
|
|
147
|
+
esac
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_#{@id}() {
|
|
151
|
+
local line=${COMP_LINE:0:COMP_POINT} node=0 word cur skip= positional= REPLY i
|
|
152
|
+
local -a words
|
|
153
|
+
read -ra words <<< "$line"
|
|
154
|
+
[[ -z $line || $line == *[[:space:]] ]] && words+=("")
|
|
155
|
+
cur=${words[${#words[@]}-1]}
|
|
156
|
+
COMPREPLY=()
|
|
157
|
+
for ((i = 1; i < ${#words[@]} - 1; i++)); do
|
|
158
|
+
word=${words[i]}
|
|
159
|
+
if [[ -n $skip ]]; then skip=; continue; fi
|
|
160
|
+
case $word in
|
|
161
|
+
--) return 0 ;;
|
|
162
|
+
--*=*) ;;
|
|
163
|
+
--*) __#{@id}_complete_takes_value "$node" "$word" && skip=$word ;;
|
|
164
|
+
-?*) __#{@id}_complete_takes_value "$node" "-${word: -1}" && skip="-${word: -1}" ;;
|
|
165
|
+
*)
|
|
166
|
+
if [[ -z $positional ]] && __#{@id}_complete_next "$node $word"; then node=$REPLY; else positional=1; fi ;;
|
|
167
|
+
esac
|
|
168
|
+
done
|
|
169
|
+
if [[ -n $skip ]]; then
|
|
170
|
+
__#{@id}_complete_choices "$node" "$skip" && COMPREPLY=($(compgen -W "$REPLY" -- "$cur"))
|
|
171
|
+
return 0
|
|
172
|
+
fi
|
|
173
|
+
if [[ $cur == --*=* ]]; then
|
|
174
|
+
local prefix=
|
|
175
|
+
[[ $COMP_WORDBREAKS == *=* ]] || prefix="${cur%%=*}="
|
|
176
|
+
__#{@id}_complete_choices "$node" "${cur%%=*}" && COMPREPLY=($(compgen -P "$prefix" -W "$REPLY" -- "${cur#*=}"))
|
|
177
|
+
return 0
|
|
178
|
+
fi
|
|
179
|
+
if [[ $cur == -* ]]; then
|
|
180
|
+
__#{@id}_complete_options "$node" && COMPREPLY=($(compgen -W "$REPLY" -- "$cur"))
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
[[ -z $positional ]] && __#{@id}_complete_words "$node" && COMPREPLY=($(compgen -W "$REPLY" -- "$cur"))
|
|
184
|
+
return 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
complete -o default -F _#{@id} #{sq(@name)}
|
|
188
|
+
BASH
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def zsh
|
|
192
|
+
<<~ZSH
|
|
193
|
+
#compdef #{@name}
|
|
194
|
+
# zsh completion for #{@name}
|
|
195
|
+
# Generated by `#{@name} completion zsh`; regenerate it when the tool changes.
|
|
196
|
+
# Load it in this shell (after compinit): eval "$(#{@name} completion zsh)"
|
|
197
|
+
# Or save it on your $fpath: #{@name} completion zsh > "${fpath[1]}/_#{@id}"
|
|
198
|
+
|
|
199
|
+
#{next_function}
|
|
200
|
+
|
|
201
|
+
#{takes_value_function}
|
|
202
|
+
|
|
203
|
+
# Status 0 with the choices in $candidates, 2 for a path, 1 for any other value.
|
|
204
|
+
__#{@id}_complete_choices() {
|
|
205
|
+
case "$1 $2" in
|
|
206
|
+
#{indent(choice_cases { |words| "candidates=(#{words.map { |w| sq(w) }.join(" ")})" }, 4)}
|
|
207
|
+
#{indent(path_cases, 4)}
|
|
208
|
+
*) return 1 ;;
|
|
209
|
+
esac
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
__#{@id}_complete_options() {
|
|
213
|
+
case "$1" in
|
|
214
|
+
#{indent(@tree.nodes.map { |n| "#{n.id}) candidates=(#{described_options(n)}) ;; # #{n.command.full_name}" }, 4)}
|
|
215
|
+
*) return 1 ;;
|
|
216
|
+
esac
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
__#{@id}_complete_words() {
|
|
220
|
+
case "$1" in
|
|
221
|
+
#{indent(word_nodes.map { |n| "#{n.id}) REPLY=#{word_kind(n)} candidates=(#{n.words.map { |w, d| described(w, d) }.join(" ")}) ;; # #{n.command.full_name}" }, 4)}
|
|
222
|
+
*) return 1 ;;
|
|
223
|
+
esac
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_#{@id}() {
|
|
227
|
+
local node=0 word cur=${words[CURRENT]} skip= positional= REPLY i rc
|
|
228
|
+
local -a candidates
|
|
229
|
+
for (( i = 2; i < CURRENT; i++ )); do
|
|
230
|
+
word=${words[i]}
|
|
231
|
+
if [[ -n $skip ]]; then skip=; continue; fi
|
|
232
|
+
case $word in
|
|
233
|
+
--) _files; return ;;
|
|
234
|
+
--*=*) ;;
|
|
235
|
+
--*) __#{@id}_complete_takes_value $node "$word" && skip=$word ;;
|
|
236
|
+
-?*) __#{@id}_complete_takes_value $node "-${word[-1]}" && skip="-${word[-1]}" ;;
|
|
237
|
+
*)
|
|
238
|
+
if [[ -z $positional ]] && __#{@id}_complete_next "$node $word"; then node=$REPLY; else positional=1; fi ;;
|
|
239
|
+
esac
|
|
240
|
+
done
|
|
241
|
+
if [[ -z $skip && $cur == --*=* ]]; then
|
|
242
|
+
skip=${cur%%=*}
|
|
243
|
+
compset -P '*='
|
|
244
|
+
fi
|
|
245
|
+
if [[ -n $skip ]]; then
|
|
246
|
+
__#{@id}_complete_choices $node "$skip"
|
|
247
|
+
rc=$?
|
|
248
|
+
if (( rc == 0 )); then compadd -a candidates; elif (( rc == 2 )); then _files; fi
|
|
249
|
+
return
|
|
250
|
+
fi
|
|
251
|
+
if [[ $cur == -* ]]; then
|
|
252
|
+
__#{@id}_complete_options $node && _describe -t options option candidates
|
|
253
|
+
return
|
|
254
|
+
fi
|
|
255
|
+
if [[ -z $positional ]] && __#{@id}_complete_words $node; then
|
|
256
|
+
_describe -t ${REPLY}s $REPLY candidates
|
|
257
|
+
else
|
|
258
|
+
_files
|
|
259
|
+
fi
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if [ "$funcstack[1]" = "_#{@id}" ]; then
|
|
263
|
+
_#{@id} "$@"
|
|
264
|
+
else
|
|
265
|
+
compdef _#{@id} #{sq(@name)}
|
|
266
|
+
fi
|
|
267
|
+
ZSH
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def fish
|
|
271
|
+
cmd = fq(@name)
|
|
272
|
+
<<~FISH
|
|
273
|
+
# fish completion for #{@name}
|
|
274
|
+
# Generated by `#{@name} completion fish`; regenerate it when the tool changes.
|
|
275
|
+
# Load it in this shell: #{@name} completion fish | source
|
|
276
|
+
# Or save it: #{@name} completion fish > ~/.config/fish/completions/#{@name}.fish
|
|
277
|
+
|
|
278
|
+
function __#{@id}_complete_next
|
|
279
|
+
switch $argv[1]
|
|
280
|
+
#{indent(fish_next_cases, 8)}
|
|
281
|
+
end
|
|
282
|
+
return 1
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
function __#{@id}_complete_takes_value
|
|
286
|
+
switch "$argv[1] $argv[2]"
|
|
287
|
+
#{indent(fish_value_cases, 8)}
|
|
288
|
+
end
|
|
289
|
+
return 1
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# The node the command line is at; "N+" once a positional argument was typed.
|
|
293
|
+
function __#{@id}_complete_node
|
|
294
|
+
set -l node 0
|
|
295
|
+
set -l skip 0
|
|
296
|
+
set -l positional 0
|
|
297
|
+
set -l tokens (commandline -opc)
|
|
298
|
+
set -e tokens[1]
|
|
299
|
+
for word in $tokens
|
|
300
|
+
if test $skip = 1
|
|
301
|
+
set skip 0
|
|
302
|
+
continue
|
|
303
|
+
end
|
|
304
|
+
if test "$word" = --
|
|
305
|
+
echo $node+
|
|
306
|
+
return
|
|
307
|
+
else if string match -q -- '--*=*' $word
|
|
308
|
+
continue
|
|
309
|
+
else if string match -q -- '-?*' $word
|
|
310
|
+
set -l opt $word
|
|
311
|
+
string match -q -- '--*' $word; or set opt -(string sub -s -1 -- $word)
|
|
312
|
+
__#{@id}_complete_takes_value $node $opt; and set skip 1
|
|
313
|
+
else if test $positional = 0; and __#{@id}_complete_next "$node $word" >/dev/null
|
|
314
|
+
set node (__#{@id}_complete_next "$node $word")
|
|
315
|
+
else
|
|
316
|
+
set positional 1
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
if test $positional = 1
|
|
320
|
+
echo $node+
|
|
321
|
+
else
|
|
322
|
+
echo $node
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Before any positional argument: where command names complete.
|
|
327
|
+
function __#{@id}_complete_at
|
|
328
|
+
test (__#{@id}_complete_node) = $argv[1]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Anywhere in the command: where its options complete.
|
|
332
|
+
function __#{@id}_complete_in
|
|
333
|
+
set -l node (__#{@id}_complete_node)
|
|
334
|
+
test "$node" = $argv[1]; or test "$node" = "$argv[1]+"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
#{fish_entries(cmd).join("\n")}
|
|
338
|
+
FISH
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
private
|
|
342
|
+
|
|
343
|
+
def word_nodes = @tree.nodes.select { |n| n.words.any? }
|
|
344
|
+
|
|
345
|
+
def word_kind(node) = node.command.declared(:completion_command).any? ? "shell" : "command"
|
|
346
|
+
|
|
347
|
+
def option_words(node)
|
|
348
|
+
node.options.flat_map { |o| [o[:long], o[:short], o[:negation]] }.compact
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def value_options(node) = node.options.select { |o| o[:value] }
|
|
352
|
+
|
|
353
|
+
def next_function
|
|
354
|
+
cases = @tree.links.group_by(&:last).map do |to, links|
|
|
355
|
+
"#{links.map { |from, word, _| sq("#{from} #{word}") }.join("|")}) REPLY=#{to} ;;"
|
|
356
|
+
end
|
|
357
|
+
<<~SH.chomp
|
|
358
|
+
__#{@id}_complete_next() {
|
|
359
|
+
case "$1" in
|
|
360
|
+
#{indent(cases, 4)}
|
|
361
|
+
*) return 1 ;;
|
|
362
|
+
esac
|
|
363
|
+
}
|
|
364
|
+
SH
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def takes_value_function
|
|
368
|
+
cases = @tree.nodes.filter_map do |n|
|
|
369
|
+
names = value_options(n).flat_map { |o| [o[:long], o[:short]] }.compact
|
|
370
|
+
"#{names.map { |name| sq("#{n.id} #{name}") }.join("|")}) return 0 ;;" if names.any?
|
|
371
|
+
end
|
|
372
|
+
<<~SH.chomp
|
|
373
|
+
__#{@id}_complete_takes_value() {
|
|
374
|
+
case "$1 $2" in
|
|
375
|
+
#{indent(cases, 4)}
|
|
376
|
+
esac
|
|
377
|
+
return 1
|
|
378
|
+
}
|
|
379
|
+
SH
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def choice_cases
|
|
383
|
+
@tree.nodes.flat_map do |n|
|
|
384
|
+
value_options(n).select { |o| o[:choices] }.map do |o|
|
|
385
|
+
"#{option_keys(n, o)}) #{yield o[:choices]} ;;"
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def path_cases
|
|
391
|
+
@tree.nodes.flat_map do |n|
|
|
392
|
+
value_options(n).select { |o| o[:path] && !o[:choices] }.map { |o| "#{option_keys(n, o)}) return 2 ;;" }
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def option_keys(node, option)
|
|
397
|
+
[option[:long], option[:short]].compact.map { |name| sq("#{node.id} #{name}") }.join("|")
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def described_options(node)
|
|
401
|
+
node.options.flat_map do |o|
|
|
402
|
+
[o[:long], o[:short], o[:negation]].compact.map { |name| described(name, o[:desc]) }
|
|
403
|
+
end.join(" ")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def described(word, desc)
|
|
407
|
+
word = word.gsub(":", "\\:")
|
|
408
|
+
sq(desc.to_s.empty? ? word : "#{word}:#{desc.to_s.lines.first.chomp}")
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def fish_next_cases
|
|
412
|
+
@tree.links.group_by(&:last).map do |to, links|
|
|
413
|
+
"case #{links.map { |from, word, _| fq("#{from} #{word}") }.join(" ")}\n echo #{to}\n return 0"
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def fish_value_cases
|
|
418
|
+
@tree.nodes.filter_map do |n|
|
|
419
|
+
names = value_options(n).flat_map { |o| [o[:long], o[:short]] }.compact
|
|
420
|
+
"case #{names.map { |name| fq("#{n.id} #{name}") }.join(" ")}\n return 0" if names.any?
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def fish_entries(cmd)
|
|
425
|
+
lines = []
|
|
426
|
+
word_nodes.each do |n|
|
|
427
|
+
lines << "# #{n.command.full_name}"
|
|
428
|
+
n.words.each do |word, desc|
|
|
429
|
+
lines << "complete -c #{cmd} -f -n '__#{@id}_complete_at #{n.id}' -a #{fq(escape(word))}#{fish_desc(desc)}"
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
@tree.nodes.each do |n|
|
|
433
|
+
lines << "# #{n.command.full_name} options"
|
|
434
|
+
n.options.each do |o|
|
|
435
|
+
next if o.equal?(HELP)
|
|
436
|
+
|
|
437
|
+
lines << "complete -c #{cmd} -n '__#{@id}_complete_in #{n.id}'#{fish_option(o)}#{fish_desc(o[:desc])}"
|
|
438
|
+
if o[:negation]
|
|
439
|
+
lines << "complete -c #{cmd} -n '__#{@id}_complete_in #{n.id}' -l #{fq(o[:negation].delete_prefix("--"))}#{fish_desc(o[:desc])}"
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
lines << "complete -c #{cmd} -s h -l help -d 'Show help'"
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def fish_option(option)
|
|
447
|
+
parts = []
|
|
448
|
+
parts << "-s #{fq(option[:short].delete_prefix("-"))}" if option[:short]
|
|
449
|
+
parts << "-l #{fq(option[:long].delete_prefix("--"))}"
|
|
450
|
+
if option[:choices]
|
|
451
|
+
parts << "-x -a #{fq(option[:choices].map { |c| escape(c) }.join(" "))}"
|
|
452
|
+
elsif option[:path]
|
|
453
|
+
parts << "-r -F"
|
|
454
|
+
elsif option[:value]
|
|
455
|
+
parts << "-x"
|
|
456
|
+
end
|
|
457
|
+
" #{parts.join(" ")}"
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def fish_desc(desc) = desc.to_s.empty? ? "" : " -d #{fq(desc.to_s.lines.first.chomp)}"
|
|
461
|
+
|
|
462
|
+
def indent(lines, spaces) = lines.flat_map { |l| l.split("\n") }.map { |l| (" " * spaces) + l }.join("\n")
|
|
463
|
+
|
|
464
|
+
SAFE = %r{\A[\w.,:@%+=/-]+\z}
|
|
465
|
+
|
|
466
|
+
# A word for bash and zsh: bare when safe, else single-quoted.
|
|
467
|
+
def sq(text)
|
|
468
|
+
text = text.to_s
|
|
469
|
+
text.match?(SAFE) ? text : "'#{text.gsub("'") { "'\\''" }}'"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# A word for fish: bare when safe, else single-quoted (where \\ and \' are the only escapes).
|
|
473
|
+
def fq(text)
|
|
474
|
+
text = text.to_s
|
|
475
|
+
text.match?(SAFE) ? text : "'#{text.gsub(/[\\']/) { "\\#{::Regexp.last_match(0)}" }}'"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# A word as it must appear in a list the shell expands again (compgen -W, complete -a).
|
|
479
|
+
def escape(word) = word.to_s.gsub(%r{[^\w.,:@%+=/-]}) { "\\#{::Regexp.last_match(0)}" }
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
extension :completion do
|
|
485
|
+
dsl(:command) do
|
|
486
|
+
# On the root: adds `completion <bash|zsh|fish>`.
|
|
487
|
+
def completion
|
|
488
|
+
raise ArgumentError, "completion goes on the root command, not #{definition.full_name}" unless definition.root?
|
|
489
|
+
|
|
490
|
+
declare(:completion, true)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
setup do
|
|
495
|
+
if declared(:completion).any?
|
|
496
|
+
tool = definition.name
|
|
497
|
+
command :completion, "Print a shell completion script" do
|
|
498
|
+
description "Print a shell completion script for #{tool}: it completes commands, aliases, options and their choices."
|
|
499
|
+
declare(:completion_command, true)
|
|
500
|
+
argument :shell, desc: "bash, zsh or fish"
|
|
501
|
+
run do
|
|
502
|
+
kind = args[:shell]
|
|
503
|
+
usage_error!(Ext::Completion.unknown(kind)) unless Ext::Completion::SHELLS.key?(kind)
|
|
504
|
+
|
|
505
|
+
shell.print(Ext::Completion.script(program, kind))
|
|
506
|
+
if shell.live?
|
|
507
|
+
line = format(Ext::Completion::SETUP.fetch(kind).first, program.name)
|
|
508
|
+
shell.err_puts("#{shell.symbol(:info)} Load it with #{shell.paint(line, :accent)} #{shell.paint("(#{program.name} completion --help)", :muted)}")
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
help_section do |command, shell|
|
|
516
|
+
["Setup", Ext::Completion.setup_lines(shell, command.root.name)] if command.declared(:completion_command).any?
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|