sight 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32b20a4a62fbca9fee67a4354538a8e033dc079b945c7aa5a6f6d23f63c0fac6
4
- data.tar.gz: 4be26d309ed3cb0649bc093a78326d6f9a4a2ce2f2f3f3ec9943b713bac9f372
3
+ metadata.gz: 144154ab3f119a135766e0deda10adc5ff600e404374801d6449e292d8f0b063
4
+ data.tar.gz: 97a888745c5407114b3b7ac8c06e9866ffdadb97d6d2d16518799f29328e68f6
5
5
  SHA512:
6
- metadata.gz: 7acf8bc51543269963f35953c5e7ffe7452483e655330bd5d9c314c394a575f6710233fe26b117eaa7ca00491c0d4f561c879e2c7ab160fcb32a6d3db15bf7e4
7
- data.tar.gz: 696a3493aa44d9e7913913df11f9ef583de057e15bc210811027121a629534d486c7cb7c197180c146644aadba2eea331db8d49315c83da9197d2a4116f472ca
6
+ metadata.gz: 05c55c338942fafe117834bb8e310d675944f9406346c2250e7638a68b775f7736131f46c47f4c2f39303ac0771697ab8f4e7236021991c589878cecfdbfac3c
7
+ data.tar.gz: 6db3dfb819a2b5b4ff43f13fe6272904fa58dd25e644e32bf30aa0d4aea40943c8c94d4e1530508373e8f51c7b6d6212c53ffdc33ea52871f0c0235128f340b1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.1] - 2026-03-26
4
+
5
+ ### Fixed
6
+
7
+ - Resolve Claude config from `~/.claude`, `XDG_CONFIG_HOME`, and `~/.config/claude` instead of hardcoding `~/.config/claude`
8
+
9
+ ## [0.6.0] - 2026-03-26
10
+
11
+ ### Changed
12
+
13
+ - Show annotation summary instead of full dump on quit
14
+ - Extracted `Summary` class from `AnnotationFormatter`
15
+ - Converted `AnnotationFormatter` from module to class
16
+
3
17
  ## [0.5.0] - 2026-03-07
4
18
 
5
19
  ### Removed
data/CLAUDE.md CHANGED
@@ -33,4 +33,5 @@ Reads `.git/sight/pending-review`, outputs annotations, and deletes the file. Hi
33
33
  - Ruby >= 3.2, uses `frozen_string_literal` in all files
34
34
  - Linter: StandardRB (ruby_version: 3.4 in `.standard.yml`)
35
35
  - Tests: Minitest with stubs/mocks, no test framework beyond minitest
36
+ - In tests, assign the subject under test to a local before asserting on it
36
37
  - Single runtime dependency: `curses ~1.4`
@@ -1,29 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sight
4
- module AnnotationFormatter
5
- module_function
4
+ class AnnotationFormatter
5
+ def initialize(annotations)
6
+ @annotations = annotations
7
+ end
6
8
 
7
- def format(annotations)
9
+ def format
8
10
  return "" if annotations.empty?
9
11
 
10
- grouped = annotations.group_by(&:file_path)
11
- grouped.map { |path, file_annotations| format_file(path, file_annotations) }.join("\n")
12
+ annotations.group_by(&:file_path).map do |path, file_annotations|
13
+ format_file(path, file_annotations)
14
+ end.join("\n")
12
15
  end
13
16
 
14
- def format_file(path, annotations)
17
+ private
18
+
19
+ attr_reader :annotations
20
+
21
+ def format_file(path, file_annotations)
15
22
  out = "## File: #{path}\n\n"
16
- annotations.each do |annotation|
17
- context = annotation.hunk.context ? " #{annotation.hunk.context}" : ""
23
+
24
+ file_annotations.each do |file_annotation|
25
+ context = file_annotation.hunk.context ? " #{file_annotation.hunk.context}" : ""
18
26
  out << "Hunk (@@#{context}):\n"
19
27
  out << "```diff\n"
20
- annotation.hunk.lines.each do |line|
28
+ file_annotation.hunk.lines.each do |line|
21
29
  next unless %i[add del].include?(line.type)
