typr 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdcaef4b1bc2c40e6fab6bd7d1462ddf19aeca03babcdbb9be74629551230647
4
- data.tar.gz: b4e927b5237f27e2366ed15057054b33d9f070ef5a50c234533699182c7228ee
3
+ metadata.gz: 65f06238b297e2c6f925f139336c89b5606e23c7a08a6fb94ed82848490e9e71
4
+ data.tar.gz: d958ec7e5f7cd5857c2b559b3e32327b590652c8fb759f0557effb33d8c27551
5
5
  SHA512:
6
- metadata.gz: 889fd403e345e89dc6777c02c6736d0865b7266b16157e13e4cf2469b9a3664d68f4fe672d1f9b5b6d72316d697c2c8e20e1f920853ae495014c9fb049fed6cd
7
- data.tar.gz: 662df0d3ef61d964cd8a03a1cd1f7caa7fb14a15d87c22afa3a2cdf9115f4780b944446ebcdc9f538c33ee80753e48c362fea7f716d2ef7fca2070f07fe5483f
6
+ metadata.gz: 8d651d702b4e5d019636814a021ef4ab62f300b841f482de5ed4aac8f3cc3fe636802ac34551b107bfaef2c19723805e7742fafa95ada76cb6c20f91066e101b
7
+ data.tar.gz: 723ef1823511febee158925266361dbe0afda106ad3974fc1f75edd83f31e5a2ed6be272e634762bbf6797df41e6e27d24c908fe48902892fcf1c34271f1d6d1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [v1.4.0] — 2026-08-15
4
+
5
+ ### Added
6
+ - `Browser#pick :directory` confirms the current directory with Return (clearing filters and sorting first), so callers can accept the directory itself; the `pick` type filter only applies to directory and MIME-type picks — `:file` and custom types like `:cmd` show the full listing
7
+ - `sanitize` terminal helper — strips ANSI escape sequences and control characters (except `\n`, `\t`) from shell-generated text
8
+ - Alt+digit hint selection — pressing Alt + a hint digit (sent as `ESC` + digit) selects that hint row and sets a `modifier` accessor on the picker (`:alt`), so callers can detect the modified selection; `Stack#pick` and `Grid#pick` both reset `modifier` to `nil` at the start of each pick
9
+
10
+ ### Changed
11
+ - `Browser#switch_to` renamed to `change_view` — clearer name for switching column views (callers in `cd` and the `views` popup updated)
12
+ - `read_line` scrolls horizontally instead of wrapping when the query exceeds the terminal width — the cursor stays pinned to the right edge of the line while overflowing
13
+ - `read_line` now shows the terminal cursor while editing and hides it again on exit (previously the cursor stayed invisible after `Typr.init`)
14
+ - `real_size` now measures via `sanitize`, so non-SGR CSI/OSC sequences and control characters count as zero width; the `printables` helper was removed in favor of `sanitize`
15
+ - Sort, filter, and view popups open in `/` search mode — typing narrows the list and a hint digit picks the result; `Line#ask` Array questions can forward pick options as a 4th element (e.g. `{ column: 0 }`)
16
+ - Default hints are digits only (`0-9`) — typing letters in any picker always filters/navigates instead of selecting by hint; explicit `hints:` overrides removed from `Browser`
17
+
18
+ ### Fixed
19
+ - Popup grids keep their height while live-filtering in `/` search mode — the box no longer collapses as rows are filtered out
20
+ - `read_key` assembles split escape sequences — CSI input (cursor keys, SGR mouse) reads on to its final byte, SS3 (`\eO`) completes to three bytes, and a lone `ESC` briefly polls for an Alt+key continuation, so a split key can no longer be mistaken for a plain Escape (which would close a popup or exit)
21
+ - Spec cleanup — merged the duplicated `Text#print` describes, removed the redundant empty-input edge case, dropped stack-spec `#pick` examples that duplicated grid-spec coverage, and updated the browser username specs to stub the passwd lookup (the info bar intentionally shows the real user, not stale `$USER`) plus the grid format-reset spec to assert `@widths` clearing instead of the removed `@maxed` state
22
+
3
23
  ## [v1.3.0] — 2026-08-03
