slk 0.9.0 → 0.11.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.
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Commands
5
+ # All indexed messages sent by the authenticated user on a date/range.
6
+ # rubocop:disable Metrics/ClassLength
7
+ class Sent < Base
8
+ def execute
9
+ result = validate_options
10
+ return result if result
11
+
12
+ run_sent
13
+ rescue ApiError => e
14
+ error("Sent search failed: #{e.message}")
15
+ 1
16
+ end
17
+
18
+ protected
19
+
20
+ def default_options
21
+ super.merge(since: nil, mine: false, changed_since: nil, lookback: 7, context: 2,
22
+ before: Services::SentConversations::DEFAULT_BEFORE,
23
+ after_minutes: Services::SentConversations::DEFAULT_AFTER_MINUTES,
24
+ max: Services::SentConversations::DEFAULT_MAX)
25
+ end
26
+
27
+ def default_width
28
+ return nil unless $stdout.tty?
29
+
30
+ columns = IO.console&.winsize&.last
31
+ columns&.positive? ? columns : super
32
+ rescue SystemCallError, IOError, NotImplementedError
33
+ super
34
+ end
35
+
36
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
37
+ def handle_option(arg, args, remaining)
38
+ case arg
39
+ when '--since' then @options[:since] = option_value(arg, args)
40
+ when '--changed-since' then @options[:changed_since] = option_value(arg, args)
41
+ when '--lookback' then @options[:lookback] = positive_integer(arg, args)
42
+ when '--context' then @options[:context] = nonnegative_integer(arg, args)
43
+ when '--mine' then @options[:mine] = true
44
+ when '--before' then @options[:before] = before_limit(arg, args)
45
+ when '--after-minutes' then @options[:after_minutes] = nonnegative_integer(arg, args)
46
+ when '--max' then @options[:max] = nonnegative_integer(arg, args)
47
+ else super
48
+ end
49
+ end
50
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
51
+
52
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
53
+ def help_text
54
+ help = Support::HelpFormatter.new('slk sent [today|yesterday|YYYY-MM-DD] [options]')
55
+ help.description('Catch up on conversations you posted in, across all workspaces.')
56
+ help.note('Search is indexed: very recent and deleted messages may be missing.')
57
+ help.note('Dates/DM history bounds use your local clock; Slack search uses your profile timezone.')
58
+ help.note('Long --since ranges can be slow (search is rate-limited; 429s retry once).')
59
+ help.note('Conversations you only read or were mentioned in but never posted in are out of scope.')
60
+ help.note('A future --mentions flag could seed mentioned conversations via to:me search.')
61
+ help.note('JSON: {date, range, conversations:[{workspace, channel_id, channel_name, channel_label, type,')
62
+ help.note('thread_ts, last_speaker_is_me, dropped_messages, messages:[{ts, user,')
63
+ help.note('user_name, text, mine, thread_ts}]}]}; --mine keeps counts/results JSON,')
64
+ help.note('with channel_label added to counts. channel_name remains the raw Slack name.')
65
+ help.note('JSON messages stay flat and timestamp-sorted; thread_ts links replies to their parent.')
66
+ help.note('Text output nests expanded replies under their parent, even when sent later, and wraps')
67
+ help.note('to the terminal width on a TTY. Use --width N to override or --no-wrap to disable.')
68
+ help.note('Thread headings show the thread ID; channel and DM headings keep their readable names.')
69
+ help.note('A thin divider separates conversations, not messages within a conversation.')
70
+ help.note('For --changed-since, H:MM or HH:MM means the most recent local occurrence: if that time has not')
71
+ help.note('arrived today, it means yesterday. Dated timestamps in the future are rejected.')
72
+ help.note('--changed-since is stateless: exact-ts history/replies detect new messages; search only finds')
73
+ help.note('the watch set. Edits/reactions do not change ts and are invisible. Search index lag may')
74
+ help.note('omit posts from the last few minutes. Conversations you never posted in remain out of scope.')
75
+ help.note('Unread followed threads supplement the watch set when available; --lookback must include')
76
+ help.note('the days you posted in any other threads you want to watch.')
77
+ help.note('Changed text marks earlier context with · and puts the most recently active conversation last.')
78
+ help.note('A wide lookback checks many thread roots and can take minutes under Slack rate limits.')
79
+ help.note('Changed JSON adds changed_since:{iso, ts}, lookback_days, new_count, new_from_others,')
80
+ help.note('and per-message new. Its messages stay flat and timestamp-sorted with thread_ts links.')
81
+ help.section('OPTIONS') do |s|
82
+ s.option('--since YYYY-MM-DD', 'From this date through today (inclusive)')
83
+ s.option('--changed-since TIME', 'Only changed conversations (H:MM/HH:MM, ISO, epoch, 90m, 2h, 1d)')
84
+ s.option('--lookback DAYS', 'Days of sent messages to watch (default: 7; changed mode)')
85
+ s.option('--context N', 'Earlier messages per changed conversation (default: 2)')
86
+ s.option('--mine', 'Only your messages, flat timeline with counts (previous behavior)')
87
+ s.option('--before N', 'Channel messages before a window (default: 5, max: 200)')
88
+ s.option('--after-minutes N', 'Channel window after each post (default: 30)')
89
+ s.option('--max N', 'Messages per conversation (default: 200; 0 for all)')
90
+ s.option('-w, --workspace NAME', 'Search one workspace instead of all')
91
+ s.option('--all', 'Search all workspaces (default)')
92
+ s.option('--json', 'Output conversations as JSON (or counts/results with --mine)')
93
+ s.option('--width N', 'Wrap text at N columns (default: terminal width on TTY)')
94
+ s.option('--no-wrap', 'Disable text wrapping')
95
+ end
96
+ help.section('EXAMPLES') do |s|
97
+ s.example('slk sent', 'Today in all workspaces')
98
+ s.example('slk sent yesterday', 'Yesterday in all workspaces')
99
+ s.example('slk sent 2026-09-19', 'One specific day')
100
+ s.example('slk sent --since 2026-09-15', 'From Sep 15 through today')
101
+ s.example('slk sent --mine --json', 'Only your sent messages, flat JSON')
102
+ s.example('slk sent --changed-since 15:17 --lookback 3', 'Diff watched conversations since 15:17')
103
+ end
104
+ help.render
105
+ end
106
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
107
+
108
+ private
109
+
110
+ def run_sent
111
+ raise UsageError, 'Use either --all or --workspace, not both.' if @options[:all] && @options[:workspace]
112
+
113
+ return run_changes if @options[:changed_since]
114
+
115
+ query = date_query
116
+ entries = collect_entries(query)
117
+ @options[:mine] ? emit(entries, query) : emit_context(entries)
118
+ 0
119
+ end
120
+
121
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
122
+ def run_changes
123
+ validate_changed_options
124
+ since = Support::CheckInTime.parse(@options[:changed_since])
125
+ entries = collect_entries(changed_query)
126
+ changes = Services::SentChanges.new(
127
+ runner: runner, since: since, context: @options[:context], max: @options[:max],
128
+ before: @options[:before], after_minutes: @options[:after_minutes]
129
+ ).collect(entries, workspaces: selected_workspaces)
130
+ Formatters::SentFormatter.new(runner: runner, options: format_options).display_changes(
131
+ changes, changed_since: since, lookback_days: @options[:lookback], json: @options[:json]
132
+ )
133
+ 0
134
+ end
135
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
136
+
137
+ def validate_changed_options
138
+ if positional_args.any? || @options[:since]
139
+ raise UsageError, 'Use --changed-since alone, not with a date or --since.'
140
+ end
141
+ raise UsageError, 'Use --changed-since without --mine.' if @options[:mine]
142
+ end
143
+
144
+ def changed_query
145
+ start_date = Date.today - (@options[:lookback] - 1)
146
+ "from:me after:#{start_date - 1} before:#{Date.today + 1}"
147
+ end
148
+
149
+ def positive_integer(flag, args)
150
+ number = nonnegative_integer(flag, args)
151
+ raise UsageError, "#{flag} expects a positive integer." if number.zero?
152
+
153
+ number
154
+ end
155
+
156
+ def before_limit(flag, args)
157
+ number = nonnegative_integer(flag, args)
158
+ raise UsageError, '--before must be at most 200.' if number > 200
159
+
160
+ number
161
+ end
162
+
163
+ def nonnegative_integer(flag, args)
164
+ value = option_value(flag, args)
165
+ number = Integer(value, exception: false)
166
+ raise UsageError, "#{flag} expects a non-negative integer." unless number && !number.negative?
167
+
168
+ number
169
+ end
170
+
171
+ def date_query
172
+ raise UsageError, 'Use either a date or --since, not both.' if @options[:since] && positional_args.any?
173
+ raise UsageError, 'Expected one date: today, yesterday, or YYYY-MM-DD.' if positional_args.length > 1
174
+
175
+ @options[:since] ? range_query : day_query
176
+ end
177
+
178
+ def range_query
179
+ since = parse_date(@options[:since])
180
+ raise UsageError, '--since must not be in the future.' if since > Date.today
181
+
182
+ "from:me after:#{since - 1} before:#{Date.today + 1}"
183
+ end
184
+
185
+ def day_query
186
+ date = case positional_args.first
187
+ when nil, 'today' then Date.today
188
+ when 'yesterday' then Date.today - 1
189
+ else parse_date(positional_args.first)
190
+ end
191
+ "from:me on:#{date}"
192
+ end
193
+
194
+ def parse_date(value)
195
+ raise UsageError, "Invalid date: #{value.inspect}. Use YYYY-MM-DD." unless value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
196
+
197
+ Date.iso8601(value)
198
+ rescue Date::Error
199
+ raise UsageError, "Invalid date: #{value.inspect}. Use YYYY-MM-DD."
200
+ end
201
+
202
+ def collect_entries(query)
203
+ entries = selected_workspaces.flat_map { |workspace| fetch_workspace(workspace, query) }
204
+ entries.sort_by { |workspace, result| [result.ts.to_f, workspace.name, result.channel_id.to_s] }
205
+ end
206
+
207
+ def selected_workspaces
208
+ @options[:workspace] ? [runner.workspace(@options[:workspace])] : runner.all_workspaces
209
+ end
210
+
211
+ def fetch_workspace(workspace, query)
212
+ pages = Services::SearchPages.new(runner.search_api(workspace.name)).fetch(query: query, sort_dir: 'asc')
213
+ pages[:results].map { |result| [workspace, result] }
214
+ end
215
+
216
+ def emit_context(entries)
217
+ start_date, end_date = selected_dates
218
+ conversations = Services::SentConversations.new(
219
+ runner: runner, start_date: start_date, end_date: end_date,
220
+ before: @options[:before], after_minutes: @options[:after_minutes], max: @options[:max]
221
+ ).collect(entries)
222
+ Formatters::SentFormatter.new(runner: runner, options: format_options).display(
223
+ conversations, start_date: start_date, end_date: end_date, json: @options[:json]
224
+ )
225
+ end
226
+
227
+ def selected_dates
228
+ return [parse_date(@options[:since]), Date.today] if @options[:since]
229
+
230
+ date = case positional_args.first
231
+ when nil, 'today' then Date.today
232
+ when 'yesterday' then Date.today - 1
233
+ else parse_date(positional_args.first)
234
+ end
235
+ [date, date]
236
+ end
237
+
238
+ def emit(entries, query)
239
+ counts = entries.group_by { |workspace, result| [workspace.name, result.channel_id] }
240
+ if @options[:json]
241
+ output_json(query: query, counts: json_counts(counts), results: entries.map do |workspace, result|
242
+ result.to_h.merge(workspace: workspace.name)
243
+ end)
244
+ else
245
+ display_counts(counts, entries)
246
+ end
247
+ end
248
+
249
+ def json_counts(counts)
250
+ counts.map do |(workspace, channel_id), group|
251
+ result = group.first.last
252
+ { workspace: workspace, channel_id: channel_id, channel_name: result.channel_name,
253
+ channel_label: destination_label(runner.workspace(workspace), result),
254
+ channel_type: result.channel_type, count: group.size }
255
+ end
256
+ end
257
+
258
+ def display_counts(counts, entries)
259
+ if entries.empty?
260
+ puts 'No sent messages found.'
261
+ return
262
+ end
263
+
264
+ counts.each_value { |group| display_count(group) }
265
+ puts
266
+ entries.each { |workspace, result| display_entry(workspace, result) }
267
+ end
268
+
269
+ def display_entry(workspace, result)
270
+ options = format_options.merge(workspace_label: true, channel_label: destination_label(workspace, result))
271
+ runner.search_formatter.display_result(result, workspace, options)
272
+ end
273
+
274
+ def display_count(group)
275
+ workspace, result = group.first
276
+ puts "[#{workspace.name}] #{destination_label(workspace, result)}: #{group.size}"
277
+ end
278
+
279
+ def destination_label(workspace, result)
280
+ runner.sent_channel_label.label(
281
+ workspace: workspace, type: result.channel_type, name: result.channel_name,
282
+ channel_id: result.channel_id, self_user_id: result.user_id, self_username: result.username
283
+ )
284
+ end
285
+ end
286
+ # rubocop:enable Metrics/ClassLength
287
+ end
288
+ end
@@ -32,7 +32,7 @@ module Slk
32
32
  format_author(attachment, lines)
