sergeant 1.0.8 → 1.0.9

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: 36d4f8c54ef4d4f7168cb42c81abe472b8094bf67c648096e9513bdfcca9ad15
4
- data.tar.gz: 820962c25da75cc167c88dfe3407fb1de3e294444bd5c737ec09779c46414aed
3
+ metadata.gz: 0ac7149617ce3dd0a2e0c767ff46f6d2627fe917383303835f40a37461097dd8
4
+ data.tar.gz: 3d5621c41d6eec97194f62cab6ed525e1e1dd826fc9ed3fce3f9a08d67fba52a
5
5
  SHA512:
6
- metadata.gz: 1838e264508e70d0c4012560764396c7772e9fb2a50ea1517c52ad584ced2f5d2cecdc7d19efab344ae25d80bb036964f8aa11d39667d5f79afedac05c4c75e4
7
- data.tar.gz: 8e00de4e37bb7e3d971f39eb1dbce3932131941b80af2549c0e778d23a3c5a6a90b0d57e5aa956582467e112d3e052e0f496c71d602c61ccebbd06a453bff2cd
6
+ metadata.gz: 1168b03a1a86233bebd92e7b287d10e33264e4d80b025bb06087b48a2560fa3f8f6c1c35e16634eedf64a7a60e723a01fb0755481ad7f32e9748a871a9be5639
7
+ data.tar.gz: 4111f86ea5fc31114d1e6a691bebf3e3a21beb4b1de6b4e26a1d34d11825c895ff3e3acdf568d5e1c818838402f06709e3bf715d12687725b1d9ea6530d4da9d
data/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [1.0.9] - 2026-08-19
10
+
11
+ ### Added
12
+ - **Syntax highlighting in the side preview panel** - text file previews are now colorized (keywords, strings, comments, numbers, names) using [Rouge](https://github.com/rouge-ruby/rouge), which guesses the language from the filename
13
+
14
+ ### Fixed
15
+ - **Starship / Ruby-version-manager compatibility** - `Gemfile` no longer declares `ruby '>= 2.7.0'`, a version-range constraint that tools which auto-detect a project's Ruby version from the Gemfile (e.g. Starship's prompt) can't parse, causing an `Unknown ruby interpreter version` error just from `cd`-ing into the project. The constraint is already enforced by `required_ruby_version` in the gemspec.
16
+
9
17
  ## [1.0.8] - 2026-08-19
10
18
 
11
19
  ### Fixed
data/Gemfile CHANGED
@@ -5,6 +5,9 @@ source 'https://rubygems.org'
5
5
  # Terminal UI library
6
6
  gem 'curses', '~> 1.4'
7
7
 
8
+ # Syntax highlighting for the file preview panel
9
+ gem 'rouge', '~> 4.0'
10
+
8
11
  group :development do
9
12
  # Code linting and style checking
10
13
  gem 'rubocop', '~> 1.81', require: false
data/README.md CHANGED
@@ -16,7 +16,7 @@ Simple, fast, and elegant.
16
16
  - 🗂️ **Visual Directory Navigation** - See all directories and files at a glance
17
17
  - ⌨️ **Keyboard Driven** - Arrow keys, vim bindings (hjkl), and shortcuts
18
18
  - 🎨 **Color-Coded Display** - Directories in cyan; files colored by type (code, archives, media, executables)
19
- - 👀 **Live Preview Pane** - Text files and directory contents preview in a side panel as you move (press 'P' to toggle)
19
+ - 👀 **Live Preview Pane** - Text files and directory contents preview in a side panel as you move (press 'P' to toggle), with syntax highlighting powered by [Rouge](https://github.com/rouge-ruby/rouge)
20
20
  - 📊 **Smart Scrolling** - Handles directories with hundreds of items
21
21
  - 🔍 **Git Branch Display** - Shows current git branch in header
22
22
  - 👤 **Ownership Toggle** - View file permissions and ownership (press 'o')
@@ -26,6 +26,18 @@ module Sergeant
26
26
  executable: 10
27
27
  }.freeze
28
28
 
29
+ # Maps token_category (Sergeant::Utils) to the syntax-highlight color
30
+ # pairs set up in Sergeant#apply_color_theme (pairs 11-16).
31
+ PREVIEW_TOKEN_COLOR_PAIRS = {
32
+ keyword: 11,
33
+ string: 12,
34
+ comment: 13,
35
+ number: 14,
36
+ function: 15,
37
+ constant: 15,
38
+ error: 16
39
+ }.freeze
40
+
29
41
  def draw_screen
30
42
  # `erase` only rewrites the virtual buffer; `refresh` then diffs it
