class String

def truncate_bytes(truncate_at, omission: "…")

to "…", for a total length not exceeding bytesize.
The truncated text ends with the :omission string, defaulting

=> "🔪🔪🔪🔪…"
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
=> 80
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
=> 20
>> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size

characters.
grapheme clusters ("perceptual characters") by truncating at combining
breaking string encoding by splitting multibyte characters or breaking
Truncates +text+ to at most bytesize bytes in length without
def truncate_bytes(truncate_at, omission: "…")
  omission ||= ""
  case
  when bytesize <= truncate_at
    dup
  when omission.bytesize > truncate_at
    raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
  when omission.bytesize == truncate_at
    omission.dup
  else
    self.class.new.tap do |cut|
      cut_at = truncate_at - omission.bytesize
      each_grapheme_cluster do |grapheme|
        if cut.bytesize + grapheme.bytesize <= cut_at
          cut << grapheme
        else
          break
        end
      end
      cut << omission
    end
  end
end