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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/README.md +5 -5
  4. data/bin/slk +3 -3
  5. data/lib/{slack_cli → slk}/api/activity.rb +10 -11
  6. data/lib/{slack_cli → slk}/api/bots.rb +5 -4
  7. data/lib/slk/api/client.rb +51 -0
  8. data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
  9. data/lib/slk/api/dnd.rb +41 -0
  10. data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
  11. data/lib/{slack_cli → slk}/api/threads.rb +13 -12
  12. data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
  13. data/lib/slk/api/users.rb +105 -0
  14. data/lib/slk/cli.rb +157 -0
  15. data/lib/slk/commands/activity.rb +152 -0
  16. data/lib/{slack_cli → slk}/commands/base.rb +67 -41
  17. data/lib/slk/commands/cache.rb +141 -0
  18. data/lib/slk/commands/catchup.rb +411 -0
  19. data/lib/slk/commands/config.rb +114 -0
  20. data/lib/slk/commands/dnd.rb +172 -0
  21. data/lib/slk/commands/emoji.rb +352 -0
  22. data/lib/slk/commands/help.rb +97 -0
  23. data/lib/slk/commands/messages.rb +299 -0
  24. data/lib/slk/commands/presence.rb +109 -0
  25. data/lib/slk/commands/preset.rb +231 -0
  26. data/lib/slk/commands/status.rb +223 -0
  27. data/lib/slk/commands/thread.rb +72 -0
  28. data/lib/slk/commands/unread.rb +305 -0
  29. data/lib/slk/commands/workspaces.rb +168 -0
  30. data/lib/slk/formatters/activity_formatter.rb +148 -0
  31. data/lib/slk/formatters/attachment_formatter.rb +65 -0
  32. data/lib/slk/formatters/block_formatter.rb +57 -0
  33. data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
  34. data/lib/slk/formatters/emoji_replacer.rb +141 -0
  35. data/lib/slk/formatters/json_message_formatter.rb +95 -0
  36. data/lib/slk/formatters/mention_replacer.rb +158 -0
  37. data/lib/slk/formatters/message_formatter.rb +174 -0
  38. data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
  39. data/lib/slk/formatters/reaction_formatter.rb +87 -0
  40. data/lib/{slack_cli → slk}/models/channel.rb +12 -10
  41. data/lib/slk/models/duration.rb +94 -0
  42. data/lib/slk/models/message.rb +242 -0
  43. data/lib/slk/models/preset.rb +78 -0
  44. data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
  45. data/lib/{slack_cli → slk}/models/status.rb +6 -6
  46. data/lib/slk/models/user.rb +55 -0
  47. data/lib/slk/models/workspace.rb +54 -0
  48. data/lib/{slack_cli → slk}/runner.rb +22 -19
  49. data/lib/slk/services/activity_enricher.rb +124 -0
  50. data/lib/slk/services/api_client.rb +145 -0
  51. data/lib/{slack_cli → slk}/services/cache_store.rb +20 -17
  52. data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
  53. data/lib/slk/services/emoji_downloader.rb +103 -0
  54. data/lib/slk/services/emoji_searcher.rb +72 -0
  55. data/lib/{slack_cli → slk}/services/encryption.rb +11 -14
  56. data/lib/slk/services/gemoji_sync.rb +97 -0
  57. data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
  58. data/lib/slk/services/reaction_enricher.rb +82 -0
  59. data/lib/slk/services/setup_wizard.rb +131 -0
  60. data/lib/slk/services/target_resolver.rb +108 -0
  61. data/lib/{slack_cli → slk}/services/token_store.rb +11 -10
  62. data/lib/slk/services/unread_marker.rb +101 -0
  63. data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
  64. data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
  65. data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
  66. data/lib/slk/support/interactive_prompt.rb +29 -0
  67. data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
  68. data/lib/slk/support/text_wrapper.rb +57 -0
  69. data/lib/slk/support/user_resolver.rb +141 -0
  70. data/lib/{slack_cli → slk}/support/xdg_paths.rb +6 -5
  71. data/lib/slk/version.rb +5 -0
  72. data/lib/slk.rb +112 -0
  73. metadata +80 -59
  74. data/lib/slack_cli/api/client.rb +0 -49
  75. data/lib/slack_cli/api/dnd.rb +0 -40
  76. data/lib/slack_cli/api/users.rb +0 -101
  77. data/lib/slack_cli/cli.rb +0 -118
  78. data/lib/slack_cli/commands/activity.rb +0 -292
  79. data/lib/slack_cli/commands/cache.rb +0 -116
  80. data/lib/slack_cli/commands/catchup.rb +0 -484
  81. data/lib/slack_cli/commands/config.rb +0 -159
  82. data/lib/slack_cli/commands/dnd.rb +0 -143
  83. data/lib/slack_cli/commands/emoji.rb +0 -412
  84. data/lib/slack_cli/commands/help.rb +0 -76
  85. data/lib/slack_cli/commands/messages.rb +0 -317
  86. data/lib/slack_cli/commands/presence.rb +0 -107
  87. data/lib/slack_cli/commands/preset.rb +0 -239
  88. data/lib/slack_cli/commands/status.rb +0 -194
  89. data/lib/slack_cli/commands/thread.rb +0 -62
  90. data/lib/slack_cli/commands/unread.rb +0 -312
  91. data/lib/slack_cli/commands/workspaces.rb +0 -151
  92. data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
  93. data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
  94. data/lib/slack_cli/formatters/message_formatter.rb +0 -429
  95. data/lib/slack_cli/models/duration.rb +0 -85
  96. data/lib/slack_cli/models/message.rb +0 -217
  97. data/lib/slack_cli/models/preset.rb +0 -73
  98. data/lib/slack_cli/models/user.rb +0 -56
  99. data/lib/slack_cli/models/workspace.rb +0 -52
  100. data/lib/slack_cli/services/api_client.rb +0 -149
  101. data/lib/slack_cli/services/reaction_enricher.rb +0 -87
  102. data/lib/slack_cli/support/user_resolver.rb +0 -114
  103. data/lib/slack_cli/version.rb +0 -5
  104. data/lib/slack_cli.rb +0 -91
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Support
5
5
  # Formats help text with auto-aligned columns