22
30
  out << "#{line.content}\n"
23
31
  end
24
32
  out << "```\n"
25
- out << "> #{annotation.comment}\n\n"
33
+ out << "> #{file_annotation.comment}\n\n"
26
34
  end
35
+
27
36
  out
28
37
  end
29
38
  end
data/lib/sight/app.rb CHANGED
@@ -4,17 +4,15 @@ require "curses"
4
4
 
5
5
  module Sight
6
6
  class App
7
- attr_reader :files, :file_lines, :annotations
8
- attr_accessor :file_idx, :offset, :hunk_idx
7
+ attr_reader :files, :lines, :annotations
8
+ attr_accessor :offset, :hunk_idx
9
9
 
10
10
  def initialize(files)
11
11
  @files = files
12
- @file_lines = files.map { build_file_lines(it) }
13
- @file_idx = 0
14
12
  @offset = 0
15
13
  @hunk_idx = 0
16
- @hunk_offsets_cache = {}
17
14
  @annotations = []
15
+ build_flat_view
18
16
  end
19
17
 
20
18
  def run
@@ -31,11 +29,24 @@ module Sight
31
29
 
32
30
  private
33
31
 
34
- def build_file_lines(file)
35
- file.hunks.flat_map do |hunk|
36
- hunk.lines.map do |diff_line|
37
- text = (diff_line.type == :meta) ? diff_line.content : diff_line.content[1..]
38
- DisplayLine.new(type: diff_line.type, text: text, lineno: diff_line.lineno)
32
+ def build_flat_view
33
+ @lines = []
34
+ @hunk_offsets = []
35
+ @hunk_entries = []
36
+
37
+ @files.each_with_index do |file, file_idx|
38
+ if file_idx > 0
39
+ @lines << DisplayLine.new(type: :file_separator, text: nil, lineno: nil)
40
+ end
41
+
42
+ file.hunks.each do |hunk|
43
+ @hunk_offsets << @lines.size
44
+ @hunk_entries << [file, hunk]
45
+
46
+ hunk.lines.each do |diff_line|
47
+ text = (diff_line.type == :meta) ? diff_line.content : diff_line.content[1..]
48
+ @lines << DisplayLine.new(type: diff_line.type, text: text, lineno: diff_line.lineno)
49
+ end
39
50
  end
40
51
  end
41
52
  end
@@ -56,10 +67,6 @@ module Sight
56
67
  Curses.init_pair(6, Curses::COLOR_MAGENTA, -1)
57
68
  end
58
69
 
59
- def lines
60
- file_lines[file_idx]
61
- end
62
-
63
70
  def render
64
71
  win = Curses.stdscr
65
72
  win.clear
@@ -71,19 +78,15 @@ module Sight
71
78
  end
72
79
 
73
80
  def render_header(win, width)
74
- file = files[file_idx]
75
- badge = "[#{file.status || :modified}]"
76
- path = file.path
77
- gap = width - path.length - badge.length
81
+ file, hunk = current_hunk_entry
82
+ return unless file
83
+ badge = " [#{file.status || :modified}]"
84
+ left = "\u2500\u2500 #{file.path} "
85
+ left += "\u2500\u2500 #{hunk.context} " if hunk.context && !hunk.context.empty?
86
+ fill_len = [width - left.length - badge.length, 0].max
78
87
  win.setpos(0, 0)
79
- if gap >= 1
80
- win.attron(color_for(:header)) { win.addstr("#{path}#{" " * gap}") }
81
- win.attron(badge_color(file.status)) { win.addstr(badge) }
82
- else
83
- win.attron(color_for(:header)) { win.addstr(path[0, width]) }
84
- end
85
- win.setpos(1, 0)
86
- win.attron(Curses.color_pair(0) | Curses::A_BOLD) { win.addstr("\u2500" * width) }
88
+ win.attron(color_for(:file_header)) { win.addstr("#{left}#{"\u2500" * fill_len}"[0, width - badge.length]) }
89
+ win.attron(badge_color(file.status)) { win.addstr(badge) }
87
90
  end
88
91
 