33
33
  format_text(att_text, lines, options) if att_text && block_images.empty?
34
34
  format_image(attachment, lines, options, message_ts: message_ts, index: index) if image_url
35
- block_images.each { |img| lines << "> [Image: #{img}]" }
35
+ format_block_images(block_images, lines)
36
36
  end
37
37
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
38
38
 
@@ -46,9 +46,13 @@ module Slk
46
46
  end
47
47
  end
48
48
 
49
+ def format_block_images(images, lines)
50
+ images.each { |img| lines << "> [Image: #{@text_processor.call(img)}]" }
51
+ end
52
+
49
53
  def format_author(attachment, lines)
50
54
  author = attachment['author_name'] || attachment['author_subname']
51
- lines << "> #{@output.bold(author)}:" if author
55
+ lines << "> #{@output.bold(@text_processor.call(author))}:" if author
52
56
  end
53
57
 
54
58
  def format_text(att_text, lines, options)
@@ -73,7 +77,7 @@ module Slk
73
77
  else
74
78
  image_url = attachment['image_url'] || attachment['thumb_url']
75
79
  filename = attachment['title'] || extract_filename(image_url)
76
- lines << "> [Image: #{filename}]"
80
+ lines << "> [Image: #{@text_processor.call(filename)}]"
77
81
  end
