slk 0.1.0 → 0.2.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 +22 -1
- data/README.md +5 -5
- data/bin/slk +3 -3
- data/lib/{slack_cli → slk}/api/activity.rb +10 -11
- data/lib/{slack_cli → slk}/api/bots.rb +5 -4
- data/lib/slk/api/client.rb +51 -0
- data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
- data/lib/slk/api/dnd.rb +41 -0
- data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
- data/lib/{slack_cli → slk}/api/threads.rb +13 -12
- data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
- data/lib/slk/api/users.rb +105 -0
- data/lib/slk/cli.rb +157 -0
- data/lib/slk/commands/activity.rb +152 -0
- data/lib/{slack_cli → slk}/commands/base.rb +67 -41
- data/lib/slk/commands/cache.rb +141 -0
- data/lib/slk/commands/catchup.rb +411 -0
- data/lib/slk/commands/config.rb +114 -0
- data/lib/slk/commands/dnd.rb +172 -0
- data/lib/slk/commands/emoji.rb +352 -0
- data/lib/slk/commands/help.rb +97 -0
- data/lib/slk/commands/messages.rb +299 -0
- data/lib/slk/commands/presence.rb +109 -0
- data/lib/slk/commands/preset.rb +231 -0
- data/lib/slk/commands/status.rb +223 -0
- data/lib/slk/commands/thread.rb +72 -0
- data/lib/slk/commands/unread.rb +305 -0
- data/lib/slk/commands/workspaces.rb +168 -0
- data/lib/slk/formatters/activity_formatter.rb +148 -0
- data/lib/slk/formatters/attachment_formatter.rb +65 -0
- data/lib/slk/formatters/block_formatter.rb +57 -0
- data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
- data/lib/slk/formatters/emoji_replacer.rb +141 -0
- data/lib/slk/formatters/json_message_formatter.rb +95 -0
- data/lib/slk/formatters/mention_replacer.rb +158 -0
- data/lib/slk/formatters/message_formatter.rb +174 -0
- data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
- data/lib/slk/formatters/reaction_formatter.rb +87 -0
- data/lib/{slack_cli → slk}/models/channel.rb +12 -10
- data/lib/slk/models/duration.rb +94 -0
- data/lib/slk/models/message.rb +242 -0
- data/lib/slk/models/preset.rb +78 -0
- data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
- data/lib/{slack_cli → slk}/models/status.rb +6 -6
- data/lib/slk/models/user.rb +55 -0
- data/lib/slk/models/workspace.rb +54 -0
- data/lib/{slack_cli → slk}/runner.rb +22 -19
- data/lib/slk/services/activity_enricher.rb +124 -0
- data/lib/slk/services/api_client.rb +145 -0
- data/lib/{slack_cli → slk}/services/cache_store.rb +20 -17
- data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
- data/lib/slk/services/emoji_downloader.rb +103 -0
- data/lib/slk/services/emoji_searcher.rb +72 -0
- data/lib/{slack_cli → slk}/services/encryption.rb +11 -14
- data/lib/slk/services/gemoji_sync.rb +97 -0
- data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
- data/lib/slk/services/reaction_enricher.rb +82 -0
- data/lib/slk/services/setup_wizard.rb +131 -0
- data/lib/slk/services/target_resolver.rb +108 -0
- data/lib/{slack_cli → slk}/services/token_store.rb +11 -10
- data/lib/slk/services/unread_marker.rb +101 -0
- data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
- data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
- data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
- data/lib/slk/support/interactive_prompt.rb +29 -0
- data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
- data/lib/slk/support/text_wrapper.rb +57 -0
- data/lib/slk/support/user_resolver.rb +141 -0
- data/lib/{slack_cli → slk}/support/xdg_paths.rb +6 -5
- data/lib/slk/version.rb +5 -0
- data/lib/slk.rb +112 -0
- metadata +80 -59
- data/lib/slack_cli/api/client.rb +0 -49
- data/lib/slack_cli/api/dnd.rb +0 -40
- data/lib/slack_cli/api/users.rb +0 -101
- data/lib/slack_cli/cli.rb +0 -118
- data/lib/slack_cli/commands/activity.rb +0 -292
- data/lib/slack_cli/commands/cache.rb +0 -116
- data/lib/slack_cli/commands/catchup.rb +0 -484
- data/lib/slack_cli/commands/config.rb +0 -159
- data/lib/slack_cli/commands/dnd.rb +0 -143
- data/lib/slack_cli/commands/emoji.rb +0 -412
- data/lib/slack_cli/commands/help.rb +0 -76
- data/lib/slack_cli/commands/messages.rb +0 -317
- data/lib/slack_cli/commands/presence.rb +0 -107
- data/lib/slack_cli/commands/preset.rb +0 -239
- data/lib/slack_cli/commands/status.rb +0 -194
- data/lib/slack_cli/commands/thread.rb +0 -62
- data/lib/slack_cli/commands/unread.rb +0 -312
- data/lib/slack_cli/commands/workspaces.rb +0 -151
- data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
- data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
- data/lib/slack_cli/formatters/message_formatter.rb +0 -429
- data/lib/slack_cli/models/duration.rb +0 -85
- data/lib/slack_cli/models/message.rb +0 -217
- data/lib/slack_cli/models/preset.rb +0 -73
- data/lib/slack_cli/models/user.rb +0 -56
- data/lib/slack_cli/models/workspace.rb +0 -52
- data/lib/slack_cli/services/api_client.rb +0 -149
- data/lib/slack_cli/services/reaction_enricher.rb +0 -87
- data/lib/slack_cli/support/user_resolver.rb +0 -114
- data/lib/slack_cli/version.rb +0 -5
- data/lib/slack_cli.rb +0 -91
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/help_formatter'
|
|
4
|
+
require_relative '../support/inline_images'
|
|
5
|
+
|
|
6
|
+
module Slk
|
|
7
|
+
module Commands
|
|
8
|
+
# Reads messages from channels, DMs, or threads
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class Messages < Base
|
|
11
|
+
include Support::InlineImages
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
result = validate_options
|
|
15
|
+
return result if result
|
|
16
|
+
|
|
17
|
+
target = positional_args.first
|
|
18
|
+
return missing_target_error unless target
|
|
19
|
+
|
|
20
|
+
resolved = target_resolver.resolve(target, default_workspace: target_workspaces.first)
|
|
21
|
+
fetch_and_display_messages(resolved)
|
|
22
|
+
rescue ApiError => e
|
|
23
|
+
error("Failed to fetch messages: #{e.message}")
|
|
24
|
+
1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def missing_target_error
|
|
28
|
+
error('Usage: slk messages <channel|@user|url>')
|
|
29
|
+
1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch_and_display_messages(resolved)
|
|
33
|
+
apply_default_limit(resolved.msg_ts)
|
|
34
|
+
messages = fetch_messages(resolved.workspace, resolved.channel_id, resolved.thread_ts, oldest: resolved.msg_ts)
|
|
35
|
+
messages = enrich_reactions(messages, resolved.workspace, resolved.channel_id) if @options[:reaction_timestamps]
|
|
36
|
+
|
|
37
|
+
output_messages(messages, resolved.workspace, resolved.channel_id)
|
|
38
|
+
0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def output_messages(messages, workspace, channel_id)
|
|
42
|
+
if @options[:json]
|
|
43
|
+
output_json_messages(messages, workspace, channel_id)
|
|
44
|
+
else
|
|
45
|
+
display_messages(messages, workspace, channel_id)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
protected
|
|
50
|
+
|
|
51
|
+
def default_options
|
|
52
|
+
super.merge(
|
|
53
|
+
limit: 500,
|
|
54
|
+
limit_set: false,
|
|
55
|
+
threads: false,
|
|
56
|
+
workspace_emoji: false
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_option(arg, args, remaining)
|
|
61
|
+
case arg
|
|
62
|
+
when '-n', '--limit' then handle_limit_option(args)
|
|
63
|
+
when '--threads' then @options[:threads] = true
|
|
64
|
+
when '--workspace-emoji' then @options[:workspace_emoji] = true
|
|
65
|
+
else super
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def handle_limit_option(args)
|
|
70
|
+
@options[:limit] = args.shift.to_i
|
|
71
|
+
@options[:limit_set] = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def help_text
|
|
75
|
+
help = Support::HelpFormatter.new('slk messages <target> [options]')
|
|
76
|
+
help.description('Read messages from a channel, DM, or thread.')
|
|
77
|
+
add_target_section(help)
|
|
78
|
+
add_options_section(help)
|
|
79
|
+
help.render
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_target_section(help)
|
|
83
|
+
help.section('TARGET') do |s|
|
|
84
|
+
s.item('#channel', 'Channel by name')
|
|
85
|
+
s.item('channel', 'Channel by name (without #)')
|
|
86
|
+
s.item('@user', 'Direct message with user')
|
|
87
|
+
s.item('C123ABC', 'Channel by ID')
|
|
88
|
+
s.item('<slack_url>', 'Slack message URL (returns message + subsequent)')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def add_options_section(help)
|
|
93
|
+
help.section('OPTIONS') do |s|
|
|
94
|
+
add_message_options(s)
|
|
95
|
+
add_formatting_options(s)
|
|
96
|
+
add_common_options(s)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_message_options(section)
|
|
101
|
+
section.option('-n, --limit N', 'Number of messages (default: 500, or 50 for message URLs)')
|
|
102
|
+
section.option('--threads', 'Show thread replies inline')
|
|
103
|
+
section.option('--workspace-emoji', 'Show workspace emoji as inline images (experimental)')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_formatting_options(section)
|
|
107
|
+
section.option('--no-emoji', 'Show :emoji: codes instead of unicode')
|
|
108
|
+
section.option('--no-reactions', 'Hide reactions')
|
|
109
|
+
section.option('--no-names', 'Skip user name lookups (faster)')
|
|
110
|
+
section.option('--reaction-names', 'Show reactions with user names')
|
|
111
|
+
section.option('--reaction-timestamps', 'Show when each person reacted')
|
|
112
|
+
section.option('--width N', 'Wrap text at N columns (default: 72 on TTY, no wrap otherwise)')
|
|
113
|
+
section.option('--no-wrap', 'Disable text wrapping')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def add_common_options(section)
|
|
117
|
+
section.option('--json', 'Output as JSON')
|
|
118
|
+
section.option('-w, --workspace', 'Specify workspace')
|
|
119
|
+
section.option('-v, --verbose', 'Show debug information')
|
|
120
|
+
section.option('-q, --quiet', 'Suppress output')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def target_resolver
|
|
126
|
+
@target_resolver ||= Services::TargetResolver.new(runner: runner, cache_store: cache_store)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def enrich_reactions(messages, workspace, channel_id)
|
|
130
|
+
enricher = Services::ReactionEnricher.new(activity_api: runner.activity_api(workspace.name))
|
|
131
|
+
enricher.enrich_messages(messages, channel_id)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def output_json_messages(messages, workspace, channel_id)
|
|
135
|
+
format_options = {
|
|
136
|
+
no_names: @options[:no_names],
|
|
137
|
+
reaction_timestamps: @options[:reaction_timestamps],
|
|
138
|
+
channel_id: channel_id
|
|
139
|
+
}
|
|
140
|
+
output_json(messages.map do |m|
|
|
141
|
+
runner.message_formatter.format_json(m, workspace: workspace, options: format_options)
|
|
142
|
+
end)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Apply default limit based on target type (50 for message URLs, 500 otherwise)
|
|
146
|
+
def apply_default_limit(msg_ts)
|
|
147
|
+
return if @options[:limit_set]
|
|
148
|
+
|
|
149
|
+
@options[:limit] = msg_ts ? 50 : 500
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def fetch_messages(workspace, channel_id, thread_ts = nil, oldest: nil)
|
|
153
|
+
api = runner.conversations_api(workspace.name)
|
|
154
|
+
raw = if thread_ts
|
|
155
|
+
fetch_thread_messages(api, channel_id, thread_ts)
|
|
156
|
+
else
|
|
157
|
+
fetch_channel_history(api, channel_id, oldest)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
raw.map { |m| Models::Message.from_api(m, channel_id: channel_id) }.reverse
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def fetch_thread_messages(api, channel_id, thread_ts)
|
|
164
|
+
messages = fetch_all_thread_replies(api, channel_id, thread_ts)
|
|
165
|
+
apply_thread_limit(messages)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def apply_thread_limit(messages)
|
|
169
|
+
return messages unless @options[:limit].positive? && messages.length > @options[:limit]
|
|
170
|
+
|
|
171
|
+
[messages.first] + messages.last(@options[:limit] - 1)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def fetch_channel_history(api, channel_id, oldest)
|
|
175
|
+
oldest_adjusted = oldest ? adjust_timestamp(oldest, -0.000001) : nil
|
|
176
|
+
response = api.history(channel: channel_id, limit: @options[:limit], oldest: oldest_adjusted)
|
|
177
|
+
response['messages'] || []
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Adjust a Slack timestamp by a small amount while preserving precision
|
|
181
|
+
def adjust_timestamp(timestamp, delta)
|
|
182
|
+
require 'bigdecimal'
|
|
183
|
+
(BigDecimal(timestamp) + BigDecimal(delta.to_s)).to_s('F')
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def fetch_all_thread_replies(api, channel_id, thread_ts)
|
|
187
|
+
all_messages = []
|
|
188
|
+
cursor = nil
|
|
189
|
+
|
|
190
|
+
loop do
|
|
191
|
+
response, cursor = fetch_thread_page(api, channel_id, thread_ts, cursor)
|
|
192
|
+
all_messages.concat(response)
|
|
193
|
+
break if cursor.nil? || cursor.empty?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
deduplicate_and_sort(all_messages)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def fetch_thread_page(api, channel_id, thread_ts, cursor)
|
|
200
|
+
response = api.replies(channel: channel_id, timestamp: thread_ts, limit: 200, cursor: cursor)
|
|
201
|
+
messages = response['messages'] || []
|
|
202
|
+
debug("Fetched #{messages.length} messages")
|
|
203
|
+
|
|
204
|
+
next_cursor = response['has_more'] ? response.dig('response_metadata', 'next_cursor') : nil
|
|
205
|
+
[messages, next_cursor]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def deduplicate_and_sort(messages)
|
|
209
|
+
messages.uniq { |m| m['ts'] }.sort_by { |m| m['ts'].to_f }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def display_messages(messages, workspace, channel_id)
|
|
213
|
+
formatter = runner.message_formatter
|
|
214
|
+
opts = format_options.merge(channel_id: channel_id)
|
|
215
|
+
|
|
216
|
+
messages.each_with_index do |message, index|
|
|
217
|
+
display_single_message(formatter, message, workspace, opts)
|
|
218
|
+
puts if index < messages.length - 1
|
|
219
|
+
|
|
220
|
+
show_thread_replies(workspace, channel_id, message, opts) if should_show_thread?(message)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def should_show_thread?(message)
|
|
225
|
+
@options[:threads] && message.thread? && !message.reply?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def display_single_message(formatter, message, workspace, opts)
|
|
229
|
+
formatted = formatter.format(message, workspace: workspace, options: opts)
|
|
230
|
+
print_with_workspace_emoji(formatted, workspace)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def show_thread_replies(workspace, channel_id, parent_message, opts)
|
|
234
|
+
api = runner.conversations_api(workspace.name)
|
|
235
|
+
replies = fetch_all_thread_replies(api, channel_id, parent_message.ts)
|
|
236
|
+
|
|
237
|
+
replies[1..].each { |reply_data| display_thread_reply(reply_data, workspace, channel_id, opts) }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def display_thread_reply(reply_data, workspace, channel_id, opts)
|
|
241
|
+
reply = Models::Message.from_api(reply_data, channel_id: channel_id)
|
|
242
|
+
formatted = runner.message_formatter.format(reply, workspace: workspace, options: opts)
|
|
243
|
+
|
|
244
|
+
lines = formatted.lines
|
|
245
|
+
print_with_workspace_emoji(" └ #{lines.first}", workspace)
|
|
246
|
+
lines[1..].each { |line| print_with_workspace_emoji(" #{line}", workspace) }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Print text, replacing workspace emoji codes with inline images when enabled
|
|
250
|
+
def print_with_workspace_emoji(text, workspace)
|
|
251
|
+
if @options[:workspace_emoji] && inline_images_supported?
|
|
252
|
+
print_line_with_emoji_images(text, workspace)
|
|
253
|
+
else
|
|
254
|
+
puts text
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Print a line, inserting inline images for workspace emoji
|
|
259
|
+
def print_line_with_emoji_images(text, workspace)
|
|
260
|
+
emoji_pattern = /:([a-zA-Z0-9_+-]+):/
|
|
261
|
+
parts = text.split(emoji_pattern)
|
|
262
|
+
|
|
263
|
+
parts.each_with_index { |part, i| print_emoji_part(part, i, workspace) }
|
|
264
|
+
puts
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def print_emoji_part(part, index, workspace)
|
|
268
|
+
if index.odd?
|
|
269
|
+
print_emoji_or_code(part, workspace)
|
|
270
|
+
else
|
|
271
|
+
print part
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def print_emoji_or_code(emoji_name, workspace)
|
|
276
|
+
emoji_path = find_workspace_emoji(workspace.name, emoji_name)
|
|
277
|
+
if emoji_path
|
|
278
|
+
print_inline_image(emoji_path, height: 1)
|
|
279
|
+
print ' ' unless in_tmux?
|
|
280
|
+
else
|
|
281
|
+
print ":#{emoji_name}:"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def find_workspace_emoji(workspace_name, emoji_name)
|
|
286
|
+
return nil if emoji_name.empty?
|
|
287
|
+
|
|
288
|
+
paths = Support::XdgPaths.new
|
|
289
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
290
|
+
workspace_dir = File.join(emoji_dir, workspace_name)
|
|
291
|
+
return nil unless Dir.exist?(workspace_dir)
|
|
292
|
+
|
|
293
|
+
# Look for emoji file with any extension
|
|
294
|
+
Dir.glob(File.join(workspace_dir, "#{emoji_name}.*")).first
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
# rubocop:enable Metrics/ClassLength
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/help_formatter'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Commands
|
|
7
|
+
# Gets or sets user presence (away/active)
|
|
8
|
+
class Presence < Base
|
|
9
|
+
def execute
|
|
10
|
+
result = validate_options
|
|
11
|
+
return result if result
|
|
12
|
+
|
|
13
|
+
dispatch_action
|
|
14
|
+
rescue ApiError => e
|
|
15
|
+
error("Failed: #{e.message}")
|
|
16
|
+
1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def dispatch_action
|
|
22
|
+
case positional_args
|
|
23
|
+
in ['away'] then set_presence('away')
|
|
24
|
+
in ['auto' | 'active'] then set_presence('auto')
|
|
25
|
+
in [] then get_presence
|
|
26
|
+
else unknown_presence
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def unknown_presence
|
|
31
|
+
error("Unknown presence: #{positional_args.first}")
|
|
32
|
+
error('Valid options: away, auto, active')
|
|
33
|
+
1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
protected
|
|
37
|
+
|
|
38
|
+
def help_text
|
|
39
|
+
help = Support::HelpFormatter.new('slk presence [away|auto|active]')
|
|
40
|
+
help.description('Get or set your presence status.')
|
|
41
|
+
help.note('GET shows all workspaces by default. SET applies to primary only.')
|
|
42
|
+
add_actions_section(help)
|
|
43
|
+
add_options_section(help)
|
|
44
|
+
help.render
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_actions_section(help)
|
|
48
|
+
help.section('ACTIONS') do |s|
|
|
49
|
+
s.action('(none)', 'Show current presence (all workspaces)')
|
|
50
|
+
s.action('away', 'Set presence to away')
|
|
51
|
+
s.action('auto', 'Set presence to auto (active)')
|
|
52
|
+
s.action('active', 'Alias for auto')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_options_section(help)
|
|
57
|
+
help.section('OPTIONS') do |s|
|
|
58
|
+
s.option('-w, --workspace', 'Limit to specific workspace')
|
|
59
|
+
s.option('--all', 'Set across all workspaces')
|
|
60
|
+
s.option('-q, --quiet', 'Suppress output')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def get_presence # rubocop:disable Naming/AccessorMethodName
|
|
67
|
+
workspaces = @options[:workspace] ? [runner.workspace(@options[:workspace])] : runner.all_workspaces
|
|
68
|
+
workspaces.each { |workspace| display_workspace_presence(workspace, workspaces.size > 1) }
|
|
69
|
+
0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def display_workspace_presence(workspace, show_header)
|
|
73
|
+
data = runner.users_api(workspace.name).get_presence
|
|
74
|
+
puts output.bold(workspace.name) if show_header
|
|
75
|
+
puts " Presence: #{format_presence_status(data[:presence], data[:manual_away])}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def format_presence_status(presence, manual)
|
|
79
|
+
case [presence, manual]
|
|
80
|
+
in ['away', true] then output.yellow('away (manual)')
|
|
81
|
+
in ['away', _] then output.yellow('away')
|
|
82
|
+
in ['active', _] then output.green('active')
|
|
83
|
+
else presence
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def set_presence(presence) # rubocop:disable Naming/AccessorMethodName
|
|
88
|
+
target_workspaces.each do |workspace|
|
|
89
|
+
runner.users_api(workspace.name).set_presence(presence)
|
|
90
|
+
|
|
91
|
+
status_text = presence == 'away' ? output.yellow('away') : output.green('active')
|
|
92
|
+
success("Presence set to #{status_text} on #{workspace.name}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
show_all_workspaces_hint
|
|
96
|
+
|
|
97
|
+
0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def show_all_workspaces_hint
|
|
101
|
+
# Show hint if user has multiple workspaces and didn't use --all or -w
|
|
102
|
+
return if @options[:all] || @options[:workspace]
|
|
103
|
+
return if runner.all_workspaces.size <= 1
|
|
104
|
+
|
|
105
|
+
info('Tip: Use --all to set across all workspaces')
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/inline_images'
|
|
4
|
+
require_relative '../support/help_formatter'
|
|
5
|
+
|
|
6
|
+
module Slk
|
|
7
|
+
module Commands
|
|
8
|
+
# Manages and applies saved status presets
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class Preset < Base
|
|
11
|
+
include Support::InlineImages
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
result = validate_options
|
|
15
|
+
return result if result
|
|
16
|
+
|
|
17
|
+
dispatch_action
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dispatch_action
|
|
21
|
+
case positional_args
|
|
22
|
+
in ['list' | 'ls'] | [] then list_presets
|
|
23
|
+
in ['add'] then add_preset
|
|
24
|
+
in ['edit', name] then edit_preset(name)
|
|
25
|
+
in ['delete' | 'rm', name] then delete_preset(name)
|
|
26
|
+
in [name, *] then apply_preset(name)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def help_text
|
|
33
|
+
help = Support::HelpFormatter.new('slk preset <action|name> [options]')
|
|
34
|
+
help.description('Manage and apply status presets.')
|
|
35
|
+
add_actions_section(help)
|
|
36
|
+
add_examples_section(help)
|
|
37
|
+
add_options_section(help)
|
|
38
|
+
help.render
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add_actions_section(help)
|
|
42
|
+
help.section('ACTIONS') do |s|
|
|
43
|
+
s.action('list', 'List all presets')
|
|
44
|
+
s.action('add', 'Add a new preset (interactive)')
|
|
45
|
+
s.action('edit <name>', 'Edit an existing preset')
|
|
46
|
+
s.action('delete <name>', 'Delete a preset')
|
|
47
|
+
s.action('<name>', 'Apply a preset')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_examples_section(help)
|
|
52
|
+
help.section('EXAMPLES') do |s|
|
|
53
|
+
s.example('slk preset list')
|
|
54
|
+
s.example('slk preset meeting')
|
|
55
|
+
s.example('slk preset add')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_options_section(help)
|
|
60
|
+
help.section('OPTIONS') do |s|
|
|
61
|
+
s.option('-w, --workspace', 'Specify workspace')
|
|
62
|
+
s.option('--all', 'Apply to all workspaces')
|
|
63
|
+
s.option('-q, --quiet', 'Suppress output')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def list_presets
|
|
70
|
+
presets = preset_store.all
|
|
71
|
+
return show_no_presets if presets.empty?
|
|
72
|
+
|
|
73
|
+
puts 'Presets:'
|
|
74
|
+
presets.each { |preset| display_preset(preset) }
|
|
75
|
+
0
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def show_no_presets
|
|
79
|
+
puts 'No presets configured.'
|
|
80
|
+
0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def display_preset(preset)
|
|
84
|
+
puts " #{output.bold(preset.name)}"
|
|
85
|
+
display_preset_status(preset) if preset_has_status?(preset)
|
|
86
|
+
display_preset_options(preset)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def preset_has_status?(preset)
|
|
90
|
+
!preset.text.empty? || !preset.emoji.empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def display_preset_options(preset)
|
|
94
|
+
puts " Duration: #{preset.duration}" unless preset.duration == '0'
|
|
95
|
+
puts " Presence: #{preset.presence}" if preset.sets_presence?
|
|
96
|
+
puts " DND: #{preset.dnd}" if preset.sets_dnd?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def display_preset_status(preset)
|
|
100
|
+
emoji_name = preset.emoji.delete_prefix(':').delete_suffix(':')
|
|
101
|
+
emoji_path = find_workspace_emoji_any(emoji_name)
|
|
102
|
+
|
|
103
|
+
if emoji_path && inline_images_supported?
|
|
104
|
+
text = " #{preset.text}"
|
|
105
|
+
print_inline_image_with_text(emoji_path, text)
|
|
106
|
+
else
|
|
107
|
+
puts " #{preset.emoji} #{preset.text}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def find_workspace_emoji_any(emoji_name)
|
|
112
|
+
return nil if emoji_name.empty?
|
|
113
|
+
|
|
114
|
+
paths = Support::XdgPaths.new
|
|
115
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
116
|
+
|
|
117
|
+
# Search across all workspaces
|
|
118
|
+
runner.all_workspaces.each do |workspace|
|
|
119
|
+
workspace_dir = File.join(emoji_dir, workspace.name)
|
|
120
|
+
next unless Dir.exist?(workspace_dir)
|
|
121
|
+
|
|
122
|
+
path = Dir.glob(File.join(workspace_dir, "#{emoji_name}.*")).first
|
|
123
|
+
return path if path
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def add_preset
|
|
130
|
+
print 'Preset name: '
|
|
131
|
+
name = $stdin.gets&.chomp
|
|
132
|
+
return error('Name is required') if name.nil? || name.empty?
|
|
133
|
+
|
|
134
|
+
preset = prompt_for_preset_fields(name)
|
|
135
|
+
preset_store.add(preset)
|
|
136
|
+
success("Preset '#{name}' created")
|
|
137
|
+
|
|
138
|
+
0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def edit_preset(name)
|
|
142
|
+
existing = preset_store.get(name)
|
|
143
|
+
return error("Preset '#{name}' not found") unless existing
|
|
144
|
+
|
|
145
|
+
puts "Editing preset '#{name}' (press Enter to keep current value)"
|
|
146
|
+
preset = prompt_for_preset_fields(name, defaults: existing)
|
|
147
|
+
preset_store.add(preset)
|
|
148
|
+
success("Preset '#{name}' updated")
|
|
149
|
+
|
|
150
|
+
0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def prompt_for_preset_fields(name, defaults: nil)
|
|
154
|
+
Models::Preset.new(
|
|
155
|
+
name: name,
|
|
156
|
+
text: prompt_field('Status text', defaults&.text),
|
|
157
|
+
emoji: prompt_field('Emoji (e.g., :calendar:)', defaults&.emoji),
|
|
158
|
+
duration: prompt_field('Duration (e.g., 1h, 30m, or 0 for none)', defaults&.duration || '0'),
|
|
159
|
+
presence: prompt_field('Presence (away/auto or blank)', defaults&.presence),
|
|
160
|
+
dnd: prompt_field('DND (e.g., 1h, off, or blank)', defaults&.dnd)
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def prompt_field(label, default = nil)
|
|
165
|
+
if default
|
|
166
|
+
print "#{label} [#{default}]: "
|
|
167
|
+
input = $stdin.gets&.chomp
|
|
168
|
+
input.empty? ? default : input
|
|
169
|
+
else
|
|
170
|
+
print "#{label}: "
|
|
171
|
+
$stdin.gets&.chomp || ''
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def delete_preset(name)
|
|
176
|
+
return error("Preset '#{name}' not found") unless preset_store.exists?(name)
|
|
177
|
+
|
|
178
|
+
preset_store.remove(name)
|
|
179
|
+
success("Preset '#{name}' deleted")
|
|
180
|
+
|
|
181
|
+
0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def apply_preset(name)
|
|
185
|
+
preset = preset_store.get(name)
|
|
186
|
+
return error("Preset '#{name}' not found") unless preset
|
|
187
|
+
|
|
188
|
+
target_workspaces.each { |workspace| apply_preset_to_workspace(workspace, preset, name) }
|
|
189
|
+
0
|
|
190
|
+
rescue ApiError => e
|
|
191
|
+
error("Failed to apply preset: #{e.message}")
|
|
192
|
+
1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def apply_preset_to_workspace(workspace, preset, name)
|
|
196
|
+
apply_status(workspace, preset)
|
|
197
|
+
apply_presence(workspace, preset)
|
|
198
|
+
apply_dnd(workspace, preset)
|
|
199
|
+
success("Applied preset '#{name}' on #{workspace.name}")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def apply_status(workspace, preset)
|
|
203
|
+
users_api = runner.users_api(workspace.name)
|
|
204
|
+
if preset.clears_status?
|
|
205
|
+
users_api.clear_status
|
|
206
|
+
else
|
|
207
|
+
users_api.set_status(text: preset.text, emoji: preset.emoji, duration: preset.duration_value)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def apply_presence(workspace, preset)
|
|
212
|
+
return unless preset.sets_presence?
|
|
213
|
+
|
|
214
|
+
runner.users_api(workspace.name).set_presence(preset.presence)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def apply_dnd(workspace, preset)
|
|
218
|
+
return unless preset.sets_dnd?
|
|
219
|
+
|
|
220
|
+
dnd_api = runner.dnd_api(workspace.name)
|
|
221
|
+
if preset.dnd == 'off'
|
|
222
|
+
dnd_api.end_snooze
|
|
223
|
+
else
|
|
224
|
+
duration = Models::Duration.parse(preset.dnd)
|
|
225
|
+
dnd_api.set_snooze(duration)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
# rubocop:enable Metrics/ClassLength
|
|
230
|
+
end
|
|
231
|
+
end
|