89
92
  def render_content(win, width)
@@ -100,12 +103,18 @@ module Sight
100
103
  idx = offset + row
101
104
  break if idx >= lines.size
102
105
  line = lines[idx]
103
- win.setpos(row + 2, 0)
106
+ win.setpos(row + 1, 0)
107
+
108
+ if line.type == :file_separator
109
+ win.attron(color_for(:file_header)) { win.addstr("\u2500" * width) }
110
+ next
111
+ end
112
+
104
113
  active = idx >= selected_start && idx < selected_end
105
114
  gutter_str = format_gutter(line.type, line.lineno, gutter)
106
115
  commented = commented_lines.include?(idx)
107
116
  separator = commented ? "\u2503" : "\u2502"
108
- sep_attr = commented ? Curses.color_pair(4) : dim
117
+ sep_attr = commented ? Curses.color_pair(4) : color_for(:file_header)
109
118
  win.attron(dim) { win.addstr("#{gutter_str} ") }
110
119
  win.attron(sep_attr) { win.addstr(separator) }
111
120
  win.attron(dim) { win.addstr(" ") }
@@ -122,8 +131,8 @@ module Sight
122
131
  else
123
132
  ((offset + scroll_height) * 100.0 / lines.size).ceil.clamp(0, 100)
124
133
  end
125
- commented = hunk_commented?(file_idx, hunk_idx) ? " [commented]" : ""
126
- status = " File #{file_idx + 1}/#{files.size} | Hunk #{hunk_idx + 1}/#{hunk_offsets.size}#{commented} | #{percent}% "
134
+ commented = hunk_commented?(hunk_idx) ? " [commented]" : ""
135
+ status = " Hunk #{hunk_idx + 1}/#{hunk_offsets.size}#{commented} | #{percent}% "
127
136
  win.addstr(status.ljust(width))
128
137
  end
129
138
  end
@@ -143,6 +152,7 @@ module Sight
143
152
  when :add then Curses.color_pair(1)
144
153
  when :del then Curses.color_pair(2)
145
154
  when :header then Curses.color_pair(0) | Curses::A_BOLD
155
+ when :file_header then Curses.color_pair(3) | Curses::A_BOLD
146
156
  else Curses.color_pair(0)
147
157
  end
148
158
  end
@@ -157,12 +167,12 @@ module Sight
157
167
  end
158
168
 
159
169
  def scroll_height
160
- Curses.lines - 3
170
+ Curses.lines - 2
161
171
  end
162
172
 
163
173
  def gutter_width
164
174
  @gutter_width ||= begin
165
- max = file_lines.flat_map { |file| file.filter_map { |line| line.lineno } }.max || 1
175
+ max = @lines.filter_map(&:lineno).max || 1
166
176
  max.to_s.length
167
177
  end
168
178
  end
@@ -171,14 +181,14 @@ module Sight
171
181
  key = Curses.getch
172
182
  case key
173
183
  when "q", 27 then return false
174
- when "j" then jump_hunk(1)
175
- when "k" then jump_hunk(-1)
176
- when "n" then jump_file(1)
177
- when "p" then jump_file(-1)
184
+ when "j" then scroll(1)
185
+ when "k" then scroll(-1)
178
186
  when 6 then scroll(scroll_height)
179
187
  when 2 then scroll(-scroll_height)
180
188
  when 4 then scroll(scroll_height / 2)
181
189
  when 21 then scroll(-scroll_height / 2)
190
+ when "n", 14 then jump_hunk(1)
191
+ when "p", 16 then jump_hunk(-1)
182
192
  when "c" then annotate_hunk
183
193
  when "?" then show_help
184
194
  end
@@ -186,14 +196,14 @@ module Sight
186
196
  end
187
197
 