6
6
  #
@@ -47,32 +47,27 @@ module SlackCli
47
47
  end
48
48
 
49
49
  def render
50
- lines = []
51
- lines << "USAGE: #{@usage}"
52
- lines << ""
53
-
54
- if @description
55
- lines << @description
56
- end
57
-
58
- @notes.each do |note|
59
- lines << note
60
- end
61
-
62
- lines << "" if @description || @notes.any?
50
+ lines = build_header
51
+ lines.concat(build_sections)
52
+ lines.pop if lines.last == ''
53
+ lines.join("\n")
54
+ end
63
55
 
64
- @sections.each do |section|
65
- lines << "#{section.title}:"
66
- lines.concat(section.render)
67
- lines << ""
68
- end
56
+ private
69
57
 
70
- # Remove trailing blank line
71
- lines.pop if lines.last == ""
58
+ def build_header
59
+ lines = ["USAGE: #{@usage}", '']
60
+ lines << @description if @description
61
+ @notes.each { |note| lines << note }
62
+ lines << '' if @description || @notes.any?
63
+ lines
64
+ end
72
65
 
73
- lines.join("\n")
66
+ def build_sections
67
+ @sections.flat_map { |section| ["#{section.title}:", *section.render, ''] }
74
68
  end
75
69
 
70
+ # Represents a section within help output (OPTIONS, EXAMPLES, etc.)
76
71
  class Section
77
72
  attr_reader :title
78
73
 
@@ -109,30 +104,27 @@ module SlackCli
109
104
  def render
110
105
  return [] if @items.empty?
111
106
 
112
- # Calculate max width of left column
113
- max_left = @items
114
- .reject { |type, _, _| type == :text }
115
- .map { |_, left, _| left.length }
116
- .max || 0
117
-
118
- # Add padding (2 spaces between columns)
119
- padding = 2
120
-
121
- @items.map do |type, left, right|
122
- case type
123
- when :text
124
- " #{left}"
125
- when :example
126
- if right
127
- " #{left.ljust(max_left + padding)}#{right}"
128
- else
129
- " #{left}"
130
- end
131
- else
132
- " #{left.ljust(max_left + padding)}#{right}"
133
- end
107
+ max_left = calculate_max_left_width
108
+ @items.map { |type, left, right| format_item(type, left, right, max_left) }
109
+ end
110
+
111
+ private
112
+
113
+ def calculate_max_left_width
114
+ @items.reject { |type, _, _| type == :text }.map { |_, left, _| left.length }.max || 0
115
+ end
116
+
117
+ def format_item(type, left, right, max_left)
118
+ case type
119
+ when :text then " #{left}"
120
+ when :example then format_example(left, right, max_left)
121
+ else " #{left.ljust(max_left + 2)}#{right}"
134
122
  end
