typr 1.9.3 → 1.9.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdabc3f928eb42904b0f1cd0778384bca290ae56dfbb050236cda6a6578d19e8
4
- data.tar.gz: 236970a28d8cd9265651985504a60a8113cdfaf2b8378303388c0a9e4883469e
3
+ metadata.gz: 161bd96f71319182c99bb701e88d300079e95bcfd6c3b321bf5c881a3ecac8b8
4
+ data.tar.gz: 7b90f171d5198a35920fff37c63106f1ab9741951e808484b33a51ebc9f692f5
5
5
  SHA512:
6
- metadata.gz: 82420afecbc67164d9271e315cd571f33898f4f4ae4ee4186da231cbb6d461df5714180e47c6237d7a54224fd0742777084d44c38e221931f763418bdc19df7d
7
- data.tar.gz: cafd2cf36cdbf58e70249042e7952ca1048325b61ae404b0c4a154eabcb9684cbe8bf75b3aaee239d0c169da7057826a1b7e1bcae1e6dd2b81fa9145942424d9
6
+ metadata.gz: 9655c6b636981a7577ab9e82c0af6fdd971cec905bcf47ca9e627812b09d637f32df4b708b11f9826e0dc1134c592af2ac1e5fffeb33022ba24c9b8a85669dd1
7
+ data.tar.gz: e0fb7b084fc514c7f8ad4107e0085e976c8558b98576b97343ddbb278b19b44ac739f40ca1779dbff220c8fd50e1d7e3dc24ca14a66d19630ec2a4acadaf67bc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [vx.y.z] — __DATE__
4
4
 
5
+ ## [v1.9.4] — 2026-10-30
6
+
7
+ ### Added
8
+ - Emoji sequence widths stop lying per terminal: `Typr.emoji_width` matches `$TERM` against a measured bare-name table (`kitty`/`ghostty`/`foot`/`wezterm`/`konsole`/`alacritty` styles) and every display-width measurement (`real_size`, `clip`, `text_width`, `slice_width`, `Text` wrapping) passes the mode to `unicode-display_width`, so filename columns no longer book the cells the terminal never prints (e.g. `🧟‍♀️🧟‍♂️` measured 31 against a 27-cell render). Unmatched terminals keep `:auto`, the library's own detection
9
+
10
+ ### Fixed
11
+ - Bash completion candidates that carry descriptions (`--opt (what it does)`) show the description in the pick grid but insert only the option — the word clips at the tab or two-space separator, filenames with single spaces stay intact
12
+
13
+ ### Changed
14
+ - `Typr.read_line` gains `prefixes:` — command-start characters stripped from the query before completion matching; the candidates are then looked up through `bash -ic` (commands, options, files) when `completions:` is nil, so the completion engine is no longer `Line#cli` private glue and any prompt can carry it
15
+ - `Line#cli` is gone — `ask`'s `[:line, opts]` question forwards its options (`initial`, `redraw`, `history`, `prefixes`, `regex`, `completions`) straight to `Typr.read_line`
16
+
5
17
  ## [v1.9.3] — 2026-10-19
6
18
 
7
19
  ### Fixed
data/README.md CHANGED
@@ -31,9 +31,9 @@ The default terminal frontend is pure ANSI on raw TTY input — **no ncurses**.
31
31
 
32
32
  ### Colors
33
33
 
34
- Named colors (basic, shades, and `dark_*`), grey shades `grey0`–`grey100`, the terminal's 8-bit palette, and RGB triplets for true-color terminals. Every component takes a per-part `colors:` hash (columns, headers, hints, search matches, …). A component-level `regex:` key maps `Regexp => color` to tint typed input (`Typr.read_line`/`Line#cli`) and the rows of `Typr::Text` areas, or names a scheme — `:generic` or `:shell`; when two patterns match the same span the earlier key wins, so order overlapping patterns longest-first. Colors may be any value `color_code` accepts — named color, `greyN`, an 8-bit index, an RGB triplet like `[10, 200, 30]`, or a `[fg, bg]` pair. Header bars render bold by default.
34
+ Named colors (basic, shades, and `dark_*`), grey shades `grey0`–`grey100`, the terminal's 8-bit palette, and RGB triplets for true-color terminals. Every component takes a per-part `colors:` hash (columns, headers, hints, search matches, …). A component-level `regex:` key maps `Regexp => color` to tint typed input (`Typr.read_line`) and the rows of `Typr::Text` areas, or names a scheme — `:generic` or `:shell`; when two patterns match the same span the earlier key wins, so order overlapping patterns longest-first. Colors may be any value `color_code` accepts — named color, `greyN`, an 8-bit index, an RGB triplet like `[10, 200, 30]`, or a `[fg, bg]` pair. Header bars render bold by default.
35
35
 
