tempest-rb 0.0.1 → 0.0.2

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: c5f6d650f85e45e0d4f39f9ace3184141099dfacc7e499144157ccc3e1b9e1fa
4
- data.tar.gz: 0d86da89ce1f2c8335d5a9d62911a8d86bc68e9f5e317e6b884aa898bd69227c
3
+ metadata.gz: e74ff03fab2ee34902b7836bd77f0a41752135cda5aca37ca97570ac5ced79f3
4
+ data.tar.gz: edb35af23affe941bfdd361a04d86e18ded6ac068afd33df4d97d82aa62f445b
5
5
  SHA512:
6
- metadata.gz: cf7a05f136b292d36a01893f7ee83d8362b53ae2391e3dece56ef80b5d24f7dc0070dfadc12f6e49d1615649f69fa6511993cce0e7be272db36f5295cbc71cee
7
- data.tar.gz: 0ee8b681779c0d7d54b51e85f5127c13d32923f90fb7c340e3c48ba49824bfab5a0600e5493523aba93723ad6b849826eaee933df7edd6746a06da6271d99e7a
6
+ metadata.gz: 0453a2009c3bed58216a221aad38a71b5cd97529e5b6a0dc210a9c2b72be3172ffc9ec3593679c577540fa937cd3226940eba11e02e952bf8255d445da1226ba
7
+ data.tar.gz: ef8abc6996dfdc86fb54d3ffdcfc7025c37444042a3e8c3fb3008a87bd3aa613d737f873a999541e73b7393c692569ab7ff3d6e226757618115350c83e66f4c1
data/README.md CHANGED
@@ -62,7 +62,7 @@ The first sign-in may prompt for the email code Bluesky sends as a second factor
62
62
 
63
63
  Anything else you type is sent as a new post.
64
64
 
