Class: Rfd::Item

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/rfd/item.rb

Constant Summary collapse

ICON_DIRECTORY =

Nerd Font icons (requires Nerd Font installed)

"\uf07c"
"\uf0c1"
ICON_EXEC =
"\uf013"
ICON_FILE =
"\uf15b"
ICON_RUBY =
"\ue791"
ICON_PYTHON =
"\ue73c"
ICON_JS =
"\ue74e"
ICON_TS =
"\ue628"
ICON_HTML =
"\ue736"
ICON_CSS =
"\ue749"
ICON_JSON =
"\ue60b"
ICON_MARKDOWN =
"\ue73e"
ICON_SHELL =
"\uf489"
ICON_C =
"\ue61e"
ICON_CPP =
"\ue61d"
ICON_GO =
"\ue626"
ICON_RUST =
"\ue7a8"
ICON_JAVA =
"\ue738"
ICON_PHP =
"\ue73d"
ICON_SWIFT =
"\ue755"
ICON_VIM =
"\ue62b"
ICON_LUA =
"\ue620"
ICON_SQL =
"\uf1c0"
ICON_PDF =
"\uf1c1"
ICON_ARCHIVE =
"\uf1c6"
ICON_IMAGE =
"\uf1c5"
ICON_AUDIO =
"\uf1c7"
ICON_VIDEO =
"\uf1c8"
ICON_FONT =
"\uf031"
ICON_LOCK =
"\uf023"
ICON_CONFIG =
"\ue615"
ICON_LOG =
"\uf18d"
FILE_ICONS =
{
  '.rb' => ICON_RUBY, '.rake' => ICON_RUBY, '.gemspec' => ICON_RUBY,
  '.py' => ICON_PYTHON, '.pyc' => ICON_PYTHON,
  '.js' => ICON_JS, '.mjs' => ICON_JS, '.cjs' => ICON_JS, '.jsx' => ICON_JS,
  '.ts' => ICON_TS, '.tsx' => ICON_TS,
  '.html' => ICON_HTML, '.htm' => ICON_HTML,
  '.css' => ICON_CSS, '.scss' => ICON_CSS, '.sass' => ICON_CSS,
  '.json' => ICON_JSON, '.yaml' => ICON_CONFIG, '.yml' => ICON_CONFIG,
  '.md' => ICON_MARKDOWN, '.markdown' => ICON_MARKDOWN,
  '.sh' => ICON_SHELL, '.bash' => ICON_SHELL, '.zsh' => ICON_SHELL,
  '.c' => ICON_C, '.h' => ICON_C,
  '.cpp' => ICON_CPP, '.hpp' => ICON_CPP, '.cc' => ICON_CPP,
  '.go' => ICON_GO,
  '.rs' => ICON_RUST,
  '.java' => ICON_JAVA, '.jar' => ICON_JAVA,
  '.php' => ICON_PHP,
  '.swift' => ICON_SWIFT,
  '.vim' => ICON_VIM,
  '.lua' => ICON_LUA,
  '.sql' => ICON_SQL,
  '.pdf' => ICON_PDF,
  '.zip' => ICON_ARCHIVE, '.tar' => ICON_ARCHIVE, '.gz' => ICON_ARCHIVE, '.rar' => ICON_ARCHIVE, '.7z' => ICON_ARCHIVE,
  '.png' => ICON_IMAGE, '.jpg' => ICON_IMAGE, '.jpeg' => ICON_IMAGE, '.gif' => ICON_IMAGE, '.svg' => ICON_IMAGE, '.webp' => ICON_IMAGE, '.ico' => ICON_IMAGE, '.heic' => ICON_IMAGE, '.heif' => ICON_IMAGE,
  '.mp3' => ICON_AUDIO, '.wav' => ICON_AUDIO, '.flac' => ICON_AUDIO, '.ogg' => ICON_AUDIO,
  '.mp4' => ICON_VIDEO, '.avi' => ICON_VIDEO, '.mov' => ICON_VIDEO, '.mkv' => ICON_VIDEO, '.webm' => ICON_VIDEO,
  '.ttf' => ICON_FONT, '.otf' => ICON_FONT, '.woff' => ICON_FONT,
  '.lock' => ICON_LOCK,
  '.env' => ICON_CONFIG,
  '.log' => ICON_LOG,
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path: nil, dir: nil, name: nil, stat: nil, window_width: nil) ⇒ Item