36
- Pass `regex: :generic` and `Typr::Text` paints numbers cyan, colon-separated keys yellow, quoted strings green, bracketed groups magenta, and remaining punctuation grey — override with a custom map, disable with `regex: {}` (the default: unpainted). Each painted span restores only the `default` foreground, so the row's own background (alternating stripes, search highlight, padding) survives; `Line#cli` colors the typed command with the `:shell` scheme (first word red, `--options` blue) when configured; it no longer tints the completion grid. Set `regex: {}` to turn a scheme off.
36
+ Pass `regex: :generic` and `Typr::Text` paints numbers cyan, colon-separated keys yellow, quoted strings green, bracketed groups magenta, and remaining punctuation grey — override with a custom map, disable with `regex: {}` (the default: unpainted). Each painted span restores only the `default` foreground, so the row's own background (alternating stripes, search highlight, padding) survives; `Typr.read_line` colors typed command lines with the `:shell` scheme (first word red, `--options` blue) when configured; it no longer tints the completion grid. Set `regex: {}` to turn a scheme off.
37
37
 
38
38
  ### Frontends
39
39
 
@@ -197,15 +197,15 @@ Typr.read_line "/", completions: ->(query, cursor) {
197
197
  Strict question types:
198
198
  - `Array` — pick a row from an object/column (hint-driven)
199
199
  - `:key` — read a single key
200
- - `:line` — read a line of text
200
+ - `[:line, opts]` — read a line of text, opts forwarded to `Typr.read_line`
201
201
 
202
202
  The question and answer are rendered in `@colors[:question]`/`@colors[:answer]`.
203
203
 
204
- ### `Line#cli`
205
- Prompts for a single command line with TAB completion matched against the shell (bash commands, options, and files). `initial:` seeds the line; `history:` recalls previous commands with Up/Down; `prefixes:` lists command-start characters stripped before matching; `redraw:` repaints while the completion grid is up. With `regex: :shell` the typed command tints red for the first word, blue for `--options`. Completion candidates are plain strings.
204
+ ### `Typr.read_line`
205
+ Prompts for a line of text with optional TAB completion. `initial:` seeds the line; `history:` recalls previous input with Up/Down; `completions:` takes a callable `(prefix) → candidates` and replaces the current word with the picked one; `prefixes:` lists command-start characters that are stripped from the word before matching, and when set with `completions:` nil the candidates come from the shell (`bash -ic`, commands, options, and files — also used by filecon's prompts); `redraw:` repaints while the completion grid is up. With `regex: :shell` the typed command tints red for the first word, blue for `--options`. Completion candidates are plain strings.
206
206
 
207
207
  ```ruby
208
- line.cli "> ", initial: "ls ", history: @recent
208
+ Typr.read_line "> ", initial: "ls ", history: @recent, prefixes: "|+=!"
209
209
  ```
210
210
 
211
211
  ### Layout properties (all components)
data/lib/line.rb CHANGED
@@ -108,7 +108,9 @@ class Line < Space
108
108
  # editor via Typr.read_line prefilled with +initial+ instead
109
109
  # of a widget pick.
110
110
  # :key - a single keypress via Typr.read_key
111
- # :line - interactive line editor via Typr.read_line at the cursor
111
+ # :line - interactive line editor via Typr.read_line at the cursor;
112
+ # [:line, opts] forwards opts (initial, redraw, history, prefixes,
113
+ # regex, completions) to read_line
112
114
  #
113
115
  # Every entry — including a trailing +with:+ — is simply another
114
116
  # question in order (a +with:+ entry is labeled "with" when drawn), so
@@ -146,9 +148,8 @@ class Line < Space
146
148
  case object
147
149
  when Array
148
150
  if object.first == :line
149
- opts = object[1] || {}
150
151
  answer << Typr.read_line(colored + color_code(@colors[:answer]),
151
- left: left, top: top, initial: opts[:initial] || "")
152
+ left: left, top: top, **(object[1] || {}))
152
153
  else
153
154
  answer << object.first.pick(object[1], **(object[3] || {}))
154
155
  end
@@ -170,65 +171,6 @@ class Line < Space
170
171
  return ( answer.one? ? answer.first : answer )
171
172
  end
172
173
 
173
- ##
174
- # Prompt for a single command line at the cursor: +prompt+ rendered in
175
- # the line's default color, typed input in the answer color. TAB
176
- # matches the current word against the shell (commands, options, files)
177
- # and replaces it through the pick grid. Regions of the typed input
178
- # matching @colors[:regex] are colored as they are typed (with the
179
- # :shell scheme: the first word red, double-dash options blue).
180
- #
181
- # +history+ is an Array whose entries Up/Down walk; +prefixes+ lists
182
- # command-start characters stripped before completion matching;
183
- # +redraw+ repaints the caller's screen after a completion pick;
184
- # +regex+ overrides @colors[:regex] for this prompt.
185
- #
186
- # line.cli "> ", initial: "cp %name"
187
- # #=> "cp source.txt" (or nil on Escape)
188
- # line.cli "> ", history: @recent, prefixes: "$>"
189
- #
190
- # @param prompt [String] rendered before the typed input
191
- # @param initial [String] prefilled input
192
- # @return [String, nil] the accepted command, or nil on Escape
193
-
194
- def cli prompt, initial: '', redraw: nil, history: nil, prefixes: nil, regex: nil
195
- re = /\A[#{Regexp.escape(prefixes || '|+=!')}]/
196
- source = ->(query) {
197
- bash_candidates(query.sub(re, '')).split("\n").reject(&:empty?).uniq }
198
- colored = color_code(@colors[:default]).to_s + prompt + color_code(@colors[:answer])
199
- Typr.read_line(colored, left: left, top: top, initial: initial,
200
- completions: source, redraw: redraw, regex: regex || @colors[:regex], history: history)
201
- end
202
-
203
- private
204
-
205
- def bash_candidates part
206
- IO.popen([{ 'COMP_LINE' => part, 'COMP_POINT' => part.bytesize.to_s },
207
- 'bash', '-ic', <<~SH], err: '/dev/null', &:read)
208
- set +m; shopt -s expand_aliases
209
- export PS1= PS2= PS3= PS4= PROMPT_COMMAND=
210
- read -r -a COMP_WORDS <<< "$COMP_LINE"
211
- [ "$COMP_LINE" != "${COMP_LINE% }" ] && COMP_WORDS+=( "" )
212
- COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
213
- [ "$COMP_CWORD" -lt 0 ] && COMP_CWORD=0
214
- cur=${COMP_WORDS[COMP_CWORD]}
215
- if declare -F __load_completion >/dev/null 2>&1; then
216
- __load_completion "${COMP_WORDS[0]}" 2>/dev/null
217
- elif declare -F _completion_loader >/dev/null 2>&1; then
218
- _completion_loader "${COMP_WORDS[0]}" 2>/dev/null
219
- fi
220
- Fn=$(complete -p "${COMP_WORDS[0]}" 2>/dev/null | awk '{print $(NF-1)}')
221
- type "$Fn" >/dev/null 2>&1 && "$Fn" "${COMP_WORDS[0]}" "$cur" 2>/dev/null
222
- if [ -z "${COMPREPLY[*]}" ]; then
223
- if [ "$COMP_CWORD" -eq 0 ]; then compgen -A command -A alias -A file -- "$cur"
224
- else compgen -A file -- "$cur"; fi
225
- else printf '%s\n' "${COMPREPLY[@]}"
226
- fi
227
- SH
228
- end
229
-
230
- public
231
-
232
174
  ##
233
175
  # Construct a Line. Position via the {Space} boundaries; +default+ sets
234
176
  # the text shown after {#reset}, +bindings+ the keybinding labels.
data/lib/terminal.rb CHANGED
@@ -179,7 +179,7 @@ module Typr
179
179
  next
180
180
  end
181
181
  break if used >= max
182
- size = Unicode::DisplayWidth.of token
182
+ size = Unicode::DisplayWidth.of token, emoji: emoji_width
183
183
  if used + size <= max
184
184
  kept << token
185
185
  used += size
@@ -190,7 +190,7 @@ module Typr
190
190
  width = 0
191
191
  out = ""
192
192
  clusters.each do |cluster|
193
- width += Unicode::DisplayWidth.of cluster
193
+ width += Unicode::DisplayWidth.of cluster, emoji: emoji_width
194
194
  break if width > need
195
195
  out << cluster
196
196
  end
@@ -248,7 +248,38 @@ module Typr
248
248
  # @param str [String] text possibly containing ANSI escapes
249
249
  # @return [Integer] width in cells
250
250
 
251
- def real_size str; Unicode::DisplayWidth.of( sanitize(str) ) end
251
+ def real_size str
252
+ Unicode::DisplayWidth.of sanitize(str), emoji: emoji_width
253
+ end
254
+
255
+ ##
256
+ # Emoji sequence cell widths, matched bare-name into $TERM (first hit).
257
+ # Emoji ligatures render differently per terminal, so one mode never
258
+ # fits them all: kitty/ghostty/foot ligate emoji ZWJ sequences to 2
259
+ # cells, and wezterm/konsole keep their ligating quirk modes. Terminal
260
+ # probe data — counting the true cursor positions after rendering —
261
+ # showed alacritty among the unlisted ones: it splits zombies with
262
+ # unwidened ♀/♂ signs, so its render coincides with the library's own
263
+ # per-codepoint count (:none under :auto) and needs no entry. Falls
264
+ # back to :auto, the unicode-display_width library's own
265
+ # detection — which for unknown terminals is a per-codepoint count that
266
+ # matches the tofu/lower-fidelity rendering of e.g. urxvt and xterm.
267
+ # Measured with cursor reports: render the sequence, ask the cursor
268
+ # where it ended up.
269
+ #
270
+ # Typr.emoji_width # the running terminal's mode
271
+ # Typr.emoji_width 'xterm-kitty' #=> :rgi
272
+ # Typr.emoji_width 'vt102' #=> :auto
273
+ #
274
+ # @param term [String] terminal name, $TERM by default
275
+ # @return [Symbol] +:rgi+, +:all+, +:all_no_vs16+, or +:auto+
276
+
277
+ def emoji_width term = ENV["TERM"].to_s
278
+ EMOJI.find { |matcher, _| matcher === term }&.last || :auto
279
+ end
280
+
281
+ module_function :emoji_width
282
+ public :emoji_width
252
283
 
253
284
  ##
254
285
  # Coerce a string into Integer, Float, or Boolean when it matches those
@@ -378,6 +409,16 @@ module Typr
378
409
  }
379
410
  }