188
198
  HELP_KEYS = [
189
- ["j", "Next hunk"],
190
- ["k", "Previous hunk"],
199
+ ["j", "Scroll down"],
200
+ ["k", "Scroll up"],
191
201
  ["C-f", "Page down"],
192
202
  ["C-b", "Page up"],
193
203
  ["C-d", "Half page down"],
194
204
  ["C-u", "Half page up"],
195
- ["n", "Next file"],
196
- ["p", "Previous file"],
205
+ ["n / C-n", "Next hunk"],
206
+ ["p / C-p", "Previous hunk"],
197
207
  ["q / Esc", "Quit"],
198
208
  ["c", "Comment on hunk"],
199
209
  ["?", "Toggle this help"]
@@ -216,32 +226,32 @@ module Sight
216
226
  def draw_box(win, top, left, width, height, title, content_lines)
217
227
  win.attron(Curses.color_pair(0)) do
218
228
  win.setpos(top, left)
219
- win.addstr("┌#{"" * (width - 2)}")
229
+ win.addstr("\u250C#{"\u2500" * (width - 2)}\u2510")
220
230
 
221
231
  win.setpos(top + 1, left)
222
232
  pad = width - 2 - title.length
223
- win.addstr("│#{" " * (pad / 2)}#{title}#{" " * (pad - pad / 2)}")
233
+ win.addstr("\u2502#{" " * (pad / 2)}#{title}#{" " * (pad - pad / 2)}\u2502")
224
234
 
225
235
  win.setpos(top + 2, left)
226
- win.addstr("├#{"" * (width - 2)}")
236
+ win.addstr("\u251C#{"\u2500" * (width - 2)}\u2524")
227
237
 
228
238
  content_lines.each_with_index do |line, i|
229
239
  win.setpos(top + 3 + i, left)
230
- win.addstr(" #{line.ljust(width - 5)} ")
240
+ win.addstr("\u2502 #{line.ljust(width - 5)} \u2502")
231
241
  end
232
242
 
233
243
  win.setpos(top + height - 1, left)
234
- win.addstr("└#{"" * (width - 2)}")
244
+ win.addstr("\u2514#{"\u2500" * (width - 2)}\u2518")
235
245
  end
236
246
  end
237
247
 
238
248
  def annotate_hunk
239
- hunk = files[file_idx].hunks[hunk_idx]
249
+ file, hunk = current_hunk_entry
240
250
  return unless hunk
241
251
  comment = prompt_comment("Comment on hunk")
242
252
  return unless comment
243
253
  @annotations << Annotation.new(
244
- file_path: files[file_idx].path,
254
+ file_path: file.path,
245
255
  type: :hunk,
246
256
  hunk: hunk,
247
257
  comment: comment
@@ -293,31 +303,25 @@ module Sight
293
303
  text&.strip&.empty? ? nil : text&.strip
294
304
  end
295
305
 
296
- def hunk_offsets
297
- @hunk_offsets_cache[file_idx] ||= begin
298
- offsets = []
299
- line_idx = 0
300
- files[file_idx].hunks.each do |hunk|
301
- offsets << line_idx
302
- line_idx += hunk.lines.size
303
- end
304
- offsets
305
- end
306
+ def current_hunk_entry
307
+ @hunk_entries[hunk_idx] || [nil, nil]
306
308
  end
307
309
 
310
+ attr_reader :hunk_offsets
311
+
308
312
  def hunk_end_offset(idx)
309
313
  (idx + 1 < hunk_offsets.size) ? hunk_offsets[idx + 1] : lines.size
310
314
  end
311
315
 
312
- def hunk_commented?(file_index, hunk_index)
313
- path = files[file_index].path
314
- hunk = files[file_index].hunks[hunk_index]
315
- annotations.any? { |a| a.file_path == path && a.hunk.equal?(hunk) }
316
+ def hunk_commented?(hunk_index)
317
+ file, hunk = @hunk_entries[hunk_index]
318
+ return false unless file
319
+ annotations.any? { |a| a.file_path == file.path && a.hunk.equal?(hunk) }
316
320
  end
317
321
 
318
322
  def commented_hunk_lines
319
323
  hunk_offsets.each_with_index.each_with_object(Set.new) do |(offset, hunk_index), set|
320
- next unless hunk_commented?(file_idx, hunk_index)
324
+ next unless hunk_commented?(hunk_index)
321
325
  (offset...hunk_end_offset(hunk_index)).each { |i| set << i }
322
326
  end
323
327
  end
@@ -325,6 +329,12 @@ module Sight
325
329
  def scroll(delta)
326
330
  max = [0, lines.size - scroll_height].max
327
331
  self.offset = (offset + delta).clamp(0, max)
332
+ sync_hunk_to_offset
333
+ end
334
+
335
+ def sync_hunk_to_offset
336
+ return if hunk_offsets.empty?
337
+ self.hunk_idx = hunk_offsets.rindex { |o| o <= offset } || 0
328
338
  end
329
339
 
330
340
  def jump_hunk(delta)
@@ -335,13 +345,5 @@ module Sight
335
345
  max = [0, lines.size - scroll_height].max
336
346
  self.offset = [target - margin, 0].max.clamp(0, max)
337
347
  end
338
-
339
- def jump_file(direction)
340
- new_idx = (file_idx + direction).clamp(0, files.size - 1)
341
- return if new_idx == file_idx
342
- self.file_idx = new_idx
343
- self.offset = 0
344
- self.hunk_idx = 0
345
- end
346
348
  end
347
349
  end
@@ -10,7 +10,13 @@ module Sight
10
10
  module_function
11
11
 
12
12
  def settings_path
13
- File.join(Dir.home, ".config", "claude", "settings.json")
13
+ candidates = [
14
+ (File.join(ENV["XDG_CONFIG_HOME"], "claude", "settings.json") if ENV["XDG_CONFIG_HOME"]),
15
+ File.join(Dir.home, ".claude", "settings.json"),
16
+ File.join(Dir.home, ".config", "claude", "settings.json")
17
+ ].compact
18
+
19
+ candidates.find { File.exist?(it) } || candidates.last
14
20
  end
15
21
 
16
22
  def install(path: settings_path)
data/lib/sight/cli.rb CHANGED
@@ -48,9 +48,8 @@ module Sight
48
48
  app.run
49
49
 
50
50
  unless app.annotations.empty?
51
- formatted = AnnotationFormatter.format(app.annotations)
52
- puts formatted
53
- Git.save_pending_review(formatted)
51
+ Git.save_pending_review(AnnotationFormatter.new(app.annotations).format)
52
+ puts Summary.new(app.annotations)
54
53
  end
55
54
  end
56
55
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sight
4
+ class Summary
5
+ def initialize(annotations)
6
+ @annotations = annotations
7
+ end
8
+
9
+ def to_s
10
+ "#{pluralize_annotations} on #{pluralize_files}"
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :annotations
16
+
17
+ def pluralize_annotations
18
+ "#{annotations.size} #{(annotations.size == 1) ? "annotation" : "annotations"}"
19
+ end
20
+
21
+ def pluralize_files
22
+ "#{file_count} #{(file_count == 1) ? "file" : "files"}"
23
+ end
24
+
25
+ def file_count
26
+ annotations.map(&:file_path).uniq.size
27
+ end
28
+ end
29
+ end
data/lib/sight/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sight
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.1"
5
5
  end
data/lib/sight.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "sight/version"
4
- require_relative "sight/display_line"
5
- require_relative "sight/diff_parser"
6
- require_relative "sight/git"
7
3
  require_relative "sight/annotation"
8
4
  require_relative "sight/annotation_formatter"
5
+ require_relative "sight/app"
9
6
  require_relative "sight/claude_hook_installer"
10
7
  require_relative "sight/cli"
11
- require_relative "sight/app"
8
+ require_relative "sight/diff_parser"
9
+ require_relative "sight/display_line"
10
+ require_relative "sight/git"
11
+ require_relative "sight/summary"
12
+ require_relative "sight/version"
12
13
 
13
14
  module Sight
14
15
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ariel Rzezak
@@ -49,6 +49,7 @@ files:
49
49
  - lib/sight/diff_parser.rb
50
50
  - lib/sight/display_line.rb
51
51
  - lib/sight/git.rb
52
+ - lib/sight/summary.rb
52
53
  - lib/sight/version.rb
53
54
  - sig/sight.rbs
54
55
  homepage: https://github.com/arzezak/sight