Returns a new instance of Item.



8
9
10
11
# File 'lib/rfd/item.rb', line 8

def initialize(path: nil, dir: nil, name: nil, stat: nil, window_width: nil)
  @path, @dir, @name, @stat, @window_width, @marked = path, dir || File.dirname(path), name || File.basename(path), stat, window_width, false
  @stat = File.lstat self.path unless stat
end

Instance Attribute Details

#dirObject (readonly)

Returns the value of attribute dir.



5
6
7
# File 'lib/rfd/item.rb', line 5

def dir
  @dir
end

#indexObject

Returns the value of attribute index.



6
7
8
# File 'lib/rfd/item.rb', line 6

def index
  @index
end

#nameObject (readonly)

Returns the value of attribute name.



5
6
7
# File 'lib/rfd/item.rb', line 5

def name
  @name
end

#statObject (readonly)

Returns the value of attribute stat.



5
6
7
# File 'lib/rfd/item.rb', line 5

def stat
  @stat
end

Instance Method Details

#<=>(o) ⇒ Object



361
362
363
364
365
366
367
368
369
# File 'lib/rfd/item.rb', line 361

def <=>(o)
  if directory? && !o.directory?
    1
  elsif !directory? && o.directory?
    -1
  else
    name <=> o.name
  end
end

#archive?Boolean

Returns:

  • (Boolean)


151
152
153
# File 'lib/rfd/item.rb', line 151

def archive?
  zip? || gz?
end

#atimeObject



71
72
73
# File 'lib/rfd/item.rb', line 71

def atime
  stat.atime.strftime('%Y-%m-%d %H:%M:%S')
end

#audio?Boolean

Returns:

  • (Boolean)


207
208
209
# File 'lib/rfd/item.rb', line 207

def audio?
  !directory? && %w[.mp3 .wav .flac .ogg .m4a .aac .aiff .wma].include?(extname.downcase)
end

#basenameObject



17
18
19
# File 'lib/rfd/item.rb', line 17

def basename
  @basename ||= File.basename name, extname
end

#colorObject



49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rfd/item.rb', line 49

def color
  if symlink?
    Curses::COLOR_MAGENTA
  elsif hidden?
    Curses::COLOR_GREEN
  elsif directory?
    Curses::COLOR_CYAN
  elsif executable?
    Curses::COLOR_RED
  else
    Curses::COLOR_WHITE
  end
end

#ctimeObject



75
76
77
# File 'lib/rfd/item.rb', line 75

def ctime
  stat.ctime.strftime('%Y-%m-%d %H:%M:%S')
end

#current_markObject



251
252
253
# File 'lib/rfd/item.rb', line 251

def current_mark
  marked? ? '*' : ' '
end

#directory?Boolean

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
111
112
113
# File 'lib/rfd/item.rb', line 103

def directory?
  @directory ||= if symlink?
    begin
      File.stat(path).directory?
    rescue Errno::ENOENT
      false
    end
  else
    stat.directory?
  end
end

#display_nameObject



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/rfd/item.rb', line 35

def display_name
  @display_name ||= begin
    n = full_display_name
    offset = ENV['RFD_NO_ICONS'] ? 15 : 18
    if mb_size(n) <= @window_width - offset
      n
    elsif symlink?
      mb_left n, @window_width - offset - 1
    else
      "#{mb_left(basename, @window_width - offset - 1 - extname.size)}…#{extname}"
    end
  end
end

#executable?Boolean

Returns:

  • (Boolean)


123
124
125
# File 'lib/rfd/item.rb', line 123

def executable?
  stat.executable?
end

#extnameObject



21
22
23
# File 'lib/rfd/item.rb', line 21

def extname
  @extname ||= File.extname name
end

#full_display_nameObject



29
30
31
32
33
# File 'lib/rfd/item.rb', line 29

def full_display_name
  n = @name.dup
  n << " -> #{target}" if symlink?
  n
end

#gz?Boolean

Returns:

  • (Boolean)