380
411
 
412
+ # Unicode::DisplayWidth emoji measurement modes by bare terminal-name
413
+ # match against $TERM (see Typr.emoji_width for the details).
414
+ EMOJI = {
415
+ /kitty/ => :rgi,
416
+ /ghostty/ => :rgi,
417
+ /foot/ => :rgi,
418
+ /wezterm/ => :all_no_vs16,
419
+ /konsole/ => :all
420
+ }
421
+
381
422
  # Returns +str+ with every region matched by the keys of +regex+ wrapped in
382
423
  # the associated color, restoring +base+ after each region. Non-matching
383
424
  # text passes through untouched. +regex+ may be nil or empty (no-op), and
@@ -658,14 +699,24 @@ module Typr
658
699
  # @param left [Integer] screen column (0-based)
659
700
  # @param top [Integer] screen row (0-based)
660
701
  # @param initial [String] prefilled query
661
- # @param completions [#call, nil] +->(prefix) { candidates }+ or nil
702
+ # @param completions [#call, nil] +->(prefix) { candidates }+ or nil;
703
+ # a candidate may carry a description after a tab or run of spaces —
704
+ # shown in the pick grid, stripped from the inserted word
662
705
  # @param redraw [#call, nil] zero-arg repaint after a completion pick
663
706
  # @param regex [Hash, Symbol, nil] paint map or scheme (see {.paint})