31
43
  # against the physical screen and repaints just the changed cells.
@@ -222,7 +234,7 @@ module Sergeant
222
234
  if @preview_lines.empty?
223
235
  draw_preview_message(col, width, '(empty file)')
224
236
  else
225
- draw_preview_lines(col, width, visible_lines, @preview_lines, color_pair(2))
237
+ draw_preview_highlighted_lines(col, width, visible_lines, @preview_lines)
226
238
  end
227
239
  when :directory
228
240
  if @preview_lines.empty?
@@ -274,6 +286,37 @@ module Sergeant
274
286
  end
275
287
  end
276
288
 
289
+ # Like draw_preview_lines, but each line is an array of
290
+ # {text:, category:} segments (built by Sergeant#build_text_preview via
291
+ # Rouge) instead of a single string, so every token can carry its own
292
+ # syntax-highlight color.
293
+ def draw_preview_highlighted_lines(col, width, visible_lines, lines)
294
+ visible_lines.times do |idx|
295
+ setpos(idx + 3, col)
296
+ draw_preview_segments(lines[idx] || [], width)
297
+ end
298
+ end
299
+
300
+ def draw_preview_segments(segments, width)
301
+ remaining = width
302
+
303
+ segments.each do |segment|
304
+ break if remaining <= 0
305
+
306
+ text = segment[:text]
307
+ chunk = text.length > remaining ? text[0, remaining] : text
308
+
309
+ attron(preview_token_attr(segment[:category])) { addstr(chunk) }
310
+ remaining -= chunk.length
311
+ end
312
+
313
+ attron(color_pair(2)) { addstr(' ' * remaining) } if remaining.positive?
314
+ end
315
+
316
+ def preview_token_attr(category)
317
+ color_pair(PREVIEW_TOKEN_COLOR_PAIRS[category] || 2)
318
+ end
319
+
277
320
  def draw_preview_message(col, width, message)
278
321
  setpos(3, col)
279
322
  attron(color_pair(2) | Curses::A_DIM) do
@@ -26,6 +26,22 @@ module Sergeant
26
26
  :file
27
27
  end
28
28
 
29
+ # Maps a Rouge token's qualified name (e.g. "Literal.String.Double") to
30
+ # one of the syntax-highlight color pairs set up in
31
+ # Sergeant#apply_color_theme. Unmatched token types (Text, Punctuation,
32
+ # Operator, ...) return nil, meaning "draw in the default text color".
33
+ def token_category(qualname)
34
+ case qualname
35
+ when /\AComment/ then :comment
36
+ when /\AKeyword/ then :keyword
37
+ when /\ALiteral\.String/ then :string
38
+ when /\ALiteral\.Number/ then :number
39
+ when /\AName\.(Function|Class|Namespace|Tag)/ then :function
40
+ when /\AName\.(Constant|Builtin|Variable\.Class)/ then :constant
41
+ when /\A(Error|Generic\.Error|Generic\.Traceback)/ then :error
42
+ end
43
+ end
44
+
29
45
  def get_git_branch
30
46
  return nil unless Dir.exist?(File.join(@current_dir, '.git'))
31
47
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sergeant
4
- VERSION = '1.0.8'
4
+ VERSION = '1.0.9'
5
5
  end
data/lib/sergeant.rb CHANGED
@@ -7,6 +7,7 @@ require 'curses'
7
7
  require 'pathname'
8
8
  require 'etc'
9
9
  require 'fileutils'
10
+ require 'rouge'
10
11
 
11
12
  require_relative 'sergeant/version'
12
13
  require_relative 'sergeant/config'
@@ -184,6 +185,15 @@ class SergeantApp
184
185
  init_pair(8, Sergeant::Config.get_color(@config['media']), Curses::COLOR_BLACK)
185
186
  init_pair(9, Sergeant::Config.get_color(@config['code']), Curses::COLOR_BLACK)
186
187
  init_pair(10, Sergeant::Config.get_color(@config['executables']), Curses::COLOR_BLACK)
188
+
189
+ # Syntax highlighting for the preview panel - not user-configurable
190
+ # (yet), curses only gives us the 8 base colors to work with anyway.
191
+ init_pair(11, Curses::COLOR_MAGENTA, Curses::COLOR_BLACK) # keyword
192
+ init_pair(12, Curses::COLOR_GREEN, Curses::COLOR_BLACK) # string
193
+ init_pair(13, Curses::COLOR_WHITE, Curses::COLOR_BLACK) # comment (dimmed)
194
+ init_pair(14, Curses::COLOR_CYAN, Curses::COLOR_BLACK) # number
195
+ init_pair(15, Curses::COLOR_YELLOW, Curses::COLOR_BLACK) # function/class name
196
+ init_pair(16, Curses::COLOR_RED, Curses::COLOR_BLACK) # error token
187
197
  end