135
123
  end
124
+
125
+ def format_example(left, right, max_left)
126
+ right ? " #{left.ljust(max_left + 2)}#{right}" : " #{left}"
127
+ end
136
128
  end
137
129
  end
138
130
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Support
5
5
  # Shared module for inline image display in iTerm2/WezTerm/Mintty terminals
6
6
  # Includes special handling for tmux passthrough sequences
@@ -9,40 +9,49 @@ module SlackCli
9
9
  def inline_images_supported?
10
10
  # iTerm2, WezTerm, Mintty support inline images
11
11
  # LC_TERMINAL persists through tmux/ssh
12
- ENV["TERM_PROGRAM"] == "iTerm.app" ||
13
- ENV["TERM_PROGRAM"] == "WezTerm" ||
14
- ENV["LC_TERMINAL"] == "iTerm2" ||
15
- ENV["LC_TERMINAL"] == "WezTerm" ||
16
- ENV["TERM"] == "mintty"
12
+ ['iTerm.app', 'WezTerm'].include?(ENV.fetch('TERM_PROGRAM', nil)) ||
13
+ ENV['LC_TERMINAL'] == 'iTerm2' ||
14
+ ENV['LC_TERMINAL'] == 'WezTerm' ||
15
+ ENV['TERM'] == 'mintty'
17
16
  end
18
17
 
19
18
  # Check if running inside tmux
20
19
  def in_tmux?
21
20
  # tmux sets TERM to screen-* or tmux-*
22
- ENV["TERM"]&.include?("screen") || ENV["TERM"]&.start_with?("tmux")
21
+ ENV['TERM']&.include?('screen') || ENV['TERM']&.start_with?('tmux')
23
22
  end
24
23
 
25
24
  # Print an inline image using iTerm2 protocol
26
25
  # In tmux, uses passthrough sequence and cursor positioning
27
26
  def print_inline_image(path, height: 1)
28
- return unless File.exist?(path)
27
+ data = read_image_data(path)
28
+ return unless data
29
29
 
30
- data = File.binread(path)
31
- encoded = [data].pack("m0") # Base64 encode
30
+ encoded = [data].pack('m0')
31
+ in_tmux? ? print_tmux_image(encoded, height) : print_iterm_image(encoded, height)
32
+ end
32
33
 
33
- if in_tmux?
34
- # tmux passthrough: \n + space required for image to render
35
- printf "\ePtmux;\e\e]1337;File=inline=1;preserveAspectRatio=0;size=%d;height=%d:%s\a\e\\\n ",
36
- encoded.length, height, encoded
37
- else
38
- # Standard iTerm2 format
39
- printf "\e]1337;File=inline=1;height=%d:%s\a", height, encoded
40
- end
34
+ def read_image_data(path)
35
+ return nil unless File.exist?(path)
36
+
37
+ File.binread(path)
38
+ rescue IOError, SystemCallError
39
+ nil
40
+ end
41
+
42
+ def print_tmux_image(encoded, height)
43
+ fmt = "\ePtmux;\e\e]1337;File=inline=1;preserveAspectRatio=0;" \
44
+ "size=%<size>d;height=%<height>d:%<data>s\a\e\\\n "
45
+ printf fmt, size: encoded.length, height: height, data: encoded
46
+ end
47
+
48
+ def print_iterm_image(encoded, height)
49
+ printf "\e]1337;File=inline=1;height=%<height>d:%<data>s\a", height: height, data: encoded
41
50
  end
42
51
 
43
52
  # Print inline image with name on same line
44
53
  # Handles tmux cursor positioning to keep image and text on same line
45
- def print_inline_image_with_text(path, text, height: 1)
54
+ def print_inline_image_with_text(path, text, height: 1) # rubocop:disable Naming/PredicateMethod
46
55
  return false unless inline_images_supported? && File.exist?(path)
47
56
 
48
57
  print_inline_image(path, height: height)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Support
5
+ # Interactive terminal prompt utilities
6
+ module InteractivePrompt
7
+ module_function
8
+
9
+ # Read a single character from the terminal
10
+ def read_single_char
11
+ if $stdin.tty?
12
+ $stdin.raw(&:readchar)
13
+ else
14
+ $stdin.gets&.chomp
15
+ end
16
+ rescue Interrupt
17
+ 'q'
18
+ end
19
+
20
+ # Display a prompt and read a single character
21
+ def prompt_for_action(prompt)
22
+ print "\n#{prompt} > "
23
+ input = read_single_char
24
+ puts
25
+ input
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Support
5
+ # Parses Slack message URLs into workspace, channel, and timestamp
5
6
  class SlackUrlParser
6
7
  # Patterns for Slack URLs
7
8
  # Channel IDs: C=channel, G=group DM, D=direct message
@@ -30,41 +31,38 @@ module SlackCli
30
31
  end
31
32
 
32
33
  def parse(input)
33
- return nil unless input.to_s.include?("slack.com")
34
+ return nil unless input.to_s.include?('slack.com')
34
35
 
35
36
  URL_PATTERNS.each do |pattern|
36
37
  match = input.match(pattern)
37
- next unless match
38
-
39
- workspace = match[1]
40
- channel_id = match[2]
41
- msg_ts = match[3] ? format_ts(match[3]) : nil
42
- thread_ts = match[4]
43
-
44
- return Result.new(
45
- workspace: workspace,
46
- channel_id: channel_id,
47
- msg_ts: msg_ts,
48
- thread_ts: thread_ts
49
- )
38
+ return build_result(match) if match
50
39
  end
51
40
 
52
41
  nil
53
42
  end
54
43
 
55
44
  def slack_url?(input)
56
- input.to_s.include?("slack.com/archives")
45
+ input.to_s.include?('slack.com/archives')
57
46
  end
58
47
 
59
48
  private
60
49
 
50
+ def build_result(match)
51
+ Result.new(
52
+ workspace: match[1],
53
+ channel_id: match[2],
54
+ msg_ts: match[3] ? format_ts(match[3]) : nil,
55
+ thread_ts: match[4]
56
+ )
57
+ end
58
+
61
59
  # Convert Slack URL timestamp format to API format
62
60
  # URL: p1234567890123456 -> API: 1234567890.123456
63
61
  def format_ts(url_ts)
64
62
  return nil unless url_ts
65
63
 
66
64
  # Remove 'p' prefix if present
67
- ts = url_ts.sub(/^p/, "")
65
+ ts = url_ts.sub(/^p/, '')
68
66
 
69
67
  # Insert decimal point
70
68
  if ts.length > 6
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Support
5
+ # Text wrapping utilities with ANSI escape code awareness
6
+ module TextWrapper
7
+ module_function
8
+
9
+ # Calculate visible length of text (excluding ANSI escape codes)
10
+ def visible_length(text)
11
+ text.gsub(/\e\[[0-9;]*m/, '').length
12
+ end
13
+
14
+ # Wrap text to width, handling first line differently and preserving existing newlines
15
+ def wrap(text, first_line_width, continuation_width)
16
+ result = []
17
+ text.each_line do |paragraph|
18
+ process_paragraph(paragraph.chomp, result, first_line_width, continuation_width)
19
+ end
20
+ result.join("\n")
21
+ end
22
+
23
+ def process_paragraph(paragraph, result, first_line_width, continuation_width)
24
+ if paragraph.empty?
25
+ result << ''
26
+ else
27
+ current_first_width = result.empty? ? first_line_width : continuation_width
28
+ result << wrap_paragraph(paragraph, current_first_width, continuation_width)
29
+ end
30
+ end
31
+
32
+ # Wrap a single paragraph (no internal newlines)
33
+ def wrap_paragraph(text, first_width, rest_width)
34
+ state = { lines: [], current_line: '', current_width: first_width, rest_width: rest_width }
35
+ text.split(/(\s+)/).each { |word| process_word(word, state) }
36
+ state[:lines] << state[:current_line] unless state[:current_line].empty?
37
+ state[:lines].join("\n")
38
+ end
39
+
40
+ def process_word(word, state)
41
+ if state[:current_line].empty?
42
+ state[:current_line] = word
43
+ elsif visible_length(state[:current_line]) + visible_length(word) <= state[:current_width]
44
+ state[:current_line] += word
45
+ else
46
+ start_new_line(word, state)
47
+ end
48
+ end
49
+
50
+ def start_new_line(word, state)
51
+ state[:lines] << state[:current_line]
52
+ state[:current_line] = word.lstrip
53
+ state[:current_width] = state[:rest_width]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Support
5
+ # Shared logic for resolving user and channel names from Slack data.
6
+ # Include this module in commands that need to look up user/channel names.
7
+ #
8
+ # == Required Interface
9
+ # Including classes must provide these methods:
10
+ # - runner: Returns the Runner instance for API access
11
+ # - cache_store: Returns the CacheStore for name lookups
12
+ # - debug(message): Logs debug messages (can be a no-op)
13
+ module UserResolver
14
+ # Resolves a DM channel ID to the user's display name
15
+ # @param workspace [Models::Workspace] The workspace to look up in
16
+ # @param channel_id [String] The DM channel ID (starts with D)
17
+ # @param conversations [Api::Conversations] API client for conversation info
18
+ # @return [String] User name or channel ID if not found
19
+ def resolve_dm_user_name(workspace, channel_id, conversations)
20
+ user_id = get_dm_user_id(channel_id, conversations)
21
+ return channel_id unless user_id
22
+
23
+ lookup_user_name(workspace, user_id) || user_id
24
+ rescue ApiError => e
25
+ debug("DM info lookup failed for #{channel_id}: #{e.message}")
26
+ channel_id
27
+ end
28
+
29
+ private
30
+
31
+ def get_dm_user_id(channel_id, conversations)
32
+ info = conversations.info(channel: channel_id)
33
+ return nil unless info['ok'] && info['channel']
34
+
35
+ info['channel']['user']
36
+ end
37
+
38
+ def lookup_user_name(workspace, user_id)
39
+ cached = cache_store.get_user(workspace.name, user_id)
40
+ return cached if cached
41
+
42
+ fetch_and_cache_user_name(workspace, user_id)
43
+ end
44
+
45
+ def fetch_and_cache_user_name(workspace, user_id)
46
+ users_api = runner.users_api(workspace.name)
47
+ user_info = users_api.info(user_id)
48
+ return nil unless user_info['ok'] && user_info['user']
49
+
50
+ name = extract_name_from_user_info(user_info['user'])
51
+ return nil unless name
52
+
53
+ cache_store.set_user(workspace.name, user_id, name, persist: true)
54
+ name
55
+ rescue ApiError => e
56
+ debug("User lookup failed for #{user_id}: #{e.message}")
57
+ nil
58
+ end
59
+
60
+ def extract_name_from_user_info(user)
61
+ profile = user['profile'] || {}
62
+ name = profile['display_name']
63
+ name = profile['real_name'] if name.to_s.empty?
64
+ name = user['name'] if name.to_s.empty?
65
+ name unless name.to_s.empty?
66
+ end
67
+
68
+ public
69
+
70
+ # Resolves a channel ID to a formatted label (@username or #channel)
71
+ # @param workspace [Models::Workspace] The workspace to look up in
72
+ # @param channel_id [String] The channel ID
73
+ # @return [String] Formatted label like "@username" or "#channel"
74
+ def resolve_conversation_label(workspace, channel_id)
75
+ return resolve_dm_label(workspace, channel_id) if channel_id.start_with?('D')
76
+
77
+ resolve_channel_label(workspace, channel_id)
78
+ end
79
+
80
+ # Extracts the user name from a message hash
81
+ # @param msg [Hash] The message data from API
82
+ # @param workspace [Models::Workspace] The workspace context
83
+ # @return [String] User name, user_id as fallback, or "unknown" if neither found
84
+ def extract_user_from_message(msg, workspace)
85
+ name_from_user_profile(msg) ||
86
+ name_from_username(msg) ||
87
+ name_from_cache(msg, workspace) ||
88
+ msg['user'] || msg['bot_id'] || 'unknown'
89
+ end
90
+
91
+ private
92
+
93
+ def resolve_dm_label(workspace, channel_id)
94
+ conversations = runner.conversations_api(workspace.name)
95
+ user_name = resolve_dm_user_name(workspace, channel_id, conversations)
96
+ "@#{user_name}"
97
+ end
98
+
99
+ def resolve_channel_label(workspace, channel_id)
100
+ cached_name = cache_store.get_channel_name(workspace.name, channel_id)
101
+ return "##{cached_name}" if cached_name
102
+
103
+ fetch_channel_label(workspace, channel_id)
104
+ end
105
+
106
+ def fetch_channel_label(workspace, channel_id)
107
+ conversations = runner.conversations_api(workspace.name)
108
+ response = conversations.info(channel: channel_id)
109
+ return "##{channel_id}" unless response['ok'] && response['channel']
110
+
111
+ name = response['channel']['name']
112
+ return "##{channel_id}" unless name
113
+
114
+ cache_store.set_channel(workspace.name, name, channel_id)
115
+ "##{name}"
116
+ rescue ApiError => e
117
+ debug("Channel info lookup failed for #{channel_id}: #{e.message}")
118
+ "##{channel_id}"
119
+ end
120
+
121
+ def name_from_user_profile(msg)
122
+ return nil unless msg['user_profile']
123
+
124
+ name = msg['user_profile']['display_name']
125
+ name = msg['user_profile']['real_name'] if name.to_s.empty?
126
+ name unless name.to_s.empty?
127
+ end
128
+
129
+ def name_from_username(msg)
130
+ msg['username'] unless msg['username'].to_s.empty?
131
+ end
132
+
133
+ def name_from_cache(msg, workspace)
134
+ user_id = msg['user'] || msg['bot_id']
135
+ return nil unless user_id
136
+
137
+ cache_store.get_user(workspace.name, user_id)
138
+ end
139
+ end
140
+ end
141
+ end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Support
5
+ # XDG-compliant paths for config and cache directories
5
6
  class XdgPaths
6
7
  def config_dir
7
8
  @config_dir ||= File.join(
8
- ENV.fetch("XDG_CONFIG_HOME", File.join(Dir.home, ".config")),
9
- "slack-cli"
9
+ ENV.fetch('XDG_CONFIG_HOME', File.join(Dir.home, '.config')),
10
+ 'slk'
10
11
  )
11
12
  end
12
13
 
13
14
  def cache_dir
14
15
  @cache_dir ||= File.join(
15
- ENV.fetch("XDG_CACHE_HOME", File.join(Dir.home, ".cache")),
16
- "slack-cli"
16
+ ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache')),
17
+ 'slk'
17
18
  )
18
19
  end
19
20
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ VERSION = '0.2.0'
5
+ end
data/lib/slk.rb ADDED
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'fileutils'
7
+ require 'optparse'
8
+ require 'time'
9
+ require 'io/console'
10
+
11
+ # Slack CLI - A command-line interface for Slack
12
+ module Slk
13
+ class Error < StandardError; end
14
+ class ApiError < Error; end
15
+ class ConfigError < Error; end
16
+ class EncryptionError < Error; end
17
+ class TokenStoreError < Error; end
18
+ class WorkspaceNotFoundError < ConfigError; end
19
+ class PresetNotFoundError < ConfigError; end
20
+
21
+ autoload :VERSION, 'slk/version'
22
+ autoload :CLI, 'slk/cli'
23
+ autoload :Runner, 'slk/runner'
24
+
25
+ # Data models for Slack entities
26
+ module Models
27
+ autoload :Duration, 'slk/models/duration'
28
+ autoload :Workspace, 'slk/models/workspace'
29
+ autoload :Status, 'slk/models/status'
30
+ autoload :Message, 'slk/models/message'
31
+ autoload :Reaction, 'slk/models/reaction'
32
+ autoload :User, 'slk/models/user'
33
+ autoload :Channel, 'slk/models/channel'
34
+ autoload :Preset, 'slk/models/preset'
35
+ end
36
+
37
+ # Application services for configuration, caching, and API communication
38
+ module Services
39
+ autoload :ApiClient, 'slk/services/api_client'
40
+ autoload :Configuration, 'slk/services/configuration'
41
+ autoload :TokenStore, 'slk/services/token_store'
42
+ autoload :CacheStore, 'slk/services/cache_store'
43
+ autoload :PresetStore, 'slk/services/preset_store'
44
+ autoload :Encryption, 'slk/services/encryption'
45
+ autoload :ReactionEnricher, 'slk/services/reaction_enricher'
46
+ autoload :GemojiSync, 'slk/services/gemoji_sync'
47
+ autoload :EmojiDownloader, 'slk/services/emoji_downloader'
48
+ autoload :EmojiSearcher, 'slk/services/emoji_searcher'
49
+ autoload :ActivityEnricher, 'slk/services/activity_enricher'
50
+ autoload :UnreadMarker, 'slk/services/unread_marker'
51
+ autoload :TargetResolver, 'slk/services/target_resolver'
52
+ autoload :SetupWizard, 'slk/services/setup_wizard'
53
+ end
54
+
55
+ # Output formatters for messages, durations, and emoji
56
+ module Formatters
57
+ autoload :Output, 'slk/formatters/output'
58
+ autoload :DurationFormatter, 'slk/formatters/duration_formatter'
59
+ autoload :MentionReplacer, 'slk/formatters/mention_replacer'
60
+ autoload :EmojiReplacer, 'slk/formatters/emoji_replacer'
61
+ autoload :MessageFormatter, 'slk/formatters/message_formatter'
62
+ autoload :ReactionFormatter, 'slk/formatters/reaction_formatter'
63
+ autoload :JsonMessageFormatter, 'slk/formatters/json_message_formatter'
64
+ autoload :ActivityFormatter, 'slk/formatters/activity_formatter'
65
+ autoload :AttachmentFormatter, 'slk/formatters/attachment_formatter'
66
+ autoload :BlockFormatter, 'slk/formatters/block_formatter'
67
+ end
68
+
69
+ # CLI commands implementing user-facing functionality
70
+ module Commands
71
+ autoload :Base, 'slk/commands/base'
72
+ autoload :Status, 'slk/commands/status'
73
+ autoload :Presence, 'slk/commands/presence'
74
+ autoload :Dnd, 'slk/commands/dnd'
75
+ autoload :Messages, 'slk/commands/messages'
76
+ autoload :Thread, 'slk/commands/thread'
77
+ autoload :Unread, 'slk/commands/unread'
78
+ autoload :Catchup, 'slk/commands/catchup'
79
+ autoload :Activity, 'slk/commands/activity'
80
+ autoload :Preset, 'slk/commands/preset'
81
+ autoload :Workspaces, 'slk/commands/workspaces'
82
+ autoload :Cache, 'slk/commands/cache'
83
+ autoload :Emoji, 'slk/commands/emoji'
84
+ autoload :Config, 'slk/commands/config'
85
+ autoload :Help, 'slk/commands/help'
86
+ end
87
+
88
+ # Thin wrappers around Slack API endpoints
89
+ module Api
90
+ autoload :Users, 'slk/api/users'
91
+ autoload :Conversations, 'slk/api/conversations'
92
+ autoload :Dnd, 'slk/api/dnd'
93
+ autoload :Emoji, 'slk/api/emoji'
94
+ autoload :Client, 'slk/api/client'
95
+ autoload :Bots, 'slk/api/bots'
96
+ autoload :Threads, 'slk/api/threads'
97
+ autoload :Usergroups, 'slk/api/usergroups'
98
+ autoload :Activity, 'slk/api/activity'
99
+ end
100
+
101
+ # Utility classes for paths, parsing, and helpers
102
+ module Support
103
+ autoload :XdgPaths, 'slk/support/xdg_paths'
104
+ autoload :SlackUrlParser, 'slk/support/slack_url_parser'
105
+ autoload :InlineImages, 'slk/support/inline_images'
106
+ autoload :HelpFormatter, 'slk/support/help_formatter'
107
+ autoload :ErrorLogger, 'slk/support/error_logger'
108
+ autoload :UserResolver, 'slk/support/user_resolver'
109
+ autoload :TextWrapper, 'slk/support/text_wrapper'
110
+ autoload :InteractivePrompt, 'slk/support/interactive_prompt'
111
+ end
112
+ end