664
707
  # @param history [Array<String>, nil] previous queries for Up/Down
708
+ # @param prefixes [String, nil] command-prefix characters stripped from the
709
+ # query before completion lookup; when set with +completions+ nil, TAB
710
+ # completes through +bash -ic+ (commands, options, files)
665
711
  # @param block [Proc] +->(key, query, cursor)+ inspector (non-nil short-circuits)
666
712
  # @return [Object, nil] the query, a block short-circuit, or nil on Escape
667
713
 
668
- def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, &block
714
+ def self.read_line prompt='', left: 0, top: 0, initial: '', completions: nil, redraw: nil, regex: nil, history: nil, prefixes: nil, &block
715
+ if completions.nil? && prefixes
716
+ pre = /\A[#{Regexp.escape(prefixes)}]/
717
+ completions = ->(query) {
718
+ bash_candidates(query.sub(pre, '')).split("\n").reject(&:empty?).uniq }
719
+ end
669
720
  return $stdin.gets&.chomp unless $stdin.tty?
670
721
  query, cursor = initial.dup, initial.length
671
722
  hist_idx = nil
@@ -751,8 +802,9 @@ module Typr
751
802
  if picked.is_a?(Integer)
752
803
  word_start = cursor
753
804
  word_start -= 1 while word_start > 0 and query[word_start - 1] != ' '
754
- query[word_start...cursor] = input[picked]
755
- cursor = word_start + input[picked].length
805
+ word = input[picked].split(/\s{2,}|\t/, 2).first.to_s
806
+ query[word_start...cursor] = word
807
+ cursor = word_start + word.length
756
808
  end
757
809
  end
758
810
  end
@@ -762,6 +814,40 @@ module Typr
762
814
  end
763
815
  end
764
816
 
817
+ # Completion candidates for +part+ through an interactive bash: loads the
818
+ # command's completion function, falls back to commands/aliases/files at
819
+ # the first word and files after it. Used by {.read_line} when +prefixes+
820
+ # is set.
821
+ #
822
+ # @param part [String] the query with any command-prefix already stripped
823
+ # @return [String] candidates, one per line
824
+
825
+ def self.bash_candidates part
826
+ IO.popen([{ 'COMP_LINE' => part, 'COMP_POINT' => part.bytesize.to_s },
827
+ 'bash', '-ic', <<~SH], err: '/dev/null', &:read)
828
+ set +m; shopt -s expand_aliases
829
+ export PS1= PS2= PS3= PS4= PROMPT_COMMAND=
830
+ read -r -a COMP_WORDS <<< "$COMP_LINE"
831
+ [ "$COMP_LINE" != "${COMP_LINE% }" ] && COMP_WORDS+=( "" )
832
+ COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
833
+ [ "$COMP_CWORD" -lt 0 ] && COMP_CWORD=0
834
+ cur=${COMP_WORDS[COMP_CWORD]}
835
+ if declare -F __load_completion >/dev/null 2>&1; then
836
+ __load_completion "${COMP_WORDS[0]}" 2>/dev/null
837
+ elif declare -F _completion_loader >/dev/null 2>&1; then
838
+ _completion_loader "${COMP_WORDS[0]}" 2>/dev/null
839
+ fi
840
+ Fn=$(complete -p "${COMP_WORDS[0]}" 2>/dev/null | awk '{print $(NF-1)}')
841
+ type "$Fn" >/dev/null 2>&1 && "$Fn" "${COMP_WORDS[0]}" "$cur" 2>/dev/null
842
+ if [ -z "${COMPREPLY[*]}" ]; then
843
+ if [ "$COMP_CWORD" -eq 0 ]; then compgen -A command -A alias -A file -- "$cur"
844
+ else compgen -A file -- "$cur"; fi
845
+ else printf '%s\n' "${COMPREPLY[@]}"
846
+ fi
847
+ SH
848
+ end
849
+ private_class_method :bash_candidates
850
+
765
851
  ##
766
852
  # Display width of +str+ after stripping ANSI escapes.
767
853
  #
@@ -771,7 +857,7 @@ module Typr
771
857
  # @return [Integer] width in cells
772
858
 
773
859
  def self.text_width str
774
- Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
860
+ Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, ''), emoji: emoji_width )
775
861
  end