65
- Each post in the timeline is prefixed with a short `$XX` id, and URLs found inside posts get their own `$LX` ids. Use those ids with `$XX <text>` to reply or `:open $LX` to open a link.
65
+ Each post in the timeline is prefixed with a short `$XX` id, and URLs found inside posts get their own `$LX` ids. Use those ids with `$XX <text>` to reply or `:open $LX` to open a link. Like and repost events show the subject post's `$XX` id in trailing brackets (for example `liked @bob's post [$AA]`) whenever the original post is still in the session registry, so you can reply to it directly.
66
66
 
67
67
  ### CLI options
68
68
 
data/lib/tempest/post.rb CHANGED
@@ -43,6 +43,9 @@ module Tempest
43
43
  record["reply"] = { "root" => ref, "parent" => ref }
44
44
  end
45
45
 
46
+ link_facets = detect_link_facets(text)
47
+ record["facets"] = link_facets unless link_facets.empty?
48
+
46
49
  client.post(
47
50
  "com.atproto.repo.createRecord",
48
51
  body: {
@@ -52,5 +55,29 @@ module Tempest
52
55
  },
53
56
  )
54
57
  end
58
+
59
+ # Scans `text` for bare URLs and builds AT Protocol link facets pointing
60
+ # at each match. Without this, the AppView treats URLs as plain text and
61
+ # does not render them as clickable links.
62
+ def self.detect_link_facets(text)
63
+ return [] if text.nil? || text.empty?
64
+
65
+ bytes = text.b
66
+ facets = []
67
+ pos = 0
68
+ while (match = /https?:\/\/\S+/n.match(bytes, pos))
69
+ byte_start = match.begin(0)
70
+ byte_end = match.end(0)
71
+ uri = match[0].dup.force_encoding(Encoding::UTF_8)
72
+ facets << {
73
+ "index" => { "byteStart" => byte_start, "byteEnd" => byte_end },
74
+ "features" => [
75
+ { "$type" => "app.bsky.richtext.facet#link", "uri" => uri },
76
+ ],
77
+ }
78
+ pos = byte_end
79
+ end
80
+ facets
81
+ end
55
82
  end
56
83
  end
@@ -76,10 +76,10 @@ module Tempest
76
76
  body = "(deleted #{event.collection}/#{event.rkey})"
77
77
  var = nil
78
78
  elsif event.respond_to?(:like?) && event.like?
79
- body = "liked #{subject_owner_label(event.subject_uri, resolver)}"
79
+ body = "liked #{subject_owner_label(event.subject_uri, resolver, registry)}"
80
80
  var = nil
81
81
  elsif event.respond_to?(:repost?) && event.repost?
82
- body = "reposted #{subject_owner_label(event.subject_uri, resolver)}"
82
+ body = "reposted #{subject_owner_label(event.subject_uri, resolver, registry)}"
83
83
  var = nil
84
84
  else
85
85
  facets = event.respond_to?(:facets) ? event.facets : nil
@@ -91,13 +91,18 @@ module Tempest
91
91
  compose(var, format_time(event.created_at), handle, event.did, body)
92
92
  end
93
93
 
94
- def subject_owner_label(subject_uri, resolver)
94
+ def subject_owner_label(subject_uri, resolver, registry = nil)
95
95
  did = subject_did(subject_uri)
96
96
  return "a post" unless did
97
97
 
98
98
  handle = resolver&.resolve(did)
99
99
  owner = handle ? handle_label(handle) : did_label(did)
100
- "#{owner}'s post"
100
+ label = "#{owner}'s post"
101
+ var = registry&.var_for_uri(subject_uri)
102
+ return label unless var
103
+
104
+ bracket = Formatter.color ? "#{DIM}[#{var}]#{RESET}" : "[#{var}]"
105
+ "#{label} #{bracket}"
101
106
  end
102
107
 
103
108
  def subject_did(subject_uri)
@@ -1,3 +1,4 @@
1
+ require "reline"
1
2
  require_relative "../../tempest"
2
3
 
3
4
  module Tempest
@@ -13,9 +14,10 @@ module Tempest
13
14
  # CSI row;col H move cursor
14
15
  # ESC 7 / ESC 8 save/restore cursor (DECSC/DECRC)
15
16
  class Screen
16
- def initialize(io:, rows: nil)
17
+ def initialize(io:, rows: nil, cols: nil)
17
18
  @io = io
18
19
  @rows = rows
20
+ @cols = cols
19
21
  @enabled = false
20
22
  @mutex = Mutex.new
21
23
  end
@@ -26,6 +28,7 @@ module Tempest
26
28
  return unless rows && rows >= 4
27
29
 
28
30
  @rows = rows
31
+ @cols ||= detect_cols
29
32
  @io.print "\e[1;#{rows - 1}r" # scrolling region: rows 1..rows-1
30
33
  @io.print "\e[#{rows};1H" # park cursor on the final row (prompt)
31
34
  @io.flush if @io.respond_to?(:flush)
@@ -98,16 +101,41 @@ module Tempest
98
101
  nil
99
102
  end
100
103
 
104
+ def detect_cols
105
+ return nil unless defined?(IO) && IO.respond_to?(:console)
106
+ console = IO.console
107
+ return nil unless console
108
+ _rows, cols = console.winsize
109
+ cols
110
+ rescue StandardError
111
+ nil
112
+ end
113
+
114
+ # The terminal would otherwise wrap a line that overflows `@cols` past
115
+ # the bottom of the scrolling region and into the prompt row. Split the
116
+ # line into width-bounded chunks so each one fits and scrolls the region
117
+ # cleanly.
101
118
  def insert_above_prompt(line)
119
+ chunks = wrap_to_cols(line)
102
120
  bottom_of_region = @rows - 1
103
121
  @io.print "\e7" # save cursor
104
- @io.print "\e[#{bottom_of_region};1H" # move to last row of scrolling region
105
- @io.print "\r\e[2K" # clear that row first
106
- @io.print "#{line}\n" # write line; \n scrolls region up by 1
122
+ chunks.each do |chunk|
123
+ @io.print "\e[#{bottom_of_region};1H" # move to last row of scrolling region
124
+ @io.print "\r\e[2K" # clear that row first
125
+ @io.print "#{chunk}\n" # write chunk; \n scrolls region up by 1
126
+ end
107
127
  @io.print "\e8" # restore cursor
108
128
  @io.flush if @io.respond_to?(:flush)
109
129
  end
110
130
 
131
+ def wrap_to_cols(line)
132
+ return [line] unless @cols && @cols.positive?
133
+ return [line] if Reline::Unicode.calculate_width(line, true) <= @cols
134
+
135
+ chunks, _ = Reline::Unicode.split_by_width(line, @cols)
136
+ chunks.compact.reject(&:empty?)
137
+ end
138
+
111
139
  def rerender_prompt
112
140
  return unless defined?(Reline)
113
141
  Reline.line_editor&.rerender
@@ -1,3 +1,3 @@
1
1
  module Tempest
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tempest-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuya Fujiwara