139
140
141
142
143
144
145
146
147
148
149
# File 'lib/rfd/item.rb', line 139

def gz?
  @gz_ ||= begin
    if directory?
      false
    else
      File.binread(realpath, 2).unpack('n').first == 0x1f8b
    end
  rescue
    false
  end
end

#heic?Boolean

Returns:

  • (Boolean)


170
171
172
173
174
175
176
177
178
179
# File 'lib/rfd/item.rb', line 170

def heic?
  @heic ||= begin
    return false if directory?
    return true if %w[.heic .heif].include?(extname.downcase)
    magic = File.binread(realpath, 12).bytes
    magic[4..7] == [0x66, 0x74, 0x79, 0x70]  # "ftyp" at offset 4
  rescue
    false
  end
end

#hidden?Boolean

Returns:

  • (Boolean)


119
120
121
# File 'lib/rfd/item.rb', line 119

def hidden?
  name.start_with?('.') && (name != '.') && (name != '..')
end

#iconObject



320
321
322
323
324
325
326
# File 'lib/rfd/item.rb', line 320

def icon
  return '' if ENV['RFD_NO_ICONS']
  return ICON_DIRECTORY if directory?
  return ICON_SYMLINK if symlink?
  return ICON_EXEC if executable?
  FILE_ICONS[extname.downcase] || ICON_FILE
end

#image?Boolean

Returns:

  • (Boolean)


155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/rfd/item.rb', line 155

def image?
  @image ||= begin
    return false if directory?
    return true if svg?
    return true if heic?
    magic = File.binread(realpath, 12).bytes
    (magic[0..3] == [0x89, 0x50, 0x4E, 0x47]) ||  # PNG
      (magic[0..2] == [0xFF, 0xD8, 0xFF]) ||      # JPEG
      (magic[0..2] == [0x47, 0x49, 0x46]) ||      # GIF
      (magic[0..3] == [0x52, 0x49, 0x46, 0x46] && magic[8..11] == [0x57, 0x45, 0x42, 0x50])  # WebP (RIFF....WEBP)
  rescue
    false
  end
end

#join(*ary) ⇒ Object



25
26
27
# File 'lib/rfd/item.rb', line 25

def join(*ary)
  File.join path, ary
end

#markdown?Boolean

Returns:

  • (Boolean)


203
204
205
# File 'lib/rfd/item.rb', line 203

def markdown?
  !directory? && %w[.md .markdown].include?(extname.downcase)
end

#marked?Boolean

Returns:

  • (Boolean)


247
248
249
# File 'lib/rfd/item.rb', line 247

def marked?
  @marked
end

#mb_char_size(c) ⇒ Object



337
338
339
# File 'lib/rfd/item.rb', line 337

def mb_char_size(c)
  c == '…' ? 1 : c.bytesize == 1 ? 1 : 2
end

#mb_left(str, size) ⇒ Object



328
329
330
331
332
333
334
335
# File 'lib/rfd/item.rb', line 328

def mb_left(str, size)
  len = 0
  index = str.each_char.with_index do |c, i|
    break i if len + mb_char_size(c) > size
    len += mb_size c
  end
  str[0, index]
end

#mb_ljust(str, size) ⇒ Object



345
346
347
# File 'lib/rfd/item.rb', line 345

def mb_ljust(str, size)
  "#{str}#{' ' * [0, size - mb_size(str)].max}"
end

#mb_size(str) ⇒ Object



341
342
343
# File 'lib/rfd/item.rb', line 341

def mb_size(str)
  str.each_char.inject(0) {|l, c| l += mb_char_size(c)}
end

#modeObject



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rfd/item.rb', line 83

def mode
  @mode ||= begin
    m = stat.mode
    ft = directory? ? 'd' : symlink? ? 'l' : '-'
    owner = (m >> 6) & 07
    group = (m >> 3) & 07
    other = m & 07
    ret = [owner, group, other].inject(ft) do |str, perms|
      str + "#{perms & 4 != 0 ? 'r' : '-'}#{perms & 2 != 0 ? 'w' : '-'}#{perms & 1 != 0 ? 'x' : '-'}"
    end
    # setuid: 's' if execute set, 'S' if not
    ret[3] = (owner & 1 != 0 ? 's' : 'S') if m & 04000 != 0
    # setgid: 's' if execute set, 'S' if not
    ret[6] = (group & 1 != 0 ? 's' : 'S') if m & 02000 != 0
    # sticky: 't' if execute set, 'T' if not
    ret[9] = (other & 1 != 0 ? 't' : 'T') if m & 01000 != 0
    ret
  end