776
862
 
777
863
  ##
@@ -797,7 +883,7 @@ module Typr
797
883
  i += seq ? seq.length : 1
798
884
  next
799
885
  end
800
- cell = Unicode::DisplayWidth.of(str[i])
886
+ cell = Unicode::DisplayWidth.of(str[i], emoji: emoji_width)
801
887
  vis += cell
802
888
  slice << str[i] if vis > offset and vis <= offset + width
803
889
  i += 1
data/lib/text.rb CHANGED
@@ -71,7 +71,7 @@ module Typr
71
71
  elsif c == ?\n
72
72
  added += 1; break
73
73
  else
74
- cw = Unicode::DisplayWidth.of(c)
74
+ cw = Unicode::DisplayWidth.of(c, emoji: emoji_width)
75
75
  break if cells + cw > cap
76
76
  cells += cw; added += 1
77
77
  end
data/typr.gemspec CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "typr"
5
- s.version = "1.9.3"
6
- s.date = "2026-10-19"
5
+ s.version = "1.9.4"
6
+ s.date = "2026-10-30"
7
7
  s.homepage = "https://codeberg.org/typr/typr"
8
8
  s.licenses = ["MIT"]
9
9
  s.require_paths = ["lib"]
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.3
4
+ version: 1.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kilian Reitmayr
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-19 00:00:00.000000000 Z
10
+ date: 2026-10-30 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: unicode-display_width