4
24
 
5
25
  ### Added
data/lib/browser.rb CHANGED
@@ -63,33 +63,25 @@ module Typr
63
63
  Dir.chdir( @directory = File.expand_path( dir ) )
64
64
  end
65
65
  data = Dir.new(@directory).children.reject{|f|
66
- !@show_hidden and f[0] == ?. }.map.with_index do |file,id|
67
- if File.symlink? file
68
- target = File.readlink( file ) rescue $!.to_s
69
- next [file, target, "????", false, "?", "?", 0, 0, 0, 0, "inode", "symlink"] unless File.exist?(file)
66
+ !@show_hidden and f[0] == ?. }.map { |file| scan_file file }
67
+
68
+ data.each_with_index{ |row, id|
69
+ file = row[NAME]
70
+ ftype = File.ftype(file) rescue next
71
+ ftype = File.stat(file).ftype if ftype == 'link' and File.exist?(file)
72
+ mime = { 'directory' => 'inode/directory', 'characterSpecial' => 'inode/chardevice',
73
+ 'blockSpecial' => 'inode/blockdevice', 'link' => 'inode/symlink',
74
+ 'fifo' => 'inode/fifo', 'socket' => 'inode/socket' }[ftype]
75
+ unless mime
76
+ if @mimetype_db and ext = file.match(/.+\.(\w+)$/)
77
+ mime = MIMETYPES[ext[1].to_sym]
78
+ end
79
+ missing[file] = id unless mime
70
80
  end
71
- stat = File.stat(file) rescue nil
72
- next [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless stat
73
- type = stat.ftype.to_sym
74
-
75
- mime = { "directory": "inode/directory",
76
- "characterSpecial": "inode/chardevice",
77
- "blockSpecial": "inode/blockdevice", "link": "inode/symlink",
78
- "fifo": "inode/fifo", "socket": "inode/socket"}[type]
79
- if type == :file and @mimetype_db
80
- ext = file.match(/.+\.(\w+)$/)
81
- mime ||= MIMETYPES[ext[1].to_sym] if ext
82
- end
83
-
84
- ( missing[file] = id ; mime = "?/?" ) unless mime
85
- [ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
86
- "rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
87
- File.executable?(file), stat.uid, stat.gid, stat.size,
88
- stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i,
89
- *mime.to_s.split( ?/ ) ]
90
- end
81
+ row[-2,2] = mime.to_s.split( ?/ ) if mime
82
+ }
91
83
 
92
- if @mimetype_magic and not missing.empty?
84
+ if @mimetype_magic and missing.any?
93
85
  begin
94
86
  out = IO.popen(['file', '--mime-type', *missing.keys]){ |io| io.read }
95
87
  rescue Errno::ENOENT
@@ -106,12 +98,33 @@ module Typr
106
98
  filter
107
99
  unless append
108
100
  @directory += ?/ unless @directory == ?/
109
- switch_to @view
101
+ change_view @view
110
102
  reset :position
111
103
  end
112
104
  return
113
105
  end
114
106
 
107
+ ##
108
+ # Build a row for a single directory entry from filesystem metadata only.
109
+ # MIME type detection (extension database and +file --mime-type+) happens
110
+ # in a batch pass inside {#cd}; type/subtype are left as +"?"+ here.
111
+ #
112
+ # browser.scan_file "image.png" # => 12-column row
113
+
114
+ def scan_file file
115
+ target = nil
116
+ if File.symlink? file
117
+ target = File.readlink( file ) rescue $!.to_s
118
+ return [file, target, "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless File.exist?(file)
119
+ end
120
+ stat = File.stat(file) rescue nil
121
+ return [file, "", "????", false, "?", "?", 0, 0, 0, 0, "?", "?"] unless stat
122
+ [ file, target, stat.mode.to_s(8)[-3..-1].chars.map{|d|
123
+ "rwx".chars.map.with_index{|c,i| d.to_i[2-i]==0 ? ?- : c } }.join,
124
+ ( File.executable?(file) and stat.file? ), stat.uid, stat.gid, stat.size,
125
+ stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, "?", "?" ]
126
+ end
127
+
115
128
  ##
116
129
  # Colors symlink names green/red based on target existence and applies
117
130
  # per-MIME-type foreground colors from +@colors[:types]+.
@@ -127,11 +140,11 @@ module Typr
127
140
  ##
128
141
  # Switch to a named or indexed column view. Resets format widths.
129
142
  #
130
- # browser.switch_to :full # all columns
131
- # browser.switch_to 0 # first view key
132
- # browser.switch_to :stats # name, size, perms, owner, group, modified
143
+ # browser.change_view :full # all columns
144
+ # browser.change_view 0 # first view key
145
+ # browser.change_view :stats # name, size, perms, owner, group, modified
133
146
 
134
- def switch_to view=nil
147
+ def change_view view=nil
135
148
  return unless view
136
149
  view = @views.keys[ view ] if view.is_a? Integer
137
150
  @sequence = @views[ view ].map{|title| @header.index title.to_s}
@@ -227,7 +240,7 @@ module Typr
227
240
  # browser.filter_view # prompts for filter text
228
241
 
229
242
  def filter_view; add_filter *@user.ask(
230
- filter: [ popup( type: :"[f]ilter" ), :row, NAME], with: :line ) end
243
+ filter: [ popup( type: :"[f]ilter" ), :row, NAME, { column: 0 }], with: :line ) end
231
244
 
232
245
  ## Open live `/` search filtered on the name column.
233
246
  #
@@ -243,13 +256,13 @@ module Typr
243
256
  #
244
257
  # browser.sort_view # pick a column header to sort
245
258
 
246
- def sort_view; sort_by @user.ask( "sort by": [ popup( type: :"[s]ort" ), :row] ) end
259
+ def sort_view; sort_by @user.ask( "sort by": [ popup( type: :"[s]ort" ), :row, nil, { column: 0 }] ) end
247
260
 
248
261
  ## Open a view-picker popup and switch to the selected view.
249
262
  #
250
263
  # browser.views # choose :simple, :compact, :stats, or :full
251
264
 
252
- def views; switch_to( popup( type: :"[v]iews", input: @views.keys ).pick :row ) end
265
+ def views; change_view( popup( type: :"[v]iews", input: @views.keys ).pick( :row, column: 0 ) ) end
253
266
 
254
267
  ## True when the row is a directory or passes base Grid filtering.
255
268
  #
@@ -273,16 +286,22 @@ module Typr
273
286
  type ||= 'file'
274
287
  type = type.to_s
275
288
  return super(type, **kw) if type[/row|column|field|none/]
276
- add_filter ( %w[audio video image text application inode].include?(
277
- type) ? TYPE : SUBTYPE ), type if type != 'file'
289
+ types = %w[audio video image text application inode]
290
+ add_filter( types.include?( type ) ? TYPE : SUBTYPE, type ) if type == 'directory' or types.include?( type )
278
291
  loop do
279
292
  show
280
293
  draw_hints :row
281
294
  key = Typr.read_key
282
295
  if key == @keymap[:exit]
283
- reset :display
296
+ @filters.clear
297
+ sort
284
298
  return
285
299
  end
300
+ if key == @keymap[:confirm] and type == 'directory'
301
+ @filters.clear
302
+ sort
303
+ return @directory
304
+ end
286
305
  if key.is_a?(Typr::Mouse)
287
306
  if key.wheel? and key.press?
288
307
  send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
@@ -298,7 +317,8 @@ module Typr
298
317
  if file[SUBTYPE] == 'directory'; cd file[TARGET] || file[NAME]; next
299
318
  else choice = file[NAME] end
300
319
  end
301
- reset :display
320
+ @filters.clear
321
+ sort
302
322
  return choice
303
323
  end
304
324
  end
data/lib/graphical.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'sdl2'
1
+ require 'sdl3'
2
2
  require_relative 'frontend'
3
3
 
4
4
  module Visuals
data/lib/grid.rb CHANGED
@@ -262,12 +262,18 @@ module Typr
262
262
  @filters = [[column, '']]
263
263
  @map = base.select{ |id| check id }
264
264
  @start = 0
265
+ @bottom ||= bottom
265
266
  show
266
267
  draw_hints
268
+ @modifier = nil
267
269
  result = Typr.read_line '/', left: left, top: self.bottom + 1,
268
270
  &->(key, query, _) {
269
271
  if key.is_a?(String) and idx = @hints[0..height-@hints_start-1].index(key)
270
272
  return page.to_a[idx + @hints_start]
273
+ elsif key.is_a?(String) and digit = key[/\A\e(\d)\z/, 1] and
274
+ idx = @hints[0..height-@hints_start-1].index(digit)
275
+ @modifier = :alt
276
+ return page.to_a[idx + @hints_start]
271
277
  end
272
278
  if key.is_a?(String) and !key.each_char.all?{ |c| c.ord.between?(32, 126) } and
273
279
  @keymap.values.include?(key) and ![KEY_BACKSPACE, "\b"].include?(key)
@@ -406,7 +412,8 @@ module Typr
406
412
  header = ( row == :header )
407
413
  return draw @header[0..width-1].ljust(width) if
408
414
  header and @header.is_a? String
409
- fields = (header ? @header : @data[row] ).dup
415
+ return unless row_data = header ? @header : @data[row]
416
+ fields = row_data.dup
410
417
  @layer[row].each{ |col,value| fields[col] = value } if
411
418
  @layer[row] if @layer unless header
412
419
  for col,id in sequence.each_with_index
data/lib/line.rb CHANGED
@@ -64,7 +64,9 @@ class Line < Space
64
64
  #
65
65
  # The +type+ selects how the answer is collected:
66
66
  # Array - interactive pick via <widget>.pick(<type>); an optional third
67
- # element renders the chosen row's field into the line
67
+ # element renders the chosen row's field into the line, and an
68
+ # optional fourth element is passed as pick keyword arguments
69
+ # (e.g. { column: 0 } to open the pick in / search mode)
68
70
  # :key - a single keypress via Typr.read_key
69
71
  # :line - interactive line editor via Typr.read_line at the cursor
70
72
  #
@@ -95,7 +97,7 @@ class Line < Space
95
97
  draw prepare( colored, width, @align, @trim )
96
98
  color( @colors[:answer] )
97
99
  case object
98
- when Array then answer << object.first.pick( object[1] )
100
+ when Array then answer << object.first.pick( object[1], **(object[3] || {}) )
99
101
  when :key then answer << Typr.read_key
100
102
  when :line then answer << Typr.read_line( colored + color_code( @colors[:answer] ),
101
103
  left: left, top: top )
data/lib/stack.rb CHANGED
@@ -21,7 +21,7 @@ module Typr
21
21
  ## +alternate+ toggles striped rows (+:grey10+ background alternating with the default bg).
22
22
 
23
23
  attr_accessor :start, :keymap, :selected, :header
24
- attr_accessor :separator, :hints, :hints_start
24
+ attr_accessor :separator, :hints, :hints_start, :modifier
25
25
 
26
26
  ##
27
27
  # Returns a +Range+ of internal data indices visible on the current page.
@@ -213,6 +213,7 @@ module Typr
213
213
  def pick type = :row, row=0 #, key=nil
214
214
  type = type.to_s
215
215
  multiple = type[-1] == ?s
216
+ @modifier = nil
216
217
  loop do
217
218
  if type['field']
218
219
  row = pick( :row ) or return
@@ -237,6 +238,13 @@ module Typr
237
238
  relative = value + @start if type['relative_']
238
239
  if type['row'] then value = page.to_a[ value ]
239
240
  elsif type['column'] and @sequence; value = @sequence[value] end
241
+ elsif key.is_a?(String) and digit = key[/\A\e(\d)\z/, 1] and
242
+ value = @hints[0..limit-@hints_start-1].index(digit)
243
+ @modifier = :alt
244
+ value += @hints_start
245
+ relative = value + @start if type['relative_']
246
+ if type['row'] then value = page.to_a[ value ]
247
+ elsif type['column'] and @sequence; value = @sequence[value] end
240
248
  end unless type['none']
241
249
  end
242
250
 
@@ -294,7 +302,7 @@ module Typr
294
302
  @keymap = { cycle: KEY_TAB, confirm: KEY_RETURN, exit: KEY_ESCAPE,
295
303
  page_down: KEY_PAGEDOWN, page_up: KEY_PAGEUP, up: KEY_UP,
296
304
  down: KEY_DOWN, to_top: KEY_HOME, to_bottom: KEY_END }.merge @keymap
297
- @hints ||= "1234567890qwertyuiopasdfghjklzxcvbnm-=[];'\,./"
305
+ @hints ||= "1234567890"
298
306
  end
299
307
  end
300
308
 
data/lib/terminal.rb CHANGED
@@ -166,11 +166,16 @@ module Typr
166
166
  str[0..-num-1] + chars )
167
167
  end
168
168
 
169
- # Strip ANSI escape sequences from +str+.
170
- def printables str; str.gsub(/\x1b\[[^m]+m|/,'') end
169
+ # Strip ANSI escape sequences and control characters (except \n, \t)
170
+ # from shell-generated text, leaving only printable content.
171
+ def sanitize str
172
+ str.scrub
173
+ .gsub(/\e(?:\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=><0A])/, '')
174
+ .gsub(/[\x00-\x08\x0b-\x1f\x7f]/, '')
175
+ end
171
176
 
172
- # Display width of +str+ after stripping ANSI escapes.
173
- def real_size str; Unicode::DisplayWidth.of( printables(str) ) end
177
+ # Display width of +str+ after stripping ANSI escapes and control chars.
178
+ def real_size str; Unicode::DisplayWidth.of( sanitize(str) ) end
174
179
 
175
180
  # Coerce a string into Integer, Float, or Boolean when it matches those
176
181
  # forms (yes/no, true/false); otherwise return it unchanged.
@@ -250,11 +255,15 @@ module Typr
250
255
  def self.read_key
251
256
  read = ->(tty) do
252
257
  str = tty.sysread 6
253
- if str.start_with?("\e[<")
254
- # SGR mouse reports (CSI < b ; x ; y M/m) can exceed six bytes.
258
+ if str.start_with?("\e[M") and str.size < 6
259
+ str << tty.sysread(6 - str.size) # X10 mouse reports
260
+ elsif str.start_with?("\e[")
261
+ # CSI sequences (arrows, SGR mouse, ...) end with a byte >= 0x40.
255
262
  str << tty.sysread(1) until str[-1].ord >= 0x40
256
- elsif str.start_with?("\e[M") and str.size < 6
257
- str << tty.sysread(6 - str.size)
263
+ elsif str.start_with?("\eO") and str.size < 3
264
+ str << tty.sysread(3 - str.size) # SS3 (application) cursor keys
265
+ elsif str == "\e" and IO.select([tty], nil, nil, 0.03)
266
+ str << tty.read_nonblock(6) rescue nil # alt+key continuation
258
267
  end
259
268
  str
260
269
  end
@@ -304,38 +313,44 @@ module Typr
304
313
  def self.read_line prompt='', left: 0, top: 0, initial: '', &block
305
314
  return $stdin.gets&.chomp unless $stdin.tty?
306
315
  query, cursor = initial.dup, initial.length
307
- loop do
308
- $>.print "\e[%i;%if" % [ top + 1, left + 1 ]
309
- $>.print ERASE_LINE
310
- $>.print prompt + query
311
- $>.print "\e[%i;%if" % [ top + 1, left + 1 +
312
- text_width( prompt + query[0...cursor] ) ]
313
- key = read_key
314
- return nil if key.nil? or key == KEY_ESCAPE
315
- return query if key == KEY_RETURN or key == "\n"
316
- case key
317
- when KEY_LEFT, "\e[D"; cursor -= 1 if cursor > 0
318
- when KEY_RIGHT, "\e[C"; cursor += 1 if cursor < query.length
319
- when "\e[1;5D", "\eOd"; cursor = word_prev query, cursor
320
- when "\e[1;5C", "\eOc"; cursor = word_next query, cursor
321
- when KEY_HOME; cursor = 0
322
- when KEY_END; cursor = query.length
323
- when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
324
- when KEY_BACKSPACE, "\b"
325
- if cursor > 0
326
- query.slice!(cursor - 1, 1)
327
- cursor -= 1
328
- end
329
- else
330
- if key.is_a?(String) and key.each_char.all?{ |char| char.ord.between?(32, 126) }
331
- query.insert(cursor, key)
332
- cursor += key.length
333
- end
316
+ $>.print CURSOR_NORMAL
317
+ begin
318
+ loop do
319
+ $>.print "\e[%i;%if" % [ top + 1, left + 1 ]
320
+ $>.print ERASE_LINE
321
+ before = text_width( prompt + query[0...cursor] )
322
+ offset = [ left + before - Typr.width, 0 ].max
323
+ $>.print slice_width( prompt + query, offset, Typr.width - left + 1 )
324
+ $>.print "\e[%i;%if" % [ top + 1, left + 1 + before - offset ]
325
+ key = read_key
326
+ return nil if key.nil? or key == KEY_ESCAPE
327
+ return query if key == KEY_RETURN or key == "\n"
328
+ case key
329
+ when KEY_LEFT, "\e[D"; cursor -= 1 if cursor > 0
330
+ when KEY_RIGHT, "\e[C"; cursor += 1 if cursor < query.length
331
+ when "\e[1;5D", "\eOd"; cursor = word_prev query, cursor
332
+ when "\e[1;5C", "\eOc"; cursor = word_next query, cursor
333
+ when KEY_HOME; cursor = 0
334
+ when KEY_END; cursor = query.length
335
+ when KEY_DC, "\e[3~"; query.slice!(cursor, 1) if cursor < query.length
336
+ when KEY_BACKSPACE, "\b"
337
+ if cursor > 0
338
+ query.slice!(cursor - 1, 1)
339
+ cursor -= 1
340
+ end
341
+ else
342
+ if key.is_a?(String) and key.each_char.all?{ |char| char.ord.between?(32, 126) }
343
+ query.insert(cursor, key)
344
+ cursor += key.length
345
+ end
346
+ end
347
+ cursor = 0 if cursor < 0
348
+ cursor = query.length if cursor > query.length
349
+ result = block.call( key, query, cursor ) if block
350
+ return result unless result.nil?
334
351
  end
335
- cursor = 0 if cursor < 0
336
- cursor = query.length if cursor > query.length
337
- result = block.call( key, query, cursor ) if block
338
- return result unless result.nil?
352
+ ensure
353
+ $>.print CURSOR_INVISIBLE
339
354
  end
340
355
  end
341
356
 
@@ -344,6 +359,28 @@ module Typr
344
359
  Unicode::DisplayWidth.of( str.gsub(/\x1b\[[^m]+m/, '') )
345
360
  end
346
361
 
362
+ # Visible substring of +str+ starting at display-width +offset+, at most
363
+ # +width+ cells wide. ANSI escapes are zero-width and carried through so
364
+ # color state applies inside the window.
365
+ def self.slice_width str, offset, width
366
+ vis = 0
367
+ slice = +""
368
+ i = 0
369
+ while i < str.length
370
+ if str[i] == "\e"
371
+ seq = str[i..][/\A\e(\[[0-9;?]*[ -\/]*[@-~]|\][^\a\e]*(?:\a|\e\\)|[()=>0-9])/]
372
+ slice << seq if seq
373
+ i += seq ? seq.length : 1
374
+ next
375
+ end
376
+ cell = Unicode::DisplayWidth.of(str[i])
377
+ vis += cell
378
+ slice << str[i] if vis > offset and vis <= offset + width
379
+ i += 1
380
+ end
381
+ slice
382
+ end
383
+
347
384
  # Move the cursor back to the start of the previous word in +str+.
348
385
  def self.word_prev str, cursor
349
386
  cursor -= 1 while cursor > 0 and str[cursor - 1] == ' '
data/typr.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "typr"
5
- s.version = "1.3.0"
5
+ s.version = "1.4.0"
6
6
  s.date = "2026-07-25"
7
7
  s.homepage = "https://codeberg.org/typr/typr"
8
8
  s.licenses = ["MIT"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kilian Reitmayr