cafeznik 0.5.61 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3819fe5b2f55ad94ab0d199ba936b872b5ac0da19b300543b1ddc5bfc2763b02
4
- data.tar.gz: 1ea542956197037f368c1c9c2a3d0d867a96f37f6b3af88a4596e0e977e6a9c5
3
+ metadata.gz: b6a5befc473305cc3827c00be0f4b2eb1541dcce37b01f599e2c917cc9fb8716
4
+ data.tar.gz: 8cee6c1e78bee69d193ceeb9d1d1b928bc3e8d0e2a724a295b4e64aae8b1e0a6
5
5
  SHA512:
6
- metadata.gz: 0a39fbc59dc0d234b471eb74ff2bb5d9a2947c54b4af1b5710b3a8784f49841f90522601437a948d5f0527e952b51d80376adcd200557e080760177c8d842888
7
- data.tar.gz: e4e01916cded3f43808d81c2421adcd3cde13b807a7b4b9316d4780637f613b2bde48989a16c23056af173eb1e28a3d9a13047204671b9cd6f6f7de34afb06bb
6
+ metadata.gz: befa9841636ca27d1d05b822ca5feb19121c620195430a79cfb0b90b2a49782f940e5419291c6875da409b9e7376d778a1ed485d4f5f12135009a7e5d309c35d
7
+ data.tar.gz: 553dd4f54051c5c927f8e4b09a642fef516516ead8e275b555222ff5ea23acf3061c705418aa28b003bc9c6d1032b8e25d65934951fbee63430ca3b6fd9af266
data/README.md CHANGED
@@ -1,6 +1,129 @@
1
1
  # Cafeznik
2
2
 
3
- Not a fez wearing cat in a track suit
3
+ _Not a fez-wearing cat in a track suit._
4
4
 