end

#mtimeObject



79
80
81
# File 'lib/rfd/item.rb', line 79

def mtime
  stat.mtime.strftime('%Y-%m-%d %H:%M:%S')
end

#pathObject



13
14
15
# File 'lib/rfd/item.rb', line 13

def path
  @path ||= File.join @dir, @name
end

#pdf?Boolean

Returns:

  • (Boolean)


193
194
195
196
197
198
199
200
201
# File 'lib/rfd/item.rb', line 193

def pdf?
  @pdf ||= begin
    return false if directory?
    magic = File.binread(realpath, 4).bytes
    magic == [0x25, 0x50, 0x44, 0x46]  # %PDF
  rescue
    false
  end
end

#preview_typeObject

Returns the preview type symbol for the preview server

Order matters here:

  • video? must come before heic? because MOV/MP4 files share the same “ftyp” magic bytes at offset 4 that heic? checks for

  • heic? must come before image? because HEIC conversion is slow and needs async handling, while other image formats can be displayed directly



222
223
224
225
226
227
228
229
230
# File 'lib/rfd/item.rb', line 222

def preview_type
  return :directory if directory?
  return :video if video?
  return :heic if heic?
  return :image if image?
  return :pdf if pdf?
  return :markdown if markdown?
  :text
end

#realpathObject



236
237
238
# File 'lib/rfd/item.rb', line 236

def realpath
  @realpath ||= File.realpath path
end

#sizeObject



63
64
65
# File 'lib/rfd/item.rb', line 63

def size
  directory? ? 0 : stat.size
end

#size_or_dirObject



67
68
69
# File 'lib/rfd/item.rb', line 67

def size_or_dir
  directory? ? '<DIR>' : size.to_s
end

#svg?Boolean

Returns:

  • (Boolean)


181
182
183
184
185
186
187
188
189
190
191
# File 'lib/rfd/item.rb', line 181

def svg?
  @svg ||= begin
    return false if directory?
    return true if extname.downcase == '.svg'
    # Check content for <svg tag
    content = File.binread(realpath, 256)
    content.include?('<svg')
  rescue
    false
  end
end

#symlink?Boolean

Returns:

  • (Boolean)


115
116
117
# File 'lib/rfd/item.rb', line 115

def symlink?
  stat.symlink?
end

#targetObject



232
233
234
# File 'lib/rfd/item.rb', line 232

def target
  File.readlink path if symlink?
end

#to_sObject



349
350
351
352
353
354
355
# File 'lib/rfd/item.rb', line 349

def to_s
  if ENV['RFD_NO_ICONS']
    "#{current_mark}#{mb_ljust(display_name, @window_width - 15)}#{size_or_dir.rjust(13)}"
  else
    "#{current_mark}#{icon} #{mb_ljust(display_name, @window_width - 18)}#{size_or_dir.rjust(13)}"
  end
end

#to_strObject



357
358
359
# File 'lib/rfd/item.rb', line 357

def to_str
  path
end

#toggle_markObject



240
241
242
243
244
245
# File 'lib/rfd/item.rb', line 240

def toggle_mark
  unless %w(. ..).include? name
    @marked = !@marked
    true
  end
end

#video?Boolean

Returns:

  • (Boolean)


211
212
213
# File 'lib/rfd/item.rb', line 211

def video?
  !directory? && %w[.mp4 .avi .mov .mkv .webm .flv .wmv .m4v .mpeg .mpg].include?(extname.downcase)
end

#zip?Boolean

Returns:

  • (Boolean)


127
128
129
130
131
132
133
134
135
136
137
# File 'lib/rfd/item.rb', line 127

def zip?
  @zip_ ||= begin
    if directory?
      false
    else
      File.binread(realpath, 4).unpack('V').first == 0x04034b50
    end
  rescue
    false
  end
end