78
82
  end
79
83
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Formatters
5
+ # RFC 4180 CSV, hand-rolled: Ruby 3.4 moved csv out of the default gems
6
+ # and into the bundled ones, so requiring it would make this tool depend
7
+ # on a gem, and it ships with none.
8
+ #
9
+ # Quotes only the fields that need it, so the common row stays readable in
10
+ # a terminal as well as in a spreadsheet.
11
+ module CsvWriter
12
+ module_function
13
+
14
+ NEEDS_QUOTES = /[",\r\n]|\A\s|\s\z/
15
+
16
+ def row(values)
17
+ values.map { |value| escape(value) }.join(',')
18
+ end
19
+
20
+ def escape(value)
21
+ text = stringify(value)
22
+ return text unless NEEDS_QUOTES.match?(text)
23
+
24
+ %("#{text.gsub('"', '""')}")
25
+ end
26
+
27
+ # nil is an empty cell. Writing the literal "nil" would give a
28
+ # spreadsheet a four-character string to count, sort and average.
29
+ def stringify(value)
30
+ value.nil? ? '' : value.to_s
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Formatters
5
+ # CSV export of a deactivation list, for the spreadsheet that inevitably
6
+ # gets asked for. Every matched row is written, not just the screenful a
7
+ # terminal would show — a truncated export is a wrong answer that looks
8
+ # like a right one.
9
+ class DeactivationCsv
10
+ HEADERS = %w[deactivated_on user_id handle real_name title email bot].freeze
11
+ TENURE_HEADERS = %w[started_on tenure_months tenure].freeze
12
+
13
+ def initialize(output:, tenures: nil)
14
+ @output = output
15
+ @tenures = tenures
16
+ end
17
+
18
+ def render(records)
19
+ @output.puts(CsvWriter.row(headers))
20
+ records.each { |record| @output.puts(CsvWriter.row(cells(record))) }
21
+ end
22
+
23
+ private
24
+
25
+ def headers
26
+ @tenures ? HEADERS + TENURE_HEADERS : HEADERS
27
+ end
28
+
29
+ def cells(record)
30
+ row = [record.date, record.user_id, record.handle, record.real_name,
31
+ record.title, record.email, record.bot]
32
+ @tenures ? row + tenure_cells(record) : row
33
+ end
34
+
35
+ # Empty cells rather than zeros, so a spreadsheet averaging tenure skips
36
+ # them instead of counting people who left the day they arrived. Two
37
+ # different unknowns land here: no start date on file at all, which
38
+ # blanks the whole group, and a start date later than the departure,
39
+ # which keeps started_on so the bad data is visible and blanks the
40
+ # length that cannot be derived from it.
41
+ def tenure_cells(record)
42
+ tenure = @tenures[record.user_id]
43
+ return [nil, nil, nil] unless tenure
44
+
45
+ [tenure.started, tenure.months, tenure.to_s]
46
+ end
47
+ end
48
+ end
49
+ end
@@ -15,11 +15,16 @@ module Slk
15
15
  @width = width || 100
16
16
  end
17
17
 
18
- # One line per departure: date, name, title. Full ISO dates on every row
19
- # (rather than month headings) so the output stays greppable.
20
- def list(records)
18
+ # One line per departure: date, name, tenure, title. Full ISO dates on
19
+ # every row (rather than month headings) so the output stays greppable.
20
+ #
21
+ # `tenures` is keyed by user ID and may cover only some of the rows: a
22
+ # start date nobody filled in leaves the column blank rather than
23
+ # guessing, and the column disappears entirely when none are known.
24
+ def list(records, tenures: {})
21
25
  name_width = name_column_width(records)
22
- records.each { |record| @output.puts(row(record, name_width)) }
26
+ tenure_width = tenure_column_width(records, tenures)
27
+ records.each { |record| @output.puts(row(record, name_width, tenures, tenure_width)) }
23
28
  end
24
29
 
25
30
  def chart(records, from: nil, to: nil)
@@ -61,21 +66,32 @@ module Slk
61
66
  "#{@output.gray(month)} #{count.to_s.rjust(label_width)} #{bar}".rstrip
62
67
  end
63
68
 
64
- def row(record, name_width)
65
- date = record.date || 'unknown'
66
- name = truncate(record.best_name.to_s, name_width).ljust(name_width)
67
- title = truncate(record.title.to_s, title_width(name_width))
68
- line = "#{@output.gray(date.ljust(DATE_WIDTH))} #{name}"
69
+ def row(record, name_width, tenures = {}, tenure_width = 0)
70
+ line = "#{@output.gray((record.date || 'unknown').ljust(DATE_WIDTH))} " \
71
+ "#{truncate(record.best_name.to_s, name_width).ljust(name_width)}"
72
+ line = "#{line} #{@output.gray(tenure_cell(record, tenures, tenure_width))}" if tenure_width.positive?
73
+ title = truncate(record.title.to_s, title_width(name_width, tenure_width))
69
74
  title.empty? ? line : "#{line} #{@output.gray(title)}"
70
75
  end
71
76
 
77
+ def tenure_cell(record, tenures, width)
78
+ tenures[record.user_id].to_s.rjust(width)
79
+ end
80
+
81
+ def tenure_column_width(records, tenures)
82
+ return 0 if tenures.nil? || tenures.empty?
83
+
84
+ records.filter_map { |r| tenures[r.user_id]&.to_s&.length }.max || 0
85
+ end
86
+
72
87
  def name_column_width(records)
73
88
  longest = records.map { |r| r.best_name.to_s.length }.max || 0
74
89
  longest.clamp(8, MAX_NAME_WIDTH)
75
90
  end
76
91
 
77
- def title_width(name_width)
78
- [@width - DATE_WIDTH - name_width - 4, MIN_TITLE_WIDTH].max
92
+ def title_width(name_width, tenure_width = 0)
93
+ tenure_space = tenure_width.positive? ? tenure_width + 2 : 0
94
+ [@width - DATE_WIDTH - name_width - tenure_space - 4, MIN_TITLE_WIDTH].max
79
95
  end
80
96
 
81
97
  # A month nobody left gets no bar at all. Rounding a zero up to one
@@ -26,6 +26,7 @@ module Slk
26
26
  @color = color.nil? ? io.tty? : color
27
27
  @verbose = verbose
28
28
  @quiet = quiet
29
+ @last_progress_width = nil
29
30
  end
30
31
 
31
32
  def puts(message = '')
@@ -52,6 +53,44 @@ module Slk
52
53
  puts(colorize(message))
53
54
  end
54
55
 
56
+ # Transient progress on stderr: it never pollutes piped stdout, and it
57
+ # overwrites itself rather than scrolling. Silent under --quiet, and
58
+ # when stderr is not a terminal, since a log file full of half-drawn
59
+ # counters helps nobody.
60
+ def progress(message)
61
+ return unless progress?
62
+
63
+ @last_progress_width = message.length
64
+ write_progress("\r#{message}")
65
+ end
66
+
67
+ # Erases whatever progress() last drew. Keyed off the saved width rather
68
+ # than re-checking tty state: if a line was drawn, it gets cleaned up.
69
+ def clear_progress
70
+ return unless @last_progress_width
71
+
72
+ write_progress("\r#{' ' * @last_progress_width}\r")
73
+ @last_progress_width = nil
74
+ end
75
+
76
+ # This is decoration. It is often called from an ensure block cleaning
77
+ # up after a real failure, and a closed or broken stderr must not
78
+ # replace that failure with one about drawing a counter.
79
+ def write_progress(text)
80
+ @err.print(text)
81
+ @err.flush
82
+ rescue SystemCallError, IOError
83
+ nil
84
+ end
85
+
86
+ def progress? = tty_err? && !@quiet
87
+
88
+ def tty_err?
89
+ @err.tty?
90
+ rescue SystemCallError, IOError
91
+ false
92
+ end
93
+
55
94
  def debug(message)
56
95
  return unless @verbose
57
96
 
@@ -23,25 +23,38 @@ module Slk
23
23
  # Display a single search result
24
24
  def display_result(result, workspace, options = {})
25
25
  timestamp = @output.blue("[#{format_time(result.timestamp)}]")
26
- channel = @output.cyan(resolve_channel(result, workspace))
26
+ channel = @output.cyan(display_channel_label(result, workspace, options))
27
27
  user = @output.bold("#{resolve_user(result, workspace)}:")
28
28
  text = prepare_text(result.text, workspace, options)
29
29
 
30
- @output.puts "#{timestamp} #{channel} #{user} #{text}"
31
- display_files(result.files) if result.files&.any?
30
+ workspace_label = workspace_prefix(workspace, options)
31
+ @output.puts "#{timestamp} #{workspace_label}#{channel} #{user} #{text}"
32
+ display_files(result.files, workspace, options) if result.files&.any?
32
33
  end
33
34
 
34
- private
35
+ # Channel label used by search timelines and per-channel summaries.
36
+ def channel_label(result, workspace)
37
+ channel_label_for(result.channel_type, result.channel_name, workspace)
38
+ end
35
39
 
36
- def resolve_channel(result, workspace)
37
- if result.dm?
38
- # For DMs, channel_name is a user ID - resolve it
39
- @mentions.replace("<@#{result.channel_name}>", workspace)
40
+ def channel_label_for(type, name, workspace)
41
+ if %w[im mpim].include?(type)
42
+ @mentions.replace("<@#{name}>", workspace)
40
43
  else
41
- "##{result.channel_name}"
44
+ "##{name}"
42
45
  end
43
46
  end
44
47
 
48
+ private
49
+
50
+ def display_channel_label(result, workspace, options)
51
+ options[:channel_label] || channel_label(result, workspace)
52
+ end
53
+
54
+ def workspace_prefix(workspace, options)
55
+ options[:workspace_label] ? "#{@output.cyan("[#{workspace.name}]")} " : ''
56
+ end
57
+
45
58
  def resolve_user(result, workspace)
46
59
  # If we have a username, use it directly
47
60
  return result.username if result.username && !result.username.empty?
@@ -57,9 +70,10 @@ module Slk
57
70
  @text_processor.process(text, workspace, options)
58
71
  end
59
72
 
60
- def display_files(files)
73
+ def display_files(files, workspace, options)
61
74
  files.each do |file|
62
- @output.puts @output.blue("[Image: #{file[:name]}]")
75
+ name = prepare_text(file[:name], workspace, options)
76
+ @output.puts @output.blue("[Image: #{name}]")
63
77
  end
64
78
  end
65
79