188
198
 
189
199
  def search_files
@@ -384,7 +394,7 @@ class SergeantApp
384
394
  if item[:type] == :directory
385
395
  [:directory, build_directory_preview(item[:path])]
386
396
  elsif text_file?(item[:path])
387
- [:text, build_text_preview(item[:path])]
397
+ [:text, build_text_preview(item[:path], item[:name])]
388
398
  else
389
399
  [:binary, []]
390
400
  end
@@ -405,20 +415,65 @@ class SergeantApp
405
415
  []
406
416
  end
407
417
 
408
- def build_text_preview(path)
409
- lines = []
418
+ def build_text_preview(path, filename)
419
+ raw_lines = []
410
420
 
411
421
  File.foreach(path, mode: 'rb') do |line|
412
422
  text = line.byteslice(0, PREVIEW_LINE_MAX_BYTES).to_s.chomp
413
- lines << text.force_encoding('UTF-8').scrub('?').gsub("\t", ' ')
414
- break if lines.length >= PREVIEW_LINE_LIMIT
423
+ raw_lines << text.force_encoding('UTF-8').scrub('?').gsub("\t", ' ')
424
+ break if raw_lines.length >= PREVIEW_LINE_LIMIT
415
425
  end
416
426
 
417
- lines
427
+ highlight_lines(raw_lines, filename)
418
428
  rescue StandardError
419
429
  []
420
430
  end
421
431
 
432
+ # Runs the preview text through Rouge and reassembles it into one array
433
+ # per line of [{text:, category:}, ...] segments, so the preview panel can
434
+ # paint each token in its own color. Falls back to plain (uncategorized)
435
+ # lines if Rouge can't make sense of the file - a highlighting bug should
436
+ # never be able to break the preview itself.
437
+ def highlight_lines(raw_lines, filename)
438
+ return [] if raw_lines.empty?
439
+
440
+ text = raw_lines.join("\n")
441
+ lexer = guess_lexer(filename, text)
442
+ tokens_to_lines(lexer, text)
443
+ rescue StandardError
444
+ raw_lines.map { |line| [{ text: line, category: nil }] }
445
+ end
446
+
447
+ def guess_lexer(filename, text)
448
+ Rouge::Lexer.guess(filename: filename, source: text)
449
+ rescue Rouge::Guesser::Ambiguous => e
450
+ e.alternatives.first || Rouge::Lexers::PlainText
451
+ rescue StandardError
452
+ Rouge::Lexers::PlainText
453
+ end
454
+
455
+ def tokens_to_lines(lexer, text)
456
+ lines = []
457
+ current = []
458
+
459
+ lexer.lex(text) do |token, value|
460
+ category = token_category(token.qualname)
461
+ pieces = value.split("\n", -1)
462
+
463
+ pieces.each_with_index do |piece, idx|
464
+ current << { text: piece, category: category } unless piece.empty?
465
+
466
+ next if idx == pieces.length - 1
467
+
468
+ lines << current
469
+ current = []
470
+ end
471
+ end
472
+
473
+ lines << current unless current.empty?
474
+ lines
475
+ end
476
+
422
477
  def refresh_items
423
478
  entries = Dir.entries(@current_dir).reject { |e| e == '.' || e == '..' }
424
479
  @items = []
data/sergeant.gemspec CHANGED
@@ -57,6 +57,7 @@ Gem::Specification.new do |spec|
57
57
 
58
58
  # Runtime dependencies
59
59
  spec.add_dependency 'curses', '~> 1.4'
60
+ spec.add_dependency 'rouge', '~> 4.0'
60
61
 
61
62
  # Development dependencies
62
63
  spec.add_development_dependency 'rspec', '~> 3.13'
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sergeant
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.8
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Grotha
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-08-24 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: curses
@@ -23,6 +24,20 @@ dependencies:
23
24
  - - "~>"
24
25
  - !ruby/object:Gem::Version
25
26
  version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rouge
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
26
41
  - !ruby/object:Gem::Dependency
27
42
  name: rspec
28
43
  requirement: !ruby/object:Gem::Requirement
@@ -106,7 +121,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
121
  - !ruby/object:Gem::Version
107
122
  version: '0'
108
123
  requirements: []
109
- rubygems_version: 4.0.3
124
+ rubygems_version: 3.5.22
125
+ signing_key:
110
126
  specification_version: 4
111
127
  summary: Interactive TUI Directory Navigator for Terminal
112
128
  test_files: []