slk 0.12.0 → 0.13.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/CHANGELOG.md +15 -0
- data/README.md +24 -4
- data/lib/slk/commands/catchup.rb +8 -0
- data/lib/slk/commands/later.rb +104 -28
- data/lib/slk/commands/messages.rb +1 -22
- data/lib/slk/commands/unread.rb +30 -14
- data/lib/slk/formatters/saved_item_formatter.rb +38 -2
- data/lib/slk/support/attachment_fetching.rb +38 -0
- data/lib/slk/support/image_preview.rb +93 -0
- data/lib/slk/version.rb +1 -1
- data/lib/slk.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbd57363408aa79edfa6cbbe3e875034d2fbcb9e3c2a7ec8ea1e9acea30c9167
|
|
4
|
+
data.tar.gz: bf9ab8b668ad9d04191ddb9b63a1c502d3652059c3b5bbc16113dff1d94eb8bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71d05dc4a9f5b46c49e5a14cad5c5e9b79042ece234142bb16f53dde80da67a5beafd1a2af0f30ef33b2bb8ef5381d64393979e640c996810bb536005e9a8180
|
|
7
|
+
data.tar.gz: 49956b5f79adb5e03f4bac033109626a315f9a1326904e321d4dcf2e17d189d7ac500bb820e2dab52199243d916c204b6ecef94b0c445c8287d591bdd1bfaa20
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.0] - 2026-10-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `slk later` now lists each saved message's files (`[File: name]`) and attachment images (`[Image: name]`), and prints an "N files not downloaded" hint like `slk messages`. Pass `--fetch-attachments` to download them to `~/.cache/slk/files/` and show the local paths. With `--json`, it also adds a `file_paths` map (file ID to local path).
|
|
15
|
+
- With `--fetch-attachments`, `slk later` draws downloaded images inline under their `[File:]` line in Ghostty, Kitty, iTerm2 and WezTerm, including inside tmux. This uses [chafa](https://hpjansson.org/chafa/) when it's installed. In other terminals, without chafa, or when output is piped, only the local path is shown. Pass `--no-image-preview` or set `SLK_IMAGE_PREVIEW=0` to download without drawing images.
|
|
16
|
+
|
|
17
|
+
## [0.12.1] - 2026-10-06
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `slk unread --json` now prints exactly one valid JSON document, with no workspace name lines mixed in. With one workspace (or `-w`), it prints `{"channels": [...], "dms": [...]}` alone. With several, it prints an object keyed by workspace name: `{"oddball": {...}, "dsva": {...}}`. This changes the output shape for multi-workspace callers.
|
|
22
|
+
- `slk unread -w WS` and `slk catchup -w WS` now limit themselves to that workspace. Before, they defaulted to all workspaces and ignored `-w`.
|
|
23
|
+
- `slk unread` text output shows workspace name headers only when it covers more than one workspace.
|
|
24
|
+
|
|
10
25
|
## [0.12.0] - 2026-09-30
|
|
11
26
|
|
|
12
27
|
### Added
|
data/README.md
CHANGED
|
@@ -354,10 +354,30 @@ rake test
|
|
|
354
354
|
### Releasing
|
|
355
355
|
|
|
356
356
|
1. Update version in `lib/slk/version.rb`
|
|
357
|
-
2. Update `CHANGELOG.md`
|
|
358
|
-
3.
|
|
359
|
-
4.
|
|
360
|
-
5.
|
|
357
|
+
2. Update `CHANGELOG.md`: add a `## [X.Y.Z] - YYYY-MM-DD` heading under `## [Unreleased]` so the pending entries move into the release
|
|
358
|
+
3. Run `bundle install` so `Gemfile.lock` picks up the new version
|
|
359
|
+
4. Verify: `bundle exec rake test && bundle exec rubocop`
|
|
360
|
+
5. Commit version, changelog, and lockfile together, then push and wait for CI:
|
|
361
|
+
```bash
|
|
362
|
+
git commit -am "chore(release): prepare vX.Y.Z"
|
|
363
|
+
git push origin master
|
|
364
|
+
gh run watch --exit-status
|
|
365
|
+
```
|
|
366
|
+
6. Build, tag, and push the tag:
|
|
367
|
+
```bash
|
|
368
|
+
bundle exec rake build
|
|
369
|
+
git tag -a vX.Y.Z -m "Version X.Y.Z"
|
|
370
|
+
git push origin vX.Y.Z
|
|
371
|
+
```
|
|
372
|
+
7. Publish to RubyGems. `fnox exec` supplies `GEM_HOST_API_KEY` from `fnox.toml` (a stale key in `~/.gem/credentials` gets "Access Denied"); MFA is required, and `GEM_HOST_OTP_CODE` skips the prompt:
|
|
373
|
+
```bash
|
|
374
|
+
fnox exec -- sh -c 'GEM_HOST_OTP_CODE="$(op item get Rubygems --otp)" gem push pkg/slk-X.Y.Z.gem'
|
|
375
|
+
```
|
|
376
|
+
8. Create the GitHub Release with the changelog entry as notes:
|
|
377
|
+
```bash
|
|
378
|
+
awk '/^## \[X.Y.Z\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md | sed '/./,$!d' > /tmp/notes.md
|
|
379
|
+
gh release create vX.Y.Z --title vX.Y.Z --notes-file /tmp/notes.md
|
|
380
|
+
```
|
|
361
381
|
|
|
362
382
|
## License
|
|
363
383
|
|
data/lib/slk/commands/catchup.rb
CHANGED
|
@@ -90,6 +90,14 @@ module Slk
|
|
|
90
90
|
|
|
91
91
|
private
|
|
92
92
|
|
|
93
|
+
# Catchup defaults to every workspace, but -w names one: let it win, or
|
|
94
|
+
# `slk catchup -w oddball` quietly walks (or batch-clears) all of them.
|
|
95
|
+
def target_workspaces
|
|
96
|
+
return [runner.workspace(@options[:workspace])] if @options[:workspace]
|
|
97
|
+
|
|
98
|
+
super
|
|
99
|
+
end
|
|
100
|
+
|
|
93
101
|
def batch_catchup
|
|
94
102
|
target_workspaces.each { |ws| batch_mark_workspace(ws) }
|
|
95
103
|
0
|
data/lib/slk/commands/later.rb
CHANGED
|
@@ -9,6 +9,17 @@ module Slk
|
|
|
9
9
|
# rubocop:disable Metrics/ClassLength
|
|
10
10
|
class Later < Base
|
|
11
11
|
include Support::InlineImages
|
|
12
|
+
include Support::AttachmentFetching
|
|
13
|
+
include Support::ImagePreview
|
|
14
|
+
|
|
15
|
+
BOOLEAN_FLAGS = {
|
|
16
|
+
'--completed' => :completed,
|
|
17
|
+
'--in-progress' => :in_progress,
|
|
18
|
+
'--counts' => :counts,
|
|
19
|
+
'--no-content' => :no_content,
|
|
20
|
+
'--no-image-preview' => :no_image_preview,
|
|
21
|
+
'--workspace-emoji' => :workspace_emoji
|
|
22
|
+
}.freeze
|
|
12
23
|
|
|
13
24
|
def execute
|
|
14
25
|
result = validate_options
|
|
@@ -86,6 +97,7 @@ module Slk
|
|
|
86
97
|
unless @options[:no_content]
|
|
87
98
|
message = fetch_message_content(workspace, item)
|
|
88
99
|
json_item[:message] = message if message
|
|
100
|
+
add_json_file_paths(json_item, workspace, item, message)
|
|
89
101
|
end
|
|
90
102
|
|
|
91
103
|
json_item
|
|
@@ -93,7 +105,13 @@ module Slk
|
|
|
93
105
|
end
|
|
94
106
|
# rubocop:enable Metrics/MethodLength
|
|
95
107
|
|
|
96
|
-
|
|
108
|
+
def add_json_file_paths(json_item, workspace, item, message)
|
|
109
|
+
return unless @options[:fetch_attachments]
|
|
110
|
+
|
|
111
|
+
paths = attachment_paths_for(workspace, item, message)
|
|
112
|
+
json_item[:file_paths] = paths unless paths.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
97
115
|
def display_formatted(workspace, items)
|
|
98
116
|
if items.empty?
|
|
99
117
|
puts 'No saved items found.'
|
|
@@ -101,37 +119,93 @@ module Slk
|
|
|
101
119
|
end
|
|
102
120
|
|
|
103
121
|
@fetch_failures = 0
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
display_with_workspace_emoji(workspace, items)
|
|
107
|
-
else
|
|
108
|
-
display_without_workspace_emoji(workspace, items)
|
|
109
|
-
end
|
|
122
|
+
@displayed_messages = []
|
|
123
|
+
display_each_item(workspace, items)
|
|
110
124
|
show_fetch_failure_summary
|
|
125
|
+
print_file_summary(@displayed_messages) unless @options[:fetch_attachments]
|
|
111
126
|
end
|
|
112
|
-
# rubocop:enable Metrics/MethodLength
|
|
113
127
|
|
|
114
|
-
def
|
|
115
|
-
|
|
128
|
+
def display_each_item(workspace, items)
|
|
129
|
+
printer = buffered_display? ? method(:display_item_buffered) : method(:display_item_direct)
|
|
116
130
|
items.each do |item|
|
|
117
131
|
message = @options[:no_content] ? nil : fetch_message_content(workspace, item)
|
|
118
|
-
|
|
132
|
+
printer.call(item, workspace, message, attachment_paths_for(workspace, item, message))
|
|
119
133
|
end
|
|
120
134
|
end
|
|
121
135
|
|
|
122
|
-
def
|
|
123
|
-
|
|
136
|
+
def display_item_direct(item, workspace, message, file_paths)
|
|
137
|
+
@direct_formatter ||= build_formatter(output)
|
|
138
|
+
format_item(@direct_formatter, item, workspace, message, file_paths)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Capture one item's output, then reprint it line by line so workspace
|
|
142
|
+
# emoji and downloaded images can be drawn inline as each item arrives.
|
|
143
|
+
def display_item_buffered(item, workspace, message, file_paths)
|
|
124
144
|
buffer = StringIO.new
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
format_item(build_formatter(create_buffer_output(buffer)), item, workspace, message, file_paths)
|
|
146
|
+
previews = preview_paths(file_paths)
|
|
147
|
+
buffer.string.each_line do |line|
|
|
148
|
+
print_buffered_line(line.chomp, workspace)
|
|
149
|
+
print_line_previews(line, previews)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
127
152
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
def format_item(formatter, item, workspace, message, file_paths)
|
|
154
|
+
formatter.display_item(item, workspace, message: message, width: display_width,
|
|
155
|
+
truncate: @options[:truncate], file_paths: file_paths)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def print_buffered_line(line, workspace)
|
|
159
|
+
if workspace_emoji_images?
|
|
160
|
+
print_with_workspace_emoji(line, workspace)
|
|
161
|
+
else
|
|
162
|
+
puts line
|
|
131
163
|
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Draw a preview under the [File: ...] / [Image: ...] line that names it
|
|
167
|
+
def print_line_previews(line, previews)
|
|
168
|
+
previews.each do |path|
|
|
169
|
+
next unless line.include?("[File: #{path}]") || line.include?("[Image: #{path}]")
|
|
132
170
|
|
|
133
|
-
|
|
134
|
-
|
|
171
|
+
previews.delete(path)
|
|
172
|
+
print_image_preview(path)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def preview_paths(file_paths)
|
|
177
|
+
return [] unless attachment_previews?
|
|
178
|
+
|
|
179
|
+
file_paths.values.uniq.select { |path| previewable_image?(path) }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Workspace emoji are terminal escape sequences, so skip them in markdown mode
|
|
183
|
+
def workspace_emoji_images?
|
|
184
|
+
@options[:workspace_emoji] && !@options[:markdown] && inline_images_supported?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def attachment_previews?
|
|
188
|
+
return false unless @options[:fetch_attachments]
|
|
189
|
+
return false if @options[:markdown] || @options[:quiet] || @options[:no_image_preview]
|
|
190
|
+
|
|
191
|
+
image_previews_supported?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def buffered_display?
|
|
195
|
+
workspace_emoji_images? || attachment_previews?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Track the message for the "N files not downloaded" summary and, with
|
|
199
|
+
# --fetch-attachments, download its files/images to the local cache.
|
|
200
|
+
# @return [Hash] file_id / "att_<ts>_<idx>" => local path
|
|
201
|
+
def attachment_paths_for(workspace, item, message)
|
|
202
|
+
return {} unless message
|
|
203
|
+
|
|
204
|
+
model = Models::Message.from_api(message, channel_id: item.channel_id)
|
|
205
|
+
@displayed_messages&.push(model)
|
|
206
|
+
return {} unless @options[:fetch_attachments]
|
|
207
|
+
|
|
208
|
+
fetch_attachment_files([model], workspace)
|
|
135
209
|
end
|
|
136
210
|
|
|
137
211
|
def show_fetch_failure_summary
|
|
@@ -187,20 +261,19 @@ module Slk
|
|
|
187
261
|
in_progress: false,
|
|
188
262
|
counts: false,
|
|
189
263
|
no_content: false,
|
|
264
|
+
no_image_preview: false,
|
|
190
265
|
truncate: false,
|
|
191
266
|
workspace_emoji: false
|
|
192
267
|
)
|
|
193
268
|
end
|
|
194
269
|
|
|
195
270
|
def handle_option(arg, args, remaining)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
when '--workspace-emoji' then @options[:workspace_emoji] = true
|
|
203
|
-
else super
|
|
271
|
+
if %w[-n --limit].include?(arg)
|
|
272
|
+
@options[:limit] = args.shift.to_i
|
|
273
|
+
elsif BOOLEAN_FLAGS.key?(arg)
|
|
274
|
+
@options[BOOLEAN_FLAGS[arg]] = true
|
|
275
|
+
else
|
|
276
|
+
super
|
|
204
277
|
end
|
|
205
278
|
end
|
|
206
279
|
|
|
@@ -221,6 +294,7 @@ module Slk
|
|
|
221
294
|
def help_text
|
|
222
295
|
help = Support::HelpFormatter.new('slk later [options]')
|
|
223
296
|
help.description('Show saved "Later" items from Slack.')
|
|
297
|
+
help.note('Set SLK_IMAGE_PREVIEW=0 to always skip inline image previews.')
|
|
224
298
|
add_options_section(help)
|
|
225
299
|
help.render
|
|
226
300
|
end
|
|
@@ -233,6 +307,8 @@ module Slk
|
|
|
233
307
|
s.option('--in-progress', 'Show in-progress items')
|
|
234
308
|
s.option('--counts', 'Show summary counts only')
|
|
235
309
|
s.option('--no-content', 'Skip fetching message text')
|
|
310
|
+
s.option('--fetch-attachments', 'Download files to ~/.cache/slk/files/ (inline image previews need chafa)')
|
|
311
|
+
s.option('--no-image-preview', 'Download attachments but skip inline image previews')
|
|
236
312
|
s.option('--workspace-emoji', 'Show workspace emoji as inline images')
|
|
237
313
|
s.option('--no-emoji', 'Show :emoji: codes instead of unicode')
|
|
238
314
|
s.option('--width N', 'Wrap text at N columns')
|
|
@@ -9,6 +9,7 @@ module Slk
|
|
|
9
9
|
# rubocop:disable Metrics/ClassLength
|
|
10
10
|
class Messages < Base
|
|
11
11
|
include Support::InlineImages
|
|
12
|
+
include Support::AttachmentFetching
|
|
12
13
|
|
|
13
14
|
# rubocop:disable Metrics/MethodLength
|
|
14
15
|
def execute
|
|
@@ -346,28 +347,6 @@ module Slk
|
|
|
346
347
|
end
|
|
347
348
|
end
|
|
348
349
|
|
|
349
|
-
def print_file_summary(messages)
|
|
350
|
-
file_count = messages.sum { |m| m.files.size + downloadable_attachment_count(m) }
|
|
351
|
-
return if file_count.zero?
|
|
352
|
-
|
|
353
|
-
label = file_count == 1 ? '1 file' : "#{file_count} files"
|
|
354
|
-
puts
|
|
355
|
-
info("#{label} not downloaded. Use --fetch-attachments to download.")
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
def downloadable_attachment_count(message)
|
|
359
|
-
message.attachments.count { |a| a['image_url'] || a['thumb_url'] }
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
def fetch_attachment_files(messages, workspace)
|
|
363
|
-
paths = Support::XdgPaths.new
|
|
364
|
-
downloader = Services::FileDownloader.new(
|
|
365
|
-
cache_dir: paths.cache_dir,
|
|
366
|
-
on_debug: ->(msg) { debug(msg) }
|
|
367
|
-
)
|
|
368
|
-
downloader.download_message_files(messages, workspace)
|
|
369
|
-
end
|
|
370
|
-
|
|
371
350
|
def find_workspace_emoji(workspace_name, emoji_name)
|
|
372
351
|
return nil if emoji_name.empty?
|
|
373
352
|
|
data/lib/slk/commands/unread.rb
CHANGED
|
@@ -95,22 +95,38 @@ module Slk
|
|
|
95
95
|
|
|
96
96
|
private
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
# Unread defaults to every workspace, but -w names one: let it win, or
|
|
99
|
+
# `slk unread -w oddball` quietly reports on all of them.
|
|
100
|
+
def target_workspaces
|
|
101
|
+
return [runner.workspace(@options[:workspace])] if @options[:workspace]
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
super
|
|
104
|
+
end
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
def show_unread
|
|
107
|
+
return show_unread_json if @options[:json]
|
|
108
|
+
|
|
109
|
+
workspaces = target_workspaces
|
|
110
|
+
workspaces.each do |workspace|
|
|
111
|
+
puts output.bold(workspace.name) if workspaces.size > 1
|
|
112
|
+
display_unread(workspace, fetch_unread_data(workspace))
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
0
|
|
112
116
|
end
|
|
113
117
|
|
|
118
|
+
# Exactly one JSON document: one workspace prints its {channels, dms}
|
|
119
|
+
# alone; several are keyed by workspace name. No name headers, so the
|
|
120
|
+
# output pipes straight into jq.
|
|
121
|
+
def show_unread_json
|
|
122
|
+
workspaces = target_workspaces
|
|
123
|
+
documents = workspaces.to_h do |workspace|
|
|
124
|
+
[workspace.name, unread_json(workspace, fetch_unread_data(workspace))]
|
|
125
|
+
end
|
|
126
|
+
output_json(workspaces.size == 1 ? documents.values.first : documents)
|
|
127
|
+
0
|
|
128
|
+
end
|
|
129
|
+
|
|
114
130
|
def fetch_unread_data(workspace)
|
|
115
131
|
counts = runner.client_api(workspace.name).counts
|
|
116
132
|
muted_ids = fetch_muted_ids(workspace)
|
|
@@ -135,11 +151,11 @@ module Slk
|
|
|
135
151
|
.reject { |c| muted_ids.include?(c['id']) }
|
|
136
152
|
end
|
|
137
153
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
def unread_json(workspace, data)
|
|
155
|
+
{
|
|
156
|
+
channels: data[:unread_channels].map { |c| format_channel_json(workspace, c) },
|
|
157
|
+
dms: data[:unread_ims].map { |i| format_dm_json(workspace, i) }
|
|
158
|
+
}
|
|
143
159
|
end
|
|
144
160
|
|
|
145
161
|
def format_channel_json(workspace, channel)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Slk
|
|
4
4
|
module Formatters
|
|
5
5
|
# Formats saved/later items for terminal display
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
|
6
7
|
class SavedItemFormatter
|
|
7
8
|
def initialize(output:, mention_replacer:, text_processor:, on_debug: nil)
|
|
8
9
|
@output = output
|
|
@@ -13,7 +14,9 @@ module Slk
|
|
|
13
14
|
|
|
14
15
|
# Display a single saved item
|
|
15
16
|
# @param truncate [Boolean] if true, truncate to single line at width instead of wrapping
|
|
16
|
-
|
|
17
|
+
# @param file_paths [Hash] file_id / "att_<ts>_<idx>" => local path for downloaded attachments
|
|
18
|
+
# rubocop:disable Metrics/ParameterLists
|
|
19
|
+
def display_item(item, workspace, message: nil, width: nil, truncate: false, file_paths: {})
|
|
17
20
|
status_badge = format_status_badge(item)
|
|
18
21
|
due_info = format_due_info(item)
|
|
19
22
|
|
|
@@ -22,9 +25,13 @@ module Slk
|
|
|
22
25
|
@output.puts header_parts.join(' | ') unless header_parts.empty?
|
|
23
26
|
|
|
24
27
|
# Message content
|
|
25
|
-
|
|
28
|
+
if message
|
|
29
|
+
display_message(message, workspace, width: width, truncate: truncate)
|
|
30
|
+
display_attachments(message, file_paths || {})
|
|
31
|
+
end
|
|
26
32
|
@output.puts # blank line between items
|
|
27
33
|
end
|
|
34
|
+
# rubocop:enable Metrics/ParameterLists
|
|
28
35
|
|
|
29
36
|
private
|
|
30
37
|
|
|
@@ -113,6 +120,34 @@ module Slk
|
|
|
113
120
|
lines[1..].each { |line| @output.puts " #{line.rstrip}" } if lines.length > 1
|
|
114
121
|
end
|
|
115
122
|
|
|
123
|
+
def display_attachments(message, file_paths)
|
|
124
|
+
display_files(message['files'] || [], file_paths)
|
|
125
|
+
display_attachment_images(message['attachments'] || [], message['ts'], file_paths)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def display_files(files, file_paths)
|
|
129
|
+
files.each do |file|
|
|
130
|
+
label = file_paths[file['id']] || file['name'] || file['title'] || 'file'
|
|
131
|
+
@output.puts " #{@output.blue("[File: #{label}]")}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def display_attachment_images(attachments, message_ts, file_paths)
|
|
136
|
+
attachments.each_with_index do |att, idx|
|
|
137
|
+
url = att['image_url'] || att['thumb_url']
|
|
138
|
+
next unless url
|
|
139
|
+
|
|
140
|
+
label = file_paths["att_#{message_ts}_#{idx}"] || att['title'] || image_filename(url)
|
|
141
|
+
@output.puts " #{@output.blue("[Image: #{label}]")}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def image_filename(url)
|
|
146
|
+
File.basename(URI.parse(url).path)
|
|
147
|
+
rescue URI::InvalidURIError
|
|
148
|
+
'image'
|
|
149
|
+
end
|
|
150
|
+
|
|
116
151
|
def resolve_message_author(message, workspace)
|
|
117
152
|
if message['user']
|
|
118
153
|
@mentions.lookup_user_name(workspace, message['user']) || message['user']
|
|
@@ -140,5 +175,6 @@ module Slk
|
|
|
140
175
|
text.length > max_length ? "#{text[0...max_length]}..." : text
|
|
141
176
|
end
|
|
142
177
|
end
|
|
178
|
+
# rubocop:enable Metrics/ClassLength
|
|
143
179
|
end
|
|
144
180
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Support
|
|
5
|
+
# Shared --fetch-attachments behavior for commands that display messages.
|
|
6
|
+
# Expects the including command to provide #debug and #info.
|
|
7
|
+
module AttachmentFetching
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Download files and attachment images for the given messages.
|
|
11
|
+
# @return [Hash] file_id / "att_<ts>_<idx>" => local path
|
|
12
|
+
def fetch_attachment_files(messages, workspace)
|
|
13
|
+
downloader = Services::FileDownloader.new(
|
|
14
|
+
cache_dir: Support::XdgPaths.new.cache_dir,
|
|
15
|
+
on_debug: ->(msg) { debug(msg) }
|
|
16
|
+
)
|
|
17
|
+
downloader.download_message_files(messages, workspace)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def downloadable_file_count(messages)
|
|
21
|
+
messages.sum { |m| m.files.size + downloadable_attachment_count(m) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def downloadable_attachment_count(message)
|
|
25
|
+
message.attachments.count { |a| a['image_url'] || a['thumb_url'] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def print_file_summary(messages)
|
|
29
|
+
file_count = downloadable_file_count(messages)
|
|
30
|
+
return if file_count.zero?
|
|
31
|
+
|
|
32
|
+
label = file_count == 1 ? '1 file' : "#{file_count} files"
|
|
33
|
+
puts
|
|
34
|
+
info("#{label} not downloaded. Use --fetch-attachments to download.")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Support
|
|
7
|
+
# Renders downloaded image attachments inline using chafa, which handles
|
|
8
|
+
# Kitty graphics (Ghostty/Kitty) and iTerm2 images, including tmux
|
|
9
|
+
# passthrough with Unicode placeholders so images scroll with the pane.
|
|
10
|
+
# Without a capable terminal or chafa, callers just show the file path.
|
|
11
|
+
# Set SLK_IMAGE_PREVIEW=0 (or false/no/off) to turn previews off.
|
|
12
|
+
# Expects Support::InlineImages to be included alongside.
|
|
13
|
+
module ImagePreview
|
|
14
|
+
PREVIEW_MAX_COLS = 100
|
|
15
|
+
PREVIEW_MAX_ROWS = 15
|
|
16
|
+
PREVIEWABLE_EXTENSIONS = %w[png jpg jpeg gif webp bmp tif tiff heic].freeze
|
|
17
|
+
DISABLED_VALUES = %w[0 false no off].freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def image_previews_supported?
|
|
22
|
+
return @image_previews_supported if defined?(@image_previews_supported)
|
|
23
|
+
|
|
24
|
+
@image_previews_supported = !image_previews_disabled_by_env? && $stdout.tty? &&
|
|
25
|
+
!image_preview_format.nil? && chafa_available?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def image_previews_disabled_by_env?
|
|
29
|
+
DISABLED_VALUES.include?(ENV.fetch('SLK_IMAGE_PREVIEW', '').strip.downcase)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def image_preview_format
|
|
33
|
+
if kitty_graphics_supported?
|
|
34
|
+
'kitty'
|
|
35
|
+
elsif iterm2_protocol_supported?
|
|
36
|
+
'iterm'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def previewable_image?(path)
|
|
41
|
+
PREVIEWABLE_EXTENSIONS.include?(File.extname(path.to_s).delete('.').downcase)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Print an inline preview of the image at path, indented by indent columns.
|
|
45
|
+
# @return [Boolean] whether a preview was printed
|
|
46
|
+
def print_image_preview(path, indent: 2) # rubocop:disable Naming/PredicateMethod
|
|
47
|
+
return false unless previewable_image?(path) && File.exist?(path)
|
|
48
|
+
|
|
49
|
+
rendered = render_image_preview(path, indent)
|
|
50
|
+
return false unless rendered
|
|
51
|
+
|
|
52
|
+
$stdout.print(' ' * indent)
|
|
53
|
+
$stdout.print(rendered)
|
|
54
|
+
$stdout.print("\n") unless rendered.end_with?("\n", "\e[?25h")
|
|
55
|
+
$stdout.flush
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_image_preview(path, indent)
|
|
60
|
+
stdout, _stderr, status = Open3.capture3(*chafa_args(path, indent))
|
|
61
|
+
status.success? && !stdout.empty? ? stdout : nil
|
|
62
|
+
rescue SystemCallError => e
|
|
63
|
+
debug("chafa failed: #{e.message}") if respond_to?(:debug, true)
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def chafa_args(path, indent)
|
|
68
|
+
args = ['chafa', "--format=#{image_preview_format}"]
|
|
69
|
+
args << '--passthrough=tmux' if in_tmux?
|
|
70
|
+
args + ["--size=#{preview_cols(indent)}x#{PREVIEW_MAX_ROWS}",
|
|
71
|
+
'--animate=off', '--probe=off', '--relative=off', path]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def preview_cols(indent)
|
|
75
|
+
[terminal_columns - indent - 1, PREVIEW_MAX_COLS].min.clamp(10, PREVIEW_MAX_COLS)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def terminal_columns
|
|
79
|
+
require 'io/console'
|
|
80
|
+
cols = IO.console&.winsize&.last.to_i
|
|
81
|
+
cols.positive? ? cols : 80
|
|
82
|
+
rescue StandardError
|
|
83
|
+
80
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def chafa_available?
|
|
87
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
|
|
88
|
+
File.executable?(File.join(dir, 'chafa'))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/slk/version.rb
CHANGED
data/lib/slk.rb
CHANGED
|
@@ -194,6 +194,8 @@ module Slk
|
|
|
194
194
|
autoload :XdgPaths, 'slk/support/xdg_paths'
|
|
195
195
|
autoload :SlackUrlParser, 'slk/support/slack_url_parser'
|
|
196
196
|
autoload :InlineImages, 'slk/support/inline_images'
|
|
197
|
+
autoload :AttachmentFetching, 'slk/support/attachment_fetching'
|
|
198
|
+
autoload :ImagePreview, 'slk/support/image_preview'
|
|
197
199
|
autoload :HelpFormatter, 'slk/support/help_formatter'
|
|
198
200
|
autoload :ErrorLogger, 'slk/support/error_logger'
|
|
199
201
|
autoload :UserResolver, 'slk/support/user_resolver'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Boehs
|
|
@@ -136,11 +136,13 @@ files:
|
|
|
136
136
|
- lib/slk/services/user_matcher.rb
|
|
137
137
|
- lib/slk/services/user_picker.rb
|
|
138
138
|
- lib/slk/services/who_target_resolver.rb
|
|
139
|
+
- lib/slk/support/attachment_fetching.rb
|
|
139
140
|
- lib/slk/support/check_in_time.rb
|
|
140
141
|
- lib/slk/support/date_parser.rb
|
|
141
142
|
- lib/slk/support/decimal_timestamp.rb
|
|
142
143
|
- lib/slk/support/error_logger.rb
|
|
143
144
|
- lib/slk/support/help_formatter.rb
|
|
145
|
+
- lib/slk/support/image_preview.rb
|
|
144
146
|
- lib/slk/support/inline_images.rb
|
|
145
147
|
- lib/slk/support/interactive_prompt.rb
|
|
146
148
|
- lib/slk/support/platform.rb
|