tempest-rb 0.3.0 → 0.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 +4 -4
- data/README.md +3 -0
- data/lib/tempest/commands/tui.rb +2 -0
- data/lib/tempest/debug_log.rb +13 -2
- data/lib/tempest/jetstream/stream_manager.rb +3 -2
- data/lib/tempest/jetstream/watchdog.rb +3 -2
- data/lib/tempest/repl/compose.rb +77 -0
- data/lib/tempest/repl/dispatcher.rb +1 -1
- data/lib/tempest/repl/runner.rb +30 -1
- data/lib/tempest/repl/screen.rb +20 -0
- data/lib/tempest/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2312e8eb343bf518ce61de53a97608a42ece28af9777501910d864e9e2f79f1f
|
|
4
|
+
data.tar.gz: 05063b25a45e82fbcb7051f93f6af1715e0c2e25f7cfeefdbf343b3d5f4877ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8eb2f34c4c3e7dcb851976b4f7ad502edfb57784661dd06ed0792eaef9f1344ef65dee5a1b05f63c51859aa84d9412f23b5714cde1d65a08360dac549245e98f
|
|
7
|
+
data.tar.gz: 23d4f50ec5dab93b6d792009103f78a56df3fa7019bd1266866ff582a55db1e5831f72376ef2949c3bb4cf029a68c1faf1b3f68ea60cb4ea627507924ece146d
|
data/README.md
CHANGED
|
@@ -113,6 +113,7 @@ If you have been using a single-account installation of an earlier `tempest` rel
|
|
|
113
113
|
|------------------|--------------------------------------------------|
|
|
114
114
|
| `:timeline` | Fetch and print the home timeline |
|
|
115
115
|
| `:stream on/off` | Toggle the Jetstream live feed |
|
|
116
|
+
| `:compose` | Open your editor to compose a multi-line post |
|
|
116
117
|
| `:open $LX` | Open the URL with id `$LX` in the browser |
|
|
117
118
|
| `:help` | Show in-app help |
|
|
118
119
|
| `:quit` | Exit (`Ctrl-D` works too) |
|
|
@@ -120,6 +121,8 @@ If you have been using a single-account installation of an earlier `tempest` rel
|
|
|
120
121
|
|
|
121
122
|
Anything else you type is sent as a new post.
|
|
122
123
|
|
|
124
|
+
`:compose` hands the terminal over to your editor so you can write a longer post without fighting the single-line prompt. The editor is picked from `$VISUAL`, then `$EDITOR`, and falls back to `vi`. Lines that start with `#` are treated as comments and stripped; save with an empty body to cancel. Remember to `export` the variable in your shell rc (`export EDITOR=nvim`) — without `export`, the value is not inherited by child processes and the fallback to `vi` kicks in.
|
|
125
|
+
|
|
123
126
|
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.
|
|
124
127
|
|
|
125
128
|
### CLI options
|
data/lib/tempest/commands/tui.rb
CHANGED
|
@@ -87,10 +87,12 @@ module Tempest
|
|
|
87
87
|
cursor_store: cursor_store(env, did: target_did),
|
|
88
88
|
filter: plan.filter,
|
|
89
89
|
logger: debug_logger,
|
|
90
|
+
did: target_did,
|
|
90
91
|
)
|
|
91
92
|
watchdog = Tempest::Jetstream::Watchdog.new(
|
|
92
93
|
stream_manager: stream_manager,
|
|
93
94
|
logger: debug_logger,
|
|
95
|
+
did: target_did,
|
|
94
96
|
**watchdog_options(env),
|
|
95
97
|
)
|
|
96
98
|
|
data/lib/tempest/debug_log.rb
CHANGED
|
@@ -127,8 +127,9 @@ module Tempest
|
|
|
127
127
|
class Channel
|
|
128
128
|
attr_reader :loggers
|
|
129
129
|
|
|
130
|
-
def initialize(loggers:)
|
|
130
|
+
def initialize(loggers:, defaults: {})
|
|
131
131
|
@loggers = Array(loggers)
|
|
132
|
+
@defaults = defaults.freeze
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
def info(mod, event:, **fields)
|
|
@@ -147,6 +148,15 @@ module Tempest
|
|
|
147
148
|
emit(Logger::ERROR, mod, event, fields)
|
|
148
149
|
end
|
|
149
150
|
|
|
151
|
+
# Returns a child channel that prepends `default_fields` to every
|
|
152
|
+
# subsequent log call. Used to attach per-session context such as `did=`
|
|
153
|
+
# to long-lived components (StreamManager, Watchdog) without sprinkling
|
|
154
|
+
# the field across every call site.
|
|
155
|
+
def with(**default_fields)
|
|
156
|
+
return self if default_fields.empty?
|
|
157
|
+
Channel.new(loggers: @loggers, defaults: @defaults.merge(default_fields))
|
|
158
|
+
end
|
|
159
|
+
|
|
150
160
|
def close
|
|
151
161
|
@loggers.each do |logger|
|
|
152
162
|
begin
|
|
@@ -162,7 +172,8 @@ module Tempest
|
|
|
162
172
|
|
|
163
173
|
def emit(level, mod, event, fields)
|
|
164
174
|
return if @loggers.empty?
|
|
165
|
-
|
|
175
|
+
merged = @defaults.merge(fields)
|
|
176
|
+
msg = format_body(event, merged)
|
|
166
177
|
@loggers.each { |logger| logger.add(level, msg, mod) }
|
|
167
178
|
end
|
|
168
179
|
|
|
@@ -25,7 +25,7 @@ module Tempest
|
|
|
25
25
|
def initialize(client:, backoff: DEFAULT_BACKOFF, sleeper: ->(s) { sleep(s) },
|
|
26
26
|
clock: -> { Time.now }, cursor_store: nil,
|
|
27
27
|
cursor_save_interval: DEFAULT_CURSOR_SAVE_INTERVAL,
|
|
28
|
-
filter: nil, logger: nil)
|
|
28
|
+
filter: nil, logger: nil, did: nil)
|
|
29
29
|
@client = client
|
|
30
30
|
@backoff = backoff
|
|
31
31
|
@sleeper = sleeper
|
|
@@ -33,7 +33,8 @@ module Tempest
|
|
|
33
33
|
@cursor_store = cursor_store
|
|
34
34
|
@cursor_save_interval = cursor_save_interval
|
|
35
35
|
@filter = filter
|
|
36
|
-
|
|
36
|
+
base = logger || Tempest::DebugLog.null_channel
|
|
37
|
+
@logger = did ? base.with(did: did) : base
|
|
37
38
|
@thread = nil
|
|
38
39
|
@mutex = Mutex.new
|
|
39
40
|
@stopping = false
|
|
@@ -25,13 +25,14 @@ module Tempest
|
|
|
25
25
|
interval_seconds: DEFAULT_INTERVAL_SECONDS,
|
|
26
26
|
clock: -> { Time.now },
|
|
27
27
|
sleeper: ->(s) { sleep(s) },
|
|
28
|
-
logger: nil)
|
|
28
|
+
logger: nil, did: nil)
|
|
29
29
|
@stream_manager = stream_manager
|
|
30
30
|
@threshold_seconds = threshold_seconds
|
|
31
31
|
@interval_seconds = interval_seconds
|
|
32
32
|
@clock = clock
|
|
33
33
|
@sleeper = sleeper
|
|
34
|
-
|
|
34
|
+
base = logger || Tempest::DebugLog.null_channel
|
|
35
|
+
@logger = did ? base.with(did: did) : base
|
|
35
36
|
@thread = nil
|
|
36
37
|
@mutex = Mutex.new
|
|
37
38
|
@stopping = false
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
|
|
3
|
+
require_relative "../../tempest"
|
|
4
|
+
|
|
5
|
+
module Tempest
|
|
6
|
+
module REPL
|
|
7
|
+
# Opens `$VISUAL` / `$EDITOR` on a scratch file so the user can compose a
|
|
8
|
+
# multi-line post in their normal editor, the same pattern `git commit`
|
|
9
|
+
# uses. Returns one of the status tuples below; the caller (typically
|
|
10
|
+
# `Runner#handle_compose`) maps each to a user-facing line and, when
|
|
11
|
+
# `:ok`, forwards the body to `Post.create`.
|
|
12
|
+
#
|
|
13
|
+
# Return values:
|
|
14
|
+
# [:ok, body] successful compose; body is non-empty
|
|
15
|
+
# [:empty, nil] user saved an empty body — treat as cancellation
|
|
16
|
+
# [:editor_failed, nil] the editor subprocess returned a non-zero status
|
|
17
|
+
#
|
|
18
|
+
# Lines beginning with `#` are stripped from the file before posting (so we
|
|
19
|
+
# can pre-populate the file with instructions a la `git commit`'s template).
|
|
20
|
+
module Compose
|
|
21
|
+
TEMPLATE = <<~EOT.freeze
|
|
22
|
+
|
|
23
|
+
# Compose your Bluesky post above this line.
|
|
24
|
+
# Lines starting with `#` and surrounding whitespace are stripped.
|
|
25
|
+
# Save with an empty body (or quit without changes) to cancel.
|
|
26
|
+
EOT
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def run(env: ENV, runner: Kernel.method(:system),
|
|
31
|
+
tempfile_factory: ->(suffix) { Tempfile.new(["tempest-compose-", suffix]) })
|
|
32
|
+
editor = pick_editor(env)
|
|
33
|
+
|
|
34
|
+
file = tempfile_factory.call(".txt")
|
|
35
|
+
path = file.path
|
|
36
|
+
begin
|
|
37
|
+
file.write(TEMPLATE)
|
|
38
|
+
file.flush
|
|
39
|
+
file.close
|
|
40
|
+
|
|
41
|
+
ok = runner.call(editor, path)
|
|
42
|
+
return [:editor_failed, nil] unless ok
|
|
43
|
+
|
|
44
|
+
body = parse(File.read(path))
|
|
45
|
+
return [:empty, nil] if body.empty?
|
|
46
|
+
[:ok, body]
|
|
47
|
+
ensure
|
|
48
|
+
begin
|
|
49
|
+
file.unlink
|
|
50
|
+
rescue StandardError
|
|
51
|
+
# File may already be gone (e.g. editor moved it); best-effort.
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Editor resolution order, matching git's convention: $VISUAL, then
|
|
57
|
+
# $EDITOR, then "vi" as a POSIX-mandated last resort. The fallback means
|
|
58
|
+
# we never need to surface a "no editor" error to the user; if even vi
|
|
59
|
+
# cannot be exec'd, `Kernel.system` returns false and the call surfaces
|
|
60
|
+
# as :editor_failed.
|
|
61
|
+
def pick_editor(env)
|
|
62
|
+
candidate = env["VISUAL"]
|
|
63
|
+
candidate = env["EDITOR"] if candidate.nil? || candidate.strip.empty?
|
|
64
|
+
candidate = "vi" if candidate.nil? || candidate.strip.empty?
|
|
65
|
+
candidate
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse(content)
|
|
69
|
+
content
|
|
70
|
+
.each_line
|
|
71
|
+
.reject { |line| line.start_with?("#") }
|
|
72
|
+
.join
|
|
73
|
+
.strip
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -5,7 +5,7 @@ module Tempest
|
|
|
5
5
|
Command = Data.define(:name, :args)
|
|
6
6
|
|
|
7
7
|
class Dispatcher
|
|
8
|
-
KNOWN_COMMANDS = %i[timeline quit help stream open relogin fav].freeze
|
|
8
|
+
KNOWN_COMMANDS = %i[timeline quit help stream open relogin fav compose].freeze
|
|
9
9
|
DOLLAR_ID = /\A\$[A-Z]{2}\z/.freeze
|
|
10
10
|
|
|
11
11
|
def dispatch(input)
|
data/lib/tempest/repl/runner.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "../../tempest"
|
|
|
4
4
|
require_relative "../timeline"
|
|
5
5
|
require_relative "../post"
|
|
6
6
|
require_relative "../jetstream/stream_manager"
|
|
7
|
+
require_relative "compose"
|
|
7
8
|
require_relative "dispatcher"
|
|
8
9
|
require_relative "formatter"
|
|
9
10
|
require_relative "registry"
|
|
@@ -17,6 +18,7 @@ module Tempest
|
|
|
17
18
|
Available commands:
|
|
18
19
|
:timeline Fetch and print the home timeline
|
|
19
20
|
:stream on|off Toggle the Jetstream live feed
|
|
21
|
+
:compose Open $EDITOR to write a multi-line post
|
|
20
22
|
:open $XX|$LX Open the post or URL with the given id in the browser
|
|
21
23
|
:fav $XX Like the post with id $XX
|
|
22
24
|
:relogin Re-authenticate when the cached session is dead
|
|
@@ -35,7 +37,7 @@ module Tempest
|
|
|
35
37
|
def initialize(session:, client:, input:, output:, dispatcher: Dispatcher.new,
|
|
36
38
|
stream_manager: nil, handle_resolver: nil, stream_output: nil,
|
|
37
39
|
timeline_store: nil, registry: Registry.new, opener: DEFAULT_OPENER,
|
|
38
|
-
avatar_store: nil, reauth: nil)
|
|
40
|
+
avatar_store: nil, reauth: nil, compose: Compose.method(:run))
|
|
39
41
|
@session = session
|
|
40
42
|
@client = client
|
|
41
43
|
@input = input
|
|
@@ -49,6 +51,7 @@ module Tempest
|
|
|
49
51
|
@opener = opener
|
|
50
52
|
@avatar_store = avatar_store
|
|
51
53
|
@reauth = reauth
|
|
54
|
+
@compose = compose
|
|
52
55
|
# URIs already printed via bootstrap_timeline or backfill_timeline.
|
|
53
56
|
# Jetstream's cursor-replay can re-emit those same posts on startup
|
|
54
57
|
# (the persisted cursor is older than the getTimeline window), so the
|
|
@@ -105,6 +108,8 @@ module Tempest
|
|
|
105
108
|
handle_stream(command.args.first)
|
|
106
109
|
when :post
|
|
107
110
|
handle_post(command.args.first)
|
|
111
|
+
when :compose
|
|
112
|
+
handle_compose
|
|
108
113
|
when :reply
|
|
109
114
|
handle_reply(command.args[0], command.args[1])
|
|
110
115
|
when :open
|
|
@@ -148,6 +153,30 @@ module Tempest
|
|
|
148
153
|
@output.puts "error: #{e.message}"
|
|
149
154
|
end
|
|
150
155
|
|
|
156
|
+
def handle_compose
|
|
157
|
+
# Hand the terminal off to $EDITOR for the duration of the compose so
|
|
158
|
+
# the editor can take full control of the screen. We re-park the
|
|
159
|
+
# Screen on return regardless of success or exception.
|
|
160
|
+
suspend_screen do
|
|
161
|
+
status, body = @compose.call
|
|
162
|
+
case status
|
|
163
|
+
when :ok
|
|
164
|
+
handle_post(body)
|
|
165
|
+
when :empty
|
|
166
|
+
@output.puts "compose cancelled (empty body)"
|
|
167
|
+
when :editor_failed
|
|
168
|
+
@output.puts "editor exited with a non-zero status; post not sent"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def suspend_screen
|
|
174
|
+
@output.suspend if @output.respond_to?(:suspend)
|
|
175
|
+
yield
|
|
176
|
+
ensure
|
|
177
|
+
@output.resume if @output.respond_to?(:resume)
|
|
178
|
+
end
|
|
179
|
+
|
|
151
180
|
def handle_relogin
|
|
152
181
|
if @reauth.nil?
|
|
153
182
|
@output.puts "relogin is not available in this session"
|
data/lib/tempest/repl/screen.rb
CHANGED
|
@@ -46,6 +46,26 @@ module Tempest
|
|
|
46
46
|
@enabled = false
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# Transient teardown for handing the terminal off to a subprocess (e.g.
|
|
50
|
+
# $EDITOR via `:compose`). Unlike `disable`, this does NOT issue the
|
|
51
|
+
# Kitty graphics delete sequence — terminals that support the Kitty
|
|
52
|
+
# protocol keep image placements in the main screen buffer even while
|
|
53
|
+
# the editor draws on the alternate buffer, so suspending without
|
|
54
|
+
# deleting lets the avatars re-appear automatically when the editor
|
|
55
|
+
# exits. Pair with `resume` to re-establish the scrolling region.
|
|
56
|
+
def suspend
|
|
57
|
+
return unless @enabled
|
|
58
|
+
uninstall_resize_trap
|
|
59
|
+
@io.print "\e[r"
|
|
60
|
+
@io.flush if @io.respond_to?(:flush)
|
|
61
|
+
@enabled = false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def resume
|
|
65
|
+
return if @enabled
|
|
66
|
+
enable
|
|
67
|
+
end
|
|
68
|
+
|
|
49
69
|
def enabled?
|
|
50
70
|
@enabled
|
|
51
71
|
end
|
data/lib/tempest/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yuya Fujiwara
|
|
@@ -128,6 +128,7 @@ files:
|
|
|
128
128
|
- lib/tempest/post.rb
|
|
129
129
|
- lib/tempest/post_view.rb
|
|
130
130
|
- lib/tempest/repl/async_output.rb
|
|
131
|
+
- lib/tempest/repl/compose.rb
|
|
131
132
|
- lib/tempest/repl/dispatcher.rb
|
|
132
133
|
- lib/tempest/repl/formatter.rb
|
|
133
134
|
- lib/tempest/repl/registry.rb
|