5
+ There are [many](https://github.com/Dicklesworthstone/your-source-to-prompt.html?tab=readme-ov-file) code2prompt tools around, but this one is mine 🪖. It’s built with a sprinkle of practicality, a dash of irreverence, and plenty of laziness—exactly how a good developer tool should be.
6
+
7
+ ## What is this?
8
+
9
+ Cafeznik is an interactive CLI (levereging the beautiful [fzf](https://github.com/junegunn/fzf)) to ease the selection and copying of code files - local or remote (GitHub) - to your clipboard.
10
+
11
+ Why? You know why - so I can feed it into LLMs like the lazy, lazy ~~script kiddie~~ *vibe programmer* I am. It’s streamlined, efficient, and dangerously habit-forming.
12
+
13
+
14
+ ## Installation
15
+
16
+ Install it directly via RubyGems (requires Ruby 3.3 and the other dependencies [listed below](#dependencies)):
17
+
18
+ ```bash
19
+ gem install cafeznik
20
+ ```
21
+
22
+ ## Then what?
23
+
24
+ ```bash
25
+ cafeznik # or cafeznik --repo owner/repo
26
+ ```
27
+ use `tab` to select multiple files, `enter` to copy them to your clipboard, and `ctrl-c` to exit.
28
+
29
+ ### looksee
30
+ Local mode:
31
+ [![asciicast](reference/local-asciinema.gif)](https://asciinema.org/a/YWcuK13nRybD234R5nkW8J7Hh)
32
+ Or remote with grep and exclude:
33
+
34
+ ## Dependencies
35
+
36
+ Cafeznik relies on a few external tools to work its magic:
37
+
38
+ - [`fzf`](https://github.com/junegunn/fzf) – Essential for interactive file selection (absolutely required)
39
+ - [`fd`](https://github.com/sharkdp/fd) – Powers local file discovery (required for local mode)
40
+ - [`ripgrep`](https://github.com/BurntSushi/ripgrep) – Enables efficient grep functionality (required when using `--grep`)
41
+ - [`bat`](https://github.com/sharkdp/bat) (& `tree`) – Provide pretty previews (optional but highly recommended)
42
+ - [`gh`](https://cli.github.com/) – Simplifies GitHub authentication (optional; you can alternatively set the GITHUB_TOKEN environment variable)
43
+
44
+ A homebrew line to install all the dependencies on macOS:
45
+
46
+ ```bash
47
+ brew install fzf fd ripgrep bat tree gh
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ### Local mode
53
+
54
+ Quickly select and copy files from your current directory:
55
+
56
+ ```bash
57
+ cafeznik
58
+ ```
59
+
60
+ Filter your selection to include only files that contain specific text:
61
+
62
+ ```bash
63
+ cafeznik --grep "def initialize"
64
+ ```
65
+
66
+ Easily exclude unwanted files or directories:
67
+
68
+ ```bash
69
+ cafeznik --exclude "*.log" --exclude "tmp/"
70
+ ```
71
+
72
+ ### GitHub mode
73
+
74
+ Fetch and copy code directly from any GitHub repository:
75
+
76
+ ```bash
77
+ cafeznik --repo owner/repo
78
+ ```
79
+
80
+ It also supports full URLs:
81
+
82
+ ```bash
83
+ cafeznik --repo https://github.com/owner/repo
84
+ ```
85
+
86
+ ## Noteworthy Flags
87
+
88
+ ```
89
+ --repo, -r Specify a GitHub repository to fetch files from
90
+ --grep, -g Only select files containing specific text patterns (works locally and remotely)
91
+ --exclude, -e Exclude files or directories matching provided patterns (also works locally and remotely)
92
+ --with-tree, -t Include a detailed file tree structure in your output (Guess what? Works locally and remotely)
93
+ ```
94
+
95
+ ## Less important flags
96
+ ```
97
+ --no-header Omit file headers from the copied content for a cleaner paste
98
+ --verbose Activate detailed logging output for debugging and transparency
99
+ ```
100
+
101
+ Or, you know:
102
+
103
+ ```bash
104
+ cafeznik --help
105
+ ```
106
+
107
+ ## What's a-comin
108
+
109
+ - History of copied files - so you can easily re-copy them. Rinse, repeat.
110
+ - Optional minification of copied files
111
+ - Binary files support for multi-modal models? Might be a stretch
112
+ - Token counting. Everyone loves token counting.
113
+
114
+ ## Noteworthy Competitors I did not take inspiration from
115
+
116
+ - [gitingest](https://github.com/davidesantangelo/gitingest) - Fellow Ruby that works much better on bigger repos, and packs it all nicely in a prompt file
117
+ - [onefilellm](https://github.com/jimmc414/onefilellm) - Does so much more, expect it's a completely different thing
118
+ - [your-source-to-prompt.html](https://github.com/Dicklesworthstone/your-source-to-prompt.html) - If you wanna leave your console for a browser, you'll get plenty of nice features for your code2prompt needs
119
+
120
+ ## License
121
+
122
+ Cafeznik is open-source software, licensed under the MIT License.
123
+
124
+ ## Contributing
125
+
126
+ Contributions of all kinds are warmly welcomed.
127
+
128
+ Enjoy your freshly copied code snacks! 🍪
5
129
 
6
- There are [many](https://github.com/Dicklesworthstone/your-source-to-prompt.html?tab=readme-ov-file) a code2prompt tools about, but this one is mine 🪖
data/lib/cafeznik/cli.rb CHANGED
@@ -7,8 +7,8 @@ module Cafeznik
7
7
  class_option :verbose, type: :boolean, aliases: "--debug", default: false, desc: "Run in verbose mode"
8
8
  class_option :no_header, type: :boolean, default: false, desc: "Exclude headers"
9
9
  class_option :with_tree, type: :boolean, aliases: "-t", default: false, desc: "Include file tree"
10
- class_option :grep, type: :string, aliases: "-g", desc: "Filter files containing the specified content"
11
- class_option :exclude, type: :array, aliases: "-e", desc: "Exclude files/folders matching patterns"
10
+ class_option :grep, type: :string, aliases: "-g", desc: "Filter files containing the specified content", banner: "Patterny Pattern"
11
+ class_option :exclude, type: :array, aliases: "-e", desc: "Exclude files/folders matching patterns", banner: "glob *.ext **/dir"
12
12
 
13
13
  map %w[-v --version] => :version
14
14
 
@@ -37,6 +37,9 @@ module Cafeznik
37
37
  ).copy_to_clipboard
38
38
  end
39
39
 
40
+ desc "help [COMMAND]", "Display detailed help information"
41
+ def help = Help.display(self)
42
+
40
43
  private
41
44
 
42
45
  def determine_source
@@ -47,8 +50,8 @@ module Cafeznik
47
50
  end
48
51
  end
49
52
 
50
- def repo = options[:repo]
51
- def grep = options[:grep]
52
53
  def exclude = options[:exclude] || []
54
+ def repo = options[:repo].tap { |r| Log.fatal("We can't do much without a repo when using -r/--repo") if r == "repo" }
55
+ def grep = options[:grep].tap { |g| Log.fatal("You gotta provide a search pattern when using -g/--grep") if g == "grep" }
53
56
  end
54
57
  end
@@ -1,22 +1,21 @@
1
1
  require "clipboard"
2
+ require "concurrent"
2
3
  require "memery"
4
+ require "tty-progressbar"
3
5
 
4
6
  module Cafeznik
5
7
  class Content
6
8
  include Memery
7
9
  MAX_LINES = 10_000
10
+ THREAD_COUNT = [Concurrent.processor_count, 8].min
11
+ THREAD_TIMEOUT = 20 # seconds
8
12
 
9
13
  def initialize(source:, file_paths:, include_headers:, include_tree:)
10
- Log.debug "Initializing Content" do
11
- <<~LOG
12
- Source: #{source.class} file_paths: #{file_paths.size}
13
- include_headers: #{include_headers} include_tree: #{include_tree}
14
- LOG
15
- end
16
14
  @source = source
17
15
  @file_paths = file_paths
18
16
  @include_headers = include_headers
19
17
  @include_tree = include_tree
18
+ log_init
20
19
  end
21
20
 
22
21
  def copy_to_clipboard
@@ -37,21 +36,81 @@ module Cafeznik
37
36
 
38
37
  private
39
38
 
39
+ def log_init
40
+ Log.debug "Initializing Content" do
41
+ <<~LOG
42
+ Source: #{@source.class} file_paths: #{@file_paths.size}
43
+ include_headers: #{@include_headers} include_tree: #{@include_tree}
44
+ LOG
45
+ end
46
+ end
47
+
40
48
  def build_content = [tree_section, files_contents.join("\n\n")].flatten.compact.join("\n\n")
41
49
 
50
+ def tree_section = @include_tree ? with_header(@source.tree.drop(1).join("\n"), "Tree") : nil
51
+ def with_header(content, title) = "==> #{title} <==\n#{content}"
52
+
42
53
  memoize def files_contents
43
- Log.debug "Processing #{@file_paths.size} files"
44
- @file_paths.each_with_object([]) do |file, memo|
45
- content = @source.content(file)
46
- memo << (@include_headers ? with_header(content, file) : content) unless content.empty?
47
- rescue StandardError => e
48
- Log.error("Error fetching content for #{file}: #{e.message}")
49
- nil
54
+ Log.debug "Processing #{@file_paths.size} files in #{THREAD_COUNT} threads"
55
+
56
+ bars = create_progress_bars
57
+ errors = Concurrent::Hash.new
58
+ executor = Concurrent::FixedThreadPool.new(THREAD_COUNT)
59
+
60
+ tasks = create_file_tasks(executor, bars, errors)
61
+
62
+ # Waits for all concurrent tasks to complete within the timeout, then returns their results as a compact array.
63
+ results = Concurrent::Promises.zip(*tasks).value!(THREAD_TIMEOUT).compact
64
+
65
+ executor.shutdown
66
+ executor.wait_for_termination(THREAD_TIMEOUT)
67
+
68
+ report_errors(errors) if errors.any?
69
+ results
70
+ end
71
+
72
+ def create_progress_bars
73
+ progress = TTY::ProgressBar::Multi.new("Processing files [:bar] :percent")
74
+ {
75
+ started: progress.register("Starting [:bar] :current/:total", total: @file_paths.size),
76
+ finished: progress.register("Completed [:bar] :current/:total", total: @file_paths.size)
77
+ }
78
+ end
79
+
80
+ def create_file_tasks(executor, bars, errors)
81
+ @file_paths.map do |file|
82
+ bars[:started].advance
83
+
84
+ Concurrent::Promises.future_on(executor) do
85
+ fetch_and_format_file(file, errors)
86
+ ensure
87
+ bars[:finished].advance
88
+ end
50
89
  end
51
90
  end
52
91
 
53
- def tree_section = @include_tree ? with_header(@source.tree.drop(1).join("\n"), "Tree") : nil
54
- def with_header(content, title) = "==> #{title} <==\n#{content}"
92
+ def fetch_and_format_file(file, errors)
93
+ content = @source.content(file)
94
+ if content && !content.empty?
95
+ @include_headers ? with_header(content, file) : content
96
+ end
97
+ rescue StandardError => e
98
+ errors[file] = e.message
99
+ Log.error("Error fetching content for #{file}: #{e.message}")
100
+ nil
101
+ end
102
+
103
+ def report_errors(errors)
104
+ Log.warn "Completed with #{errors.size} errors:"
105
+ errors.each.with_index(1) do |(file, message), i|
106
+ Log.warn " #{i}. #{file}: #{message}"
107
+ break if i >= 5 && errors.size > 5
108
+ end
109
+
110
+ return unless errors.size > 5
111
+
112
+ Log.warn " ... and #{errors.size - 5} more errors"
113
+ end
55
114
 
56
115
  def confirm_size!
57
116
  line_count = @content.lines.size
@@ -0,0 +1,55 @@
1
+ # rubocop:disable Metrics/MethodLength
2
+ module Cafeznik
3
+ module Help
4
+ def self.display(cli)
5
+ version = Cafeznik::VERSION
6
+ banner = <<~BANNER
7
+ ╔◤ CΛFΞΖПIK v#{version} ◢═══════════╗
8
+ ║─┳─ interactive ║ code2pilfer ║
9
+ ╚═╂══════════════╩══════ꙮ══════╝
10
+ BANNER
11
+ cli.say banner, :green
12
+ cli.say "\n"
13
+
14
+ cli.say_status("Usage", "cafeznik [OPTIONS]", :yellow)
15
+ cli.print_wrapped(
16
+ "The default behavior (local mode) is invoked when no subcommand is given. " \
17
+ "To use GitHub mode, specify a repository with the --repo option.", indent: 2
18
+ )
19
+ cli.say "\n"
20
+
21
+ cli.say_status("Modes", "", :blue)
22
+ modes = [
23
+ ["Local Mode (default)", "Copies files from your local file system."],
24
+ ["GitHub Mode", "Copies files from a GitHub repository (specify with --repo owner/repo)."]
25
+ ]
26
+ cli.print_table(modes, indent: 2, borders: true)
27
+ cli.say "\n"
28
+
29
+ cli.say_status("Options", "", :cyan)
30
+ options = [
31
+ ["--repo, -r", "Specify a GitHub repository (owner/repo format)"],
32
+ ["--grep, -g", "Filter files: include only those containing a specific pattern"],
33
+ ["--exclude", "Exclude files/folders matching given glob patterns"],
34
+ ["--with_tree, -t", "Include the file tree structure in the output"],
35
+ ["--no_header", "Exclude file headers from the copied content"],
36
+ ["--version, -v", "Display version information"],
37
+ ["--verbose", "Enable verbose logging (detailed output)"],
38
+ ["-h/help", "Display this help message"]
39
+ ]
40
+ cli.print_table(options, indent: 2, borders: false)
41
+ cli.say "\n"
42
+
43
+ cli.say_status("Examples", "", :green)
44
+ examples = [
45
+ ["cafeznik", "# Runs in local mode, letting you select local files to copy to the clipboard"],
46
+ ["cafeznik --repo LemuelCushing/cafeznik", "# Runs in GitHub mode and gets the repo"],
47
+ ["cafeznik --no_header --with_tree", "# Does not include headers and includes the file tree"],
48
+ ["cafeznik --grep \"ChunkyBacon.new\"", "# Only includes files where new ChunkyBacon are chunked"]
49
+ ]
50
+ cli.print_table(examples, indent: 2)
51
+ end
52
+ end
53
+ end
54
+
55
+ # rubocop:enable Metrics/MethodLength
@@ -5,7 +5,7 @@ module Cafeznik
5
5
  MAX_FILES = 20
6
6
 
7
7
  def initialize(source)
8
- Log.fatal "fzf is kinda the centerpiece of this little tool here. Go install, deal. I'll be here when you're done" unless ToolChecker.fzf_available?
8
+ Log.fatal "fzf is kinda the centerpiece of this little tool here. Go install it, dear. I'll be here when you're done" unless ToolChecker.fzf_available?
9
9
  @source = source
10
10
  end
11
11
 
@@ -26,7 +26,13 @@ module Cafeznik
26
26
 
27
27
  def select_paths_with_fzf
28
28
  Log.debug "Running fzf"
29
- run_fzf_command.then { |selected| selected.include?("./") ? [:all_files] : selected }
29
+ result = run_fzf_command
30
+ if result.include?("./")
31
+ @select_all = true
32
+ ["./"]
33
+ else
34
+ result
35
+ end
30
36
  rescue TTY::Command::ExitError => e
31
37
  handle_fzf_error(e)
32
38
  end
@@ -37,8 +43,17 @@ module Cafeznik
37
43
  end
38
44
  end
39
45
 
46
+ def preview_command
47
+ return "" unless @source.is_a?(Cafeznik::Source::Local)
48
+
49
+ file_preview = ToolChecker.bat_available? ? "bat --style=numbers --color=always {}" : "tail -n +1 {}"
50
+ warn = "🌳 Preview tree may be off - greps and excludes are not taken into account 🌴\n #{'=' * 100}"
51
+
52
+ "([[ -d {} ]] && (echo '#{warn}'; tree --gitignore -C {} | head -n 50) || #{file_preview})"
53
+ end
54
+
40
55
  def run_fzf_command = TTY::Command.new(printer: Log.verbose? ? :pretty : :null)
41
- .run("fzf --multi", stdin: @source.tree.join("\n"))
56
+ .run("fzf --multi --preview \"#{preview_command}\"", stdin: @source.tree.join("\n"))
42
57
  .out.split("\n")
43
58
 
44
59
  def handle_fzf_error(error)
@@ -53,19 +68,24 @@ module Cafeznik
53
68
  end
54
69
 
55
70
  def expand_paths(paths)
56
- # TODO: I think I can remove the reject here, as it's already done in the source
57
- return @source.all_files.reject { |path| @source.exclude?(path) } if paths == [:all_files]
71
+ if @select_all
72
+ Log.debug "Root directory selected, returning all files"
73
+ return @source.all_files
74
+ end
58
75
 
59
- paths.flat_map do |path|
76
+ result = paths.flat_map do |path|
60
77
  dir?(path) ? @source.expand_dir(path) : path
61
78
  end.uniq
79
+
80
+ Log.debug "Expanded #{paths.size} paths to #{result.size} files"
81
+ result
62
82
  end
63
83
 
64
84
  def confirm_count!(paths)
65
85
  Log.info "Selected #{paths.size} files"
66
86
  return paths if paths.size <= MAX_FILES
67
87
 
68
- Log.warn "Selected more than #{MAX_FILES} files. Continue? (y/N)"
88
+ Log.warn "Selected more than #{MAX_FILES} files (#{paths.size}). Continue? (y/N)"
69
89
  unless CLI.user_agrees?
70
90
  Log.info "Copy operation cancelled by user"
71
91
  exit 0
@@ -3,17 +3,21 @@ module Cafeznik
3
3
  class Base
4
4
  BINARY_EXCLUDES = [
5
5
  # Images and media
6
- %w[*.png *.jpg *.jpeg *.gif *.svg *.ico],
7
- %w[*.pdf *.mov *.mp4 *.mp3 *.wav],
6
+ %w[*.png *.jpg *.jpeg *.gif *.svg *.ico
7
+ *.pdf *.mov *.mp4 *.mp3 *.wav *.cast],
8
8
  # Archives
9
9
  %w[*.zip *.tar.gz *.tgz *.rar *.7z],
10
10
  # Compiled code
11
- %w[*.pyc *.pyo *.class *.jar *.dll],
12
- %w[*.exe *.so *.dylib *.o *.obj],
11
+ %w[*.pyc *.pyo *.class *.jar *.dll
12
+ *.exe *.so *.dylib *.o *.obj],
13
13
  # Minified files
14
14
  %w[*.min.js *.min.css],
15
+ # Lockfiles
16
+ %w[package-lock.json yarn.lock Gemfile.lock],
17
+ # Fonts
18
+ %w[*.woff *.woff2 *.ttf *.eot *.otf],
15
19
  # Pesky necessities
16
- %w[.git .DS_Store Thumbs.db]
20
+ %w[.git .DS_Store Thumbs.db .ruby-lsp]
17
21
  ].flatten.freeze
18
22
 
19
23
  # TODO: change to `root: nil, repo: nil`
@@ -30,16 +34,13 @@ module Cafeznik
30
34
  def full_tree = raise NotImplementedError
31
35
 
32
36
  def exclude?(path)
33
- Log.debug "Checking exclusion for #{path} against #{@exclude}"
34
- excluded = @exclude.any? do |pattern|
37
+ @exclude.any? do |pattern|
35
38
  if pattern.include?(File::SEPARATOR) || pattern.include?("/")
36
39
  File.fnmatch?(pattern, path, File::FNM_PATHNAME)
37
40
  else
38
41
  File.fnmatch?(pattern, File.basename(path))
39
42
  end
40
43
  end
41
- Log.debug "Exclusion result: #{excluded}"
42
- excluded
43
44
  end
44
45
 
45
46
  def all_files = tree.reject(&method(:dir?))
@@ -5,6 +5,9 @@ require "base64"
5
5
  module Cafeznik
6
6
  module Source
7
7
  class GitHub < Base
8
+ MAX_RETRIES = 3
9
+ BASE_DELAY = 2
10
+
8
11
  def initialize(repo:, grep: nil, exclude: [])
9
12
  super
10
13
  @client = Octokit::Client.new(access_token:, auto_paginate: true)
@@ -14,8 +17,8 @@ module Cafeznik
14
17
 
15
18
  def tree
16
19
  @_tree ||= begin
17
- all_paths = @grep ? grep_files(@grep) : full_tree
18
- all_paths.reject { |path| exclude?(path) }
20
+ all_paths = (@grep ? grep_files(@grep) : full_tree)
21
+ all_paths.reject { exclude?(it) }.push("./").sort
19
22
  end
20
23
  rescue Octokit::Error => e
21
24
  Log.error "Error fetching GitHub tree: #{e.message}"
@@ -23,7 +26,7 @@ module Cafeznik
23
26
  end
24
27
 
25
28
  def content(path)
26
- Base64.decode64 @client.contents(@repo, path:)[:content]
29
+ fetch_file_content_with_retry(path)
27
30
  rescue Octokit::Error => e
28
31
  Log.error "Error fetching GitHub content: #{e.message}"
29
32
  nil
@@ -57,9 +60,7 @@ module Cafeznik
57
60
 
58
61
  def full_tree
59
62
  branch = @client.repository(@repo).default_branch
60
- # get all all paths and add a trailing slash for directories
61
- paths = @client.tree(@repo, branch, recursive: true).tree.map { "#{it.path}#{'/' if it.type == 'tree'}" }
62
- (["./"] + paths).sort
63
+ @client.tree(@repo, branch, recursive: true).tree.map { "#{it.path}#{'/' if it.type == 'tree'}" }
63
64
  end
64
65
 
65
66
  def fetch_token_via_gh
@@ -73,6 +74,7 @@ module Cafeznik
73
74
 
74
75
  def grep_files(pattern)
75
76
  Log.debug "Searching for pattern '#{pattern}' within #{@repo}"
77
+ Log.fatal "We can't search for empty patterns. Please provide a pattern to search for" if pattern.empty?
76
78
  results = @client.search_code("#{pattern} repo:#{@repo} in:file").items.map(&:path)
77
79
  Log.debug "Found #{results.size} files matching pattern '#{pattern}' in #{@repo}"
78
80
  results
@@ -80,6 +82,22 @@ module Cafeznik
80
82
  Log.error "Error during search for pattern '#{pattern}': #{e.message}"
81
83
  []
82
84
  end
85
+
86
+ def fetch_file_content_with_retry(path, retries = MAX_RETRIES, base_delay = BASE_DELAY)
87
+ content_data = @client.contents(@repo, path: path)[:content]
88
+ Base64.decode64(content_data)
89
+ rescue Octokit::TooManyRequests
90
+ handle_rate_limit(path, retries, base_delay)
91
+ end
92
+
93
+ def handle_rate_limit(path, retries, base_delay)
94
+ return raise if retries <= 0
95
+
96
+ delay = (base_delay**(MAX_RETRIES - retries + 1)) * (0.8 + (0.4 * rand))
97
+ Log.warn "Rate limit exceeded, waiting #{delay.round(1)} seconds..."
98
+ sleep(delay)
99
+ fetch_file_content_with_retry(path, retries - 1, base_delay)
100
+ end
83
101
  end
84
102
  end
85
103
  end
@@ -1,3 +1,3 @@
1
1
  module Cafeznik
2
- VERSION = "0.5.61".freeze
2
+ VERSION = "0.8.0".freeze
3
3
  end
data/lib/cafeznik.rb CHANGED
@@ -5,3 +5,4 @@ require_relative "cafeznik/sources"
5
5
  require_relative "cafeznik/content"
6
6
  require_relative "cafeznik/tool_checker"
7
7
  require_relative "cafeznik/version"
8
+ require_relative "cafeznik/help"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cafeznik
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.61
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lem
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-14 00:00:00.000000000 Z
10
+ date: 2025-03-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: concurrent-ruby
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: faraday-multipart
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -121,6 +135,20 @@ dependencies:
121
135
  - - "~>"
122
136
  - !ruby/object:Gem::Version
123
137
  version: '0.10'
138
+ - !ruby/object:Gem::Dependency
139
+ name: tty-progressbar
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.18'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '0.18'
124
152
  - !ruby/object:Gem::Dependency
125
153
  name: rspec
126
154
  requirement: !ruby/object:Gem::Requirement
@@ -195,16 +223,16 @@ dependencies:
195
223
  name: super_diff
196
224
  requirement: !ruby/object:Gem::Requirement
197
225
  requirements:
198
- - - ">="
226
+ - - "~>"
199
227
  - !ruby/object:Gem::Version
200
- version: '0'
228
+ version: '0.15'
201
229
  type: :development
202
230
  prerelease: false
203
231
  version_requirements: !ruby/object:Gem::Requirement
204
232
  requirements:
205
- - - ">="
233
+ - - "~>"
206
234
  - !ruby/object:Gem::Version
207
- version: '0'
235
+ version: '0.15'
208
236
  - !ruby/object:Gem::Dependency
209
237
  name: webmock
210
238
  requirement: !ruby/object:Gem::Requirement
@@ -234,6 +262,7 @@ files:
234
262
  - lib/cafeznik.rb
235
263
  - lib/cafeznik/cli.rb
236
264
  - lib/cafeznik/content.rb
265
+ - lib/cafeznik/help.rb
237
266
  - lib/cafeznik/log.rb
238
267
  - lib/cafeznik/selector.rb
239
268
  - lib/cafeznik/sources.rb
@@ -261,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
261
290
  - !ruby/object:Gem::Version
262
291
  version: '0'
263
292
  requirements: []
264
- rubygems_version: 3.6.4
293
+ rubygems_version: 3.6.2
265
294
  specification_version: 4
266
295
  summary: CLI tool for copying files to your clipboard en masse
267
296
  test_files: []