slk 0.4.0 → 0.4.1
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 +23 -0
- data/lib/slk/api/saved.rb +26 -0
- data/lib/slk/cli.rb +12 -1
- data/lib/slk/commands/activity.rb +7 -13
- data/lib/slk/commands/base.rb +2 -1
- data/lib/slk/commands/help.rb +2 -0
- data/lib/slk/commands/later.rb +297 -0
- data/lib/slk/commands/messages.rb +4 -3
- data/lib/slk/formatters/activity_formatter.rb +4 -7
- data/lib/slk/formatters/markdown_output.rb +98 -0
- data/lib/slk/formatters/message_formatter.rb +10 -18
- data/lib/slk/formatters/reaction_formatter.rb +1 -1
- data/lib/slk/formatters/saved_item_formatter.rb +144 -0
- data/lib/slk/formatters/search_formatter.rb +4 -8
- data/lib/slk/formatters/text_processor.rb +48 -0
- data/lib/slk/models/saved_item.rb +128 -0
- data/lib/slk/runner.rb +22 -2
- data/lib/slk/services/message_resolver.rb +38 -0
- data/lib/slk/support/inline_images.rb +94 -10
- data/lib/slk/version.rb +1 -1
- data/lib/slk.rb +7 -0
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef8284e736f39c42a65ef336e2d8db1a36f998393070433809db242cf6de0861
|
|
4
|
+
data.tar.gz: 6af8a0cdb8cbc5f35aea41e89ba0221cd0eb7ab5e4fbbf360cc60b2eb750c4d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c384f28530877909f82a596d4b306ca342989b5720f7cee47ea8157b91f59bd21202101c85e8878a83cb909a3ff954dec6dcabea8d3de2ae72c5d49e6f55882
|
|
7
|
+
data.tar.gz: 1da065c6704edfd5beaf45b2989a9fec1a183061109d5d167467943d7eecaf3b1f7c707e46ae4b517b2e28c3d1edcb23cf37d0f90fda1882408d362be59fc992
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Ghostty/Kitty terminal support** - Inline emoji images now work in Ghostty and Kitty terminals
|
|
13
|
+
- Uses Kitty graphics protocol with Unicode placeholders for proper tmux support
|
|
14
|
+
- Images clear correctly with `clear` command (no floating artifacts)
|
|
15
|
+
- Converts GIF/JPEG to PNG automatically (macOS only via `sips`)
|
|
16
|
+
|
|
17
|
+
- **`later` command** - View Slack's "Save for Later" items
|
|
18
|
+
- Lists saved messages with content preview
|
|
19
|
+
- Filter by state: `--completed`, `--in-progress`
|
|
20
|
+
- `--counts` for summary statistics (total, overdue, with due dates)
|
|
21
|
+
- `--no-content` to skip fetching message text
|
|
22
|
+
- `--workspace-emoji` for inline custom emoji images
|
|
23
|
+
- `--width N` to wrap text at N columns
|
|
24
|
+
- `--no-wrap` to truncate messages to single line
|
|
25
|
+
- `--json` output includes message content
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- New `TextProcessor` service centralizes text processing (HTML decode, mentions, emoji)
|
|
30
|
+
- New `MessageResolver` service extracted from activity command for reuse
|
|
31
|
+
- Refactored formatters to use shared TextProcessor
|
|
32
|
+
|
|
10
33
|
## [0.4.0] - 2026-01-30
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Api
|
|
5
|
+
# Thin wrapper for the Slack saved.list API endpoint
|
|
6
|
+
# Used to fetch "Save for Later" items
|
|
7
|
+
class Saved
|
|
8
|
+
def initialize(api_client, workspace)
|
|
9
|
+
@api = api_client
|
|
10
|
+
@workspace = workspace
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# List saved items
|
|
14
|
+
# @param filter [String] Filter type: 'saved', 'in_progress', 'completed', 'archived'
|
|
15
|
+
# @param limit [Integer] Number of items to return (default: 15)
|
|
16
|
+
# @param cursor [String, nil] Pagination cursor
|
|
17
|
+
# @return [Hash] API response with 'ok' and 'saved_items' keys
|
|
18
|
+
# @raise [ApiError] if the API call fails (network error, auth error, etc.)
|
|
19
|
+
def list(filter: 'saved', limit: 15, cursor: nil)
|
|
20
|
+
params = { filter: filter, limit: limit.to_s }
|
|
21
|
+
params[:cursor] = cursor if cursor
|
|
22
|
+
@api.post_form(@workspace, 'saved.list', params)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/slk/cli.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Slk
|
|
|
13
13
|
'unread' => Commands::Unread,
|
|
14
14
|
'catchup' => Commands::Catchup,
|
|
15
15
|
'activity' => Commands::Activity,
|
|
16
|
+
'later' => Commands::Later,
|
|
16
17
|
'search' => Commands::Search,
|
|
17
18
|
'preset' => Commands::Preset,
|
|
18
19
|
'workspaces' => Commands::Workspaces,
|
|
@@ -25,6 +26,7 @@ module Slk
|
|
|
25
26
|
def initialize(argv, output: nil)
|
|
26
27
|
@argv = argv.dup
|
|
27
28
|
@output = output || Formatters::Output.new
|
|
29
|
+
@output_injected = !output.nil?
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def run
|
|
@@ -118,13 +120,22 @@ module Slk
|
|
|
118
120
|
def build_runner(args)
|
|
119
121
|
verbose = verbose_mode?(args)
|
|
120
122
|
very_verbose = args.include?('-vv') || args.include?('--very-verbose')
|
|
121
|
-
|
|
123
|
+
markdown = args.include?('--markdown')
|
|
124
|
+
output = @output_injected ? @output : build_output(verbose: verbose, markdown: markdown)
|
|
122
125
|
runner = Runner.new(output: output)
|
|
123
126
|
setup_verbose_logging(runner, output) if verbose
|
|
124
127
|
setup_very_verbose_logging(runner, output) if very_verbose
|
|
125
128
|
runner
|
|
126
129
|
end
|
|
127
130
|
|
|
131
|
+
def build_output(verbose:, markdown:)
|
|
132
|
+
if markdown
|
|
133
|
+
Formatters::MarkdownOutput.new(verbose: verbose)
|
|
134
|
+
else
|
|
135
|
+
Formatters::Output.new(verbose: verbose)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
128
139
|
def setup_verbose_logging(runner, output)
|
|
129
140
|
runner.api_client.on_request = lambda { |method, count|
|
|
130
141
|
output.debug("[API ##{count}] #{method}")
|
|
@@ -118,7 +118,7 @@ module Slk
|
|
|
118
118
|
output: output,
|
|
119
119
|
enricher: enricher(workspace),
|
|
120
120
|
emoji_replacer: runner.emoji_replacer,
|
|
121
|
-
|
|
121
|
+
text_processor: runner.text_processor,
|
|
122
122
|
on_debug: ->(msg) { debug(msg) }
|
|
123
123
|
)
|
|
124
124
|
end
|
|
@@ -131,20 +131,14 @@ module Slk
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def fetch_message(workspace, channel_id, message_ts)
|
|
134
|
-
|
|
135
|
-
return nil unless response['ok'] && response['messages']&.any?
|
|
136
|
-
|
|
137
|
-
response['messages'].find { |msg| msg['ts'] == message_ts }
|
|
138
|
-
rescue ApiError => e
|
|
139
|
-
debug("Could not fetch message #{message_ts} from #{channel_id}: #{e.message}")
|
|
140
|
-
nil
|
|
134
|
+
message_resolver(workspace).fetch_by_ts(channel_id, message_ts)
|
|
141
135
|
end
|
|
142
136
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
def message_resolver(workspace)
|
|
138
|
+
Services::MessageResolver.new(
|
|
139
|
+
conversations_api: runner.conversations_api(workspace.name),
|
|
140
|
+
on_debug: ->(msg) { debug(msg) }
|
|
141
|
+
)
|
|
148
142
|
end
|
|
149
143
|
end
|
|
150
144
|
# rubocop:enable Metrics/ClassLength
|
data/lib/slk/commands/base.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Slk
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def base_options
|
|
35
|
-
{ workspace: nil, all: false, verbose: false, quiet: false, json: false, width: default_width }
|
|
35
|
+
{ workspace: nil, all: false, verbose: false, quiet: false, json: false, markdown: false, width: default_width }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def formatting_options
|
|
@@ -72,6 +72,7 @@ module Slk
|
|
|
72
72
|
when '-vv', '--very-verbose' then @options[:verbose] = @options[:very_verbose] = true
|
|
73
73
|
when '-q', '--quiet' then @options[:quiet] = true
|
|
74
74
|
when '--json' then @options[:json] = true
|
|
75
|
+
when '--markdown' then @options[:markdown] = true
|
|
75
76
|
when '-h', '--help' then @options[:help] = true
|
|
76
77
|
when '--no-emoji' then @options[:no_emoji] = true
|
|
77
78
|
when '--no-reactions' then @options[:no_reactions] = true
|
data/lib/slk/commands/help.rb
CHANGED
|
@@ -45,6 +45,8 @@ module Slk
|
|
|
45
45
|
#{output.cyan('messages')} Read channel or DM messages
|
|
46
46
|
#{output.cyan('search')} Search messages across channels
|
|
47
47
|
#{output.cyan('unread')} View and clear unread messages
|
|
48
|
+
#{output.cyan('activity')} Show activity feed (reactions, mentions, threads)
|
|
49
|
+
#{output.cyan('later')} Show saved "Later" items
|
|
48
50
|
#{output.cyan('preset')} Manage and apply status presets
|
|
49
51
|
#{output.cyan('workspaces')} Manage Slack workspaces
|
|
50
52
|
#{output.cyan('cache')} Manage user/channel cache
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
require_relative '../support/help_formatter'
|
|
5
|
+
|
|
6
|
+
module Slk
|
|
7
|
+
module Commands
|
|
8
|
+
# Displays saved "Later" items from Slack
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class Later < Base
|
|
11
|
+
include Support::InlineImages
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
result = validate_options
|
|
15
|
+
return result if result
|
|
16
|
+
|
|
17
|
+
workspace = target_workspaces.first
|
|
18
|
+
fetch_and_display_later_items(workspace)
|
|
19
|
+
rescue ApiError => e
|
|
20
|
+
error("Failed to fetch saved items: #{e.message}")
|
|
21
|
+
1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def fetch_and_display_later_items(workspace)
|
|
27
|
+
api = runner.saved_api(workspace.name)
|
|
28
|
+
response = api.list(filter: filter_type, limit: @options[:limit])
|
|
29
|
+
|
|
30
|
+
return error_result(response) unless response['ok']
|
|
31
|
+
|
|
32
|
+
items = parse_items(response['saved_items'] || [])
|
|
33
|
+
display_items(workspace, items)
|
|
34
|
+
0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error_result(response)
|
|
38
|
+
error("Failed to fetch saved items: #{response['error']}")
|
|
39
|
+
1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse_items(items_data)
|
|
43
|
+
items_data.map { |data| Models::SavedItem.from_api(data) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def filter_type
|
|
47
|
+
if @options[:completed]
|
|
48
|
+
'completed'
|
|
49
|
+
elsif @options[:in_progress]
|
|
50
|
+
'in_progress'
|
|
51
|
+
else
|
|
52
|
+
'saved'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def display_items(workspace, items)
|
|
57
|
+
if @options[:counts]
|
|
58
|
+
display_counts(items)
|
|
59
|
+
elsif @options[:json]
|
|
60
|
+
output_json(build_json_output(workspace, items))
|
|
61
|
+
else
|
|
62
|
+
display_formatted(workspace, items)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def display_counts(items)
|
|
67
|
+
puts "Total: #{items.size}"
|
|
68
|
+
puts "Overdue: #{items.count(&:overdue?)}" unless @options[:completed]
|
|
69
|
+
puts "With due dates: #{items.count(&:due_date?)}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# rubocop:disable Metrics/MethodLength
|
|
73
|
+
def build_json_output(workspace, items)
|
|
74
|
+
items.map do |item|
|
|
75
|
+
json_item = {
|
|
76
|
+
channel_id: item.channel_id,
|
|
77
|
+
ts: item.ts,
|
|
78
|
+
state: item.state,
|
|
79
|
+
date_created: item.date_created,
|
|
80
|
+
date_due: item.date_due,
|
|
81
|
+
date_completed: item.date_completed,
|
|
82
|
+
overdue: item.overdue?
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Fetch message content unless --no-content
|
|
86
|
+
unless @options[:no_content]
|
|
87
|
+
message = fetch_message_content(workspace, item)
|
|
88
|
+
json_item[:message] = message if message
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
json_item
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
# rubocop:enable Metrics/MethodLength
|
|
95
|
+
|
|
96
|
+
# rubocop:disable Metrics/MethodLength
|
|
97
|
+
def display_formatted(workspace, items)
|
|
98
|
+
if items.empty?
|
|
99
|
+
puts 'No saved items found.'
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@fetch_failures = 0
|
|
104
|
+
# Skip workspace emoji inline images in markdown mode (they're terminal escape sequences)
|
|
105
|
+
if @options[:workspace_emoji] && inline_images_supported? && !@options[:markdown]
|
|
106
|
+
display_with_workspace_emoji(workspace, items)
|
|
107
|
+
else
|
|
108
|
+
display_without_workspace_emoji(workspace, items)
|
|
109
|
+
end
|
|
110
|
+
show_fetch_failure_summary
|
|
111
|
+
end
|
|
112
|
+
# rubocop:enable Metrics/MethodLength
|
|
113
|
+
|
|
114
|
+
def display_without_workspace_emoji(workspace, items)
|
|
115
|
+
formatter = build_formatter(output)
|
|
116
|
+
items.each do |item|
|
|
117
|
+
message = @options[:no_content] ? nil : fetch_message_content(workspace, item)
|
|
118
|
+
formatter.display_item(item, workspace, message: message, width: display_width, truncate: @options[:truncate])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def display_with_workspace_emoji(workspace, items)
|
|
123
|
+
# Capture output to StringIO, then reprint with workspace emoji
|
|
124
|
+
buffer = StringIO.new
|
|
125
|
+
buffer_output = create_buffer_output(buffer)
|
|
126
|
+
formatter = build_formatter(buffer_output)
|
|
127
|
+
|
|
128
|
+
items.each do |item|
|
|
129
|
+
message = @options[:no_content] ? nil : fetch_message_content(workspace, item)
|
|
130
|
+
formatter.display_item(item, workspace, message: message, width: display_width, truncate: @options[:truncate])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Reprint each line with workspace emoji replacement
|
|
134
|
+
buffer.string.each_line { |line| print_with_workspace_emoji(line.chomp, workspace) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def show_fetch_failure_summary
|
|
138
|
+
return if @fetch_failures.zero?
|
|
139
|
+
|
|
140
|
+
output.puts output.gray("Note: Could not load content for #{@fetch_failures} item(s). Use -v for details.")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def display_width
|
|
144
|
+
@options[:width]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def build_formatter(out)
|
|
148
|
+
Formatters::SavedItemFormatter.new(
|
|
149
|
+
output: out,
|
|
150
|
+
mention_replacer: runner.mention_replacer,
|
|
151
|
+
text_processor: runner.text_processor,
|
|
152
|
+
on_debug: ->(msg) { debug(msg) }
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Create a buffer output using the same class as the main output
|
|
157
|
+
# This ensures markdown mode works correctly with workspace emoji buffering
|
|
158
|
+
def create_buffer_output(buffer)
|
|
159
|
+
if @options[:markdown]
|
|
160
|
+
Formatters::MarkdownOutput.new(io: buffer, err: $stderr)
|
|
161
|
+
else
|
|
162
|
+
Formatters::Output.new(io: buffer, err: $stderr, color: $stdout.tty?)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def fetch_message_content(workspace, item)
|
|
167
|
+
return nil unless item.ts && item.channel_id
|
|
168
|
+
|
|
169
|
+
result = message_resolver(workspace).fetch_by_ts(item.channel_id, item.ts)
|
|
170
|
+
@fetch_failures += 1 if result.nil?
|
|
171
|
+
result
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def message_resolver(workspace)
|
|
175
|
+
Services::MessageResolver.new(
|
|
176
|
+
conversations_api: runner.conversations_api(workspace.name),
|
|
177
|
+
on_debug: ->(msg) { debug(msg) }
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
protected
|
|
182
|
+
|
|
183
|
+
def default_options
|
|
184
|
+
super.merge(
|
|
185
|
+
limit: 15,
|
|
186
|
+
completed: false,
|
|
187
|
+
in_progress: false,
|
|
188
|
+
counts: false,
|
|
189
|
+
no_content: false,
|
|
190
|
+
truncate: false,
|
|
191
|
+
workspace_emoji: false
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def handle_option(arg, args, remaining)
|
|
196
|
+
case arg
|
|
197
|
+
when '-n', '--limit' then @options[:limit] = args.shift.to_i
|
|
198
|
+
when '--completed' then @options[:completed] = true
|
|
199
|
+
when '--in-progress' then @options[:in_progress] = true
|
|
200
|
+
when '--counts' then @options[:counts] = true
|
|
201
|
+
when '--no-content' then @options[:no_content] = true
|
|
202
|
+
when '--workspace-emoji' then @options[:workspace_emoji] = true
|
|
203
|
+
else super
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Override to intercept --no-wrap before base class handles it
|
|
208
|
+
def parse_single_option(arg, args, remaining)
|
|
209
|
+
if arg == '--no-wrap'
|
|
210
|
+
@options[:truncate] = true
|
|
211
|
+
@options[:width] ||= 140
|
|
212
|
+
else
|
|
213
|
+
super
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def default_width
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def help_text
|
|
222
|
+
help = Support::HelpFormatter.new('slk later [options]')
|
|
223
|
+
help.description('Show saved "Later" items from Slack.')
|
|
224
|
+
add_options_section(help)
|
|
225
|
+
help.render
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# rubocop:disable Metrics/MethodLength
|
|
229
|
+
def add_options_section(help)
|
|
230
|
+
help.section('OPTIONS') do |s|
|
|
231
|
+
s.option('-n, --limit N', 'Number of items (default: 15)')
|
|
232
|
+
s.option('--completed', 'Show completed items instead')
|
|
233
|
+
s.option('--in-progress', 'Show in-progress items')
|
|
234
|
+
s.option('--counts', 'Show summary counts only')
|
|
235
|
+
s.option('--no-content', 'Skip fetching message text')
|
|
236
|
+
s.option('--workspace-emoji', 'Show workspace emoji as inline images')
|
|
237
|
+
s.option('--no-emoji', 'Show :emoji: codes instead of unicode')
|
|
238
|
+
s.option('--width N', 'Wrap text at N columns')
|
|
239
|
+
s.option('--no-wrap', 'Truncate to single line instead of wrapping')
|
|
240
|
+
add_common_options(s)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# rubocop:enable Metrics/MethodLength
|
|
245
|
+
|
|
246
|
+
def add_common_options(section)
|
|
247
|
+
section.option('--json', 'Output as JSON')
|
|
248
|
+
section.option('-w, --workspace', 'Specify workspace')
|
|
249
|
+
section.option('-v, --verbose', 'Show debug information')
|
|
250
|
+
section.option('-q, --quiet', 'Suppress output')
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Print text, replacing workspace emoji codes with inline images
|
|
254
|
+
def print_with_workspace_emoji(text, workspace)
|
|
255
|
+
print_line_with_emoji_images(text, workspace)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def print_line_with_emoji_images(text, workspace)
|
|
259
|
+
emoji_pattern = /:([a-zA-Z0-9_+-]+):/
|
|
260
|
+
parts = text.split(emoji_pattern)
|
|
261
|
+
|
|
262
|
+
parts.each_with_index { |part, i| print_emoji_part(part, i, workspace) }
|
|
263
|
+
puts
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def print_emoji_part(part, index, workspace)
|
|
267
|
+
if index.odd?
|
|
268
|
+
print_emoji_or_code(part, workspace)
|
|
269
|
+
else
|
|
270
|
+
print part
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def print_emoji_or_code(emoji_name, workspace)
|
|
275
|
+
emoji_path = find_workspace_emoji(workspace.name, emoji_name)
|
|
276
|
+
if emoji_path
|
|
277
|
+
print_inline_image(emoji_path, height: 1)
|
|
278
|
+
print ' ' unless in_tmux?
|
|
279
|
+
else
|
|
280
|
+
print ":#{emoji_name}:"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def find_workspace_emoji(workspace_name, emoji_name)
|
|
285
|
+
return nil if emoji_name.empty?
|
|
286
|
+
|
|
287
|
+
paths = Support::XdgPaths.new
|
|
288
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
289
|
+
workspace_dir = File.join(emoji_dir, workspace_name)
|
|
290
|
+
return nil unless Dir.exist?(workspace_dir)
|
|
291
|
+
|
|
292
|
+
Dir.glob(File.join(workspace_dir, "#{emoji_name}.*")).first
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
# rubocop:enable Metrics/ClassLength
|
|
296
|
+
end
|
|
297
|
+
end
|
|
@@ -192,10 +192,10 @@ module Slk
|
|
|
192
192
|
raw = if thread_ts
|
|
193
193
|
fetch_thread_messages(api, channel_id, thread_ts)
|
|
194
194
|
else
|
|
195
|
-
fetch_channel_history(api, channel_id, oldest)
|
|
195
|
+
fetch_channel_history(api, channel_id, oldest).reverse
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
raw.map { |m| Models::Message.from_api(m, channel_id: channel_id) }
|
|
198
|
+
raw.map { |m| Models::Message.from_api(m, channel_id: channel_id) }
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
def fetch_thread_messages(api, channel_id, thread_ts)
|
|
@@ -295,8 +295,9 @@ module Slk
|
|
|
295
295
|
end
|
|
296
296
|
|
|
297
297
|
# Print text, replacing workspace emoji codes with inline images when enabled
|
|
298
|
+
# Skip inline images in markdown mode (they're terminal escape sequences)
|
|
298
299
|
def print_with_workspace_emoji(text, workspace)
|
|
299
|
-
if @options[:workspace_emoji] && inline_images_supported?
|
|
300
|
+
if @options[:workspace_emoji] && inline_images_supported? && !@options[:markdown]
|
|
300
301
|
print_line_with_emoji_images(text, workspace)
|
|
301
302
|
else
|
|
302
303
|
puts text
|
|
@@ -5,11 +5,11 @@ module Slk
|
|
|
5
5
|
# Formats activity feed items for terminal display
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class ActivityFormatter
|
|
8
|
-
def initialize(output:, enricher:, emoji_replacer:,
|
|
8
|
+
def initialize(output:, enricher:, emoji_replacer:, text_processor:, on_debug: nil)
|
|
9
9
|
@output = output
|
|
10
10
|
@enricher = enricher
|
|
11
11
|
@emoji = emoji_replacer
|
|
12
|
-
@
|
|
12
|
+
@text_processor = text_processor
|
|
13
13
|
@on_debug = on_debug
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -119,10 +119,7 @@ module Slk
|
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def prepare_message_text(message, workspace)
|
|
122
|
-
|
|
123
|
-
return '[No text]' if text.empty?
|
|
124
|
-
|
|
125
|
-
@mentions.replace(text, workspace)
|
|
122
|
+
@text_processor.process(message['text'], workspace)
|
|
126
123
|
end
|
|
127
124
|
|
|
128
125
|
def display_additional_lines(lines)
|
|
@@ -136,7 +133,7 @@ module Slk
|
|
|
136
133
|
end
|
|
137
134
|
|
|
138
135
|
def format_time(slack_timestamp)
|
|
139
|
-
Time.at(slack_timestamp.to_f).strftime('%b %d %-I:%M %p')
|
|
136
|
+
Time.at(slack_timestamp.to_f).strftime('%b %d %-I:%M:%S %p')
|
|
140
137
|
end
|
|
141
138
|
|
|
142
139
|
def debug_missing(item_type)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Formatters
|
|
5
|
+
# Markdown output adapter with same interface as Output
|
|
6
|
+
# Produces markdown formatting instead of ANSI colors
|
|
7
|
+
class MarkdownOutput
|
|
8
|
+
attr_reader :verbose, :quiet
|
|
9
|
+
|
|
10
|
+
# Accept color: parameter for interface compatibility with Output (ignored for markdown)
|
|
11
|
+
def initialize(io: $stdout, err: $stderr, color: nil, verbose: false, quiet: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
12
|
+
@io = io
|
|
13
|
+
@err = err
|
|
14
|
+
@verbose = verbose
|
|
15
|
+
@quiet = quiet
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def puts(message = '')
|
|
19
|
+
@io.puts(message) unless @quiet
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def print(message)
|
|
23
|
+
@io.print(message) unless @quiet
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def error(message)
|
|
27
|
+
@err.puts("**Error:** #{message}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def warn(message)
|
|
31
|
+
@err.puts("*Warning:* #{message}") unless @quiet
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def success(message)
|
|
35
|
+
puts("✓ #{message}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def info(message)
|
|
39
|
+
puts(message)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def debug(message)
|
|
43
|
+
return unless @verbose
|
|
44
|
+
|
|
45
|
+
@err.puts("*[debug]* #{message}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Markdown formatting helpers - string interpolation handles nil conversion
|
|
49
|
+
# bold -> **text**
|
|
50
|
+
def bold(text)
|
|
51
|
+
"**#{text}**"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# red -> **text** (emphasis for errors)
|
|
55
|
+
def red(text)
|
|
56
|
+
"**#{text}**"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# green -> plain text (success doesn't need markup)
|
|
60
|
+
def green(text)
|
|
61
|
+
text.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# yellow -> *text* (italics for warnings)
|
|
65
|
+
def yellow(text)
|
|
66
|
+
"*#{text}*"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# blue -> `text` (code for timestamps)
|
|
70
|
+
def blue(text)
|
|
71
|
+
"`#{text}`"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# magenta -> *text* (italics for secondary)
|
|
75
|
+
def magenta(text)
|
|
76
|
+
"*#{text}*"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# cyan -> `text` (code for metadata like timestamps)
|
|
80
|
+
def cyan(text)
|
|
81
|
+
"`#{text}`"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# gray -> *text* (italics for secondary info)
|
|
85
|
+
def gray(text)
|
|
86
|
+
"*#{text}*"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def with_verbose(value)
|
|
90
|
+
self.class.new(io: @io, err: @err, verbose: value, quiet: @quiet)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def with_quiet(value)
|
|
94
|
+
self.class.new(io: @io, err: @err, verbose: @verbose, quiet: value)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|