colorls/lib/colorls/core.rb
2024-09-05 22:47:17 +02:00

488 lines
15 KiB
Ruby

# frozen_string_literal: true
module ColorLS
# on Windows (were the special 'nul' device exists) we need to use UTF-8
@file_encoding = File.exist?('nul') ? Encoding::UTF_8 : Encoding::ASCII_8BIT
def self.file_encoding
@file_encoding
end
def self.terminal_width
console = IO.console
width = IO.console_size[1]
return width if console.nil? || console.winsize[1].zero?
console.winsize[1]
end
@screen_width = terminal_width
def self.screen_width
@screen_width
end
class Core # rubocop:disable Metrics/ClassLength
MIN_SIZE_CHARS = 4
def initialize(all: false, sort: false, show: false,
mode: nil, show_git: false, almost_all: false, colors: [], group: nil,
reverse: false, hyperlink: false, tree_depth: nil, show_inode: false,
indicator_style: 'slash', long_style_options: {}, icons: true)
@count = {folders: 0, recognized_files: 0, unrecognized_files: 0}
@all = all
@almost_all = almost_all
@hyperlink = hyperlink
@sort = sort
@reverse = reverse
@group = group
@show = show
@one_per_line = mode == :one_per_line
@show_inode = show_inode
init_long_format(mode,long_style_options)
@tree = {mode: mode == :tree, depth: tree_depth}
@horizontal = mode == :horizontal
@git_status = init_git_status(show_git)
@time_style = long_style_options.key?(:time_style) ? long_style_options[:time_style] : ''
@indicator_style = indicator_style
@hard_links_count = long_style_options.key?(:hard_links_count) ? long_style_options[:hard_links_count] : true
@icons = icons
init_colors colors
init_icons
end
def additional_chars_per_item
12 + (@show_git ? 4 : 0) + (@show_inode ? 10 : 0)
end
def ls_dir(info)
if @tree[:mode]
print "\n"
return tree_traverse(info.path, 0, 1, 2)
end
@contents = Dir.entries(info.path, encoding: ColorLS.file_encoding)
filter_hidden_contents
@contents.map! { |e| FileInfo.dir_entry(info.path, e, link_info: @long) }
filter_contents if @show
sort_contents if @sort
group_contents if @group
return print "\n Nothing to show here\n".colorize(@colors[:empty]) if @contents.empty?
ls
end
def ls_files(files)
@contents = files
ls
end
def display_report(report_mode)
if report_mode == :short
puts <<~REPORT
\s\s\s\sFolders: #{@count[:folders]}, Files: #{@count[:recognized_files] + @count[:unrecognized_files]}.
REPORT
.colorize(@colors[:report])
else
puts <<~REPORT
Found #{@count.values.sum} items in total.
\tFolders\t\t\t: #{@count[:folders]}
\tRecognized files\t: #{@count[:recognized_files]}
\tUnrecognized files\t: #{@count[:unrecognized_files]}
REPORT
.colorize(@colors[:report])
end
end
private
def ls
init_column_lengths
layout = case
when @horizontal
HorizontalLayout.new(@contents, item_widths, ColorLS.screen_width)
when @one_per_line || @long
SingleColumnLayout.new(@contents)
else
VerticalLayout.new(@contents, item_widths, ColorLS.screen_width)
end
layout.each_line do |line, widths|
ls_line(line, widths)
end
clear_chars_for_size
end
def init_colors(colors)
@colors = colors
@modes = Hash.new do |hash, key|
color = case key
when 'r' then :read
when 'w' then :write
when '-' then :no_access
when 'x', 's', 'S', 't', 'T' then :exec
end
hash[key] = key.colorize(@colors[color]).freeze
end
end
def init_long_format(mode, long_style_options)
@long = mode == :long
@show_group = long_style_options.key?(:show_group) ? long_style_options[:show_group] : true
@show_user = long_style_options.key?(:show_user) ? long_style_options[:show_user] : true
@show_symbol_dest = long_style_options.key?(:show_symbol_dest) ? long_style_options[:show_symbol_dest] : false
@show_human_readable_size =
long_style_options.key?(:human_readable_size) ? long_style_options[:human_readable_size] : true
end
def init_git_status(show_git)
@show_git = show_git
return {}.freeze unless show_git
# stores git status information per directory
Hash.new do |hash, key|
path = File.absolute_path key.parent
if hash.key? path
hash[path]
else
hash[path] = Git.status(path)
end
end
end
def item_widths
@contents.map { |item| Unicode::DisplayWidth.of(item.show) + additional_chars_per_item }
end
def filter_hidden_contents
@contents -= %w[. ..] unless @all
@contents.keep_if { |x| !x.start_with? '.' } unless @all || @almost_all
end
def init_column_lengths
return unless @long
maxlink = maxuser = maxgroup = 0
@contents.each do |c|
maxlink = c.nlink if c.nlink > maxlink
maxuser = c.owner.length if c.owner.length > maxuser
maxgroup = c.group.length if c.group.length > maxgroup
end
@linklength = maxlink.digits.length
@userlength = maxuser
@grouplength = maxgroup
end
def filter_contents
@contents.keep_if do |x|
x.directory? == (@show == :dirs)
end
end
def sort_contents
case @sort
when :extension
@contents.sort_by! do |f|
name = f.name
ext = File.extname(name)
name = name.chomp(ext) unless ext.empty?
[ext, name].map { |s| CLocale.strxfrm(s) }
end
when :time
@contents.sort_by! { |a| -a.mtime.to_f }
when :size
@contents.sort_by! { |a| -a.size }
else
@contents.sort_by! { |a| CLocale.strxfrm(a.name) }
end
@contents.reverse! if @reverse
end
def group_contents
return unless @group
dirs, files = @contents.partition(&:directory?)
@contents = case @group
when :dirs then dirs.push(*files)
when :files then files.push(*dirs)
end
end
def init_icons
@files = ColorLS::Yaml.new('files.yaml').load
@file_aliases = ColorLS::Yaml.new('file_aliases.yaml').load(aliase: true)
@folders = ColorLS::Yaml.new('folders.yaml').load
@folder_aliases = ColorLS::Yaml.new('folder_aliases.yaml').load(aliase: true)
end
def format_mode(rwx, special, char)
m_r = (rwx & 4).zero? ? '-' : 'r'
m_w = (rwx & 2).zero? ? '-' : 'w'
m_x = if special
(rwx & 1).zero? ? char.upcase : char
else
(rwx & 1).zero? ? '-' : 'x'
end
@modes[m_r] + @modes[m_w] + @modes[m_x]
end
def mode_info(stat)
m = stat.mode
format_mode(m >> 6, stat.setuid?, 's') +
format_mode(m >> 3, stat.setgid?, 's') +
format_mode(m, stat.sticky?, 't')
end
def user_info(content)
content.owner.ljust(@userlength, ' ').colorize(@colors[:user])
end
def group_info(group)
group.to_s.ljust(@grouplength, ' ').colorize(@colors[:normal])
end
def size_info(filesize)
filesize = Filesize.new(filesize)
size = @show_human_readable_size ? filesize.pretty(precision: 0) : filesize.to_s('B', precision: 0)
size = size.split
size = justify_size_info(size)
return size.colorize(@colors[:file_large]) if filesize >= 512 * (1024 ** 2)
return size.colorize(@colors[:file_medium]) if filesize >= 128 * (1024 ** 2)
size.colorize(@colors[:file_small])
end
def chars_for_size
@chars_for_size ||= if @show_human_readable_size
MIN_SIZE_CHARS
else
max_size = @contents.max_by(&:size).size
reqd_chars = max_size.to_s.length
[reqd_chars, MIN_SIZE_CHARS].max
end
end
def justify_size_info(size)
size_num = size[0].rjust(chars_for_size, ' ')
size_unit = @show_human_readable_size ? size[1].ljust(3, ' ') : size[1]
"#{size_num} #{size_unit}"
end
def clear_chars_for_size
@chars_for_size = nil
end
def mtime_info(file_mtime)
mtime = @time_style.start_with?('+') ? file_mtime.strftime(@time_style.delete_prefix('+')) : file_mtime.asctime
now = Time.now
return mtime.colorize(@colors[:hour_old]) if now - file_mtime < 60 * 60
return mtime.colorize(@colors[:day_old]) if now - file_mtime < 24 * 60 * 60
mtime.colorize(@colors[:no_modifier])
end
def git_info(content)
return '' unless (status = @git_status[content])
if content.directory?
git_dir_info(content, status)
else
git_file_info(status[content.name])
end
end
def git_file_info(status)
return Git.colored_status_symbols(status, @colors) if status
' ✓ '
.encode(Encoding.default_external, undef: :replace, replace: '=')
.colorize(@colors[:unchanged])
end
def git_dir_info(content, status)
modes = if content.path == '.'
Set.new(status.values).flatten
else
status[content.name]
end
if modes.empty? && Dir.empty?(content.path)
' '
else
Git.colored_status_symbols(modes, @colors)
end
end
def inode(content)
return '' unless @show_inode
content.stats.ino.to_s.rjust(10).colorize(@colors[:inode])
end
def long_info(content)
return '' unless @long
links = content.nlink.to_s.rjust(@linklength)
line_array = [mode_info(content.stats)]
line_array.push links if @hard_links_count
line_array.push user_info(content) if @show_user
line_array.push group_info(content.group) if @show_group
line_array.push(size_info(content.size), mtime_info(content.mtime))
line_array.join(' ')
end
def symlink_info(content)
return '' unless @long && content.symlink?
target = content.link_target.nil? ? '…' : content.link_target
link_info = "#{target}"
if content.dead?
"#{link_info} [Dead link]".colorize(@colors[:dead_link])
else
link_info.colorize(@colors[:link])
end
end
def update_content_if_show_symbol_dest(content, show_symbol_dest_flag)
return content unless show_symbol_dest_flag
return content unless content.symlink?
return content if content.link_target.nil?
return content if content.dead?
FileInfo.info(content.link_target)
end
def out_encode(str)
str.encode(Encoding.default_external, undef: :replace, replace: '')
end
def fetch_string(content, key, color, increment)
@count[increment] += 1
value = increment == :folders ? @folders[key] : @files[key]
logo = value.gsub(/\\u[\da-f]{4}/i) { |m| [m[-4..].to_i(16)].pack('U') }
name = @hyperlink ? make_link(content) : content.show
name += content.directory? && @indicator_style != 'none' ? '/' : ' '
entry = @icons ? "#{out_encode(logo)} #{out_encode(name)}" : out_encode(name).to_s
entry = entry.bright if !content.directory? && content.executable?
symlink_info_string = symlink_info(content)
content = update_content_if_show_symbol_dest(content,@show_symbol_dest)
"#{inode(content)} #{long_info(content)} #{git_info(content)} #{entry.colorize(color)}#{symlink_info_string}"
end
def ls_line(chunk, widths)
padding = 0
line = +''
chunk.each_with_index do |content, i|
entry = fetch_string(content, *options(content))
line << (' ' * padding)
line << ' ' << entry.encode(Encoding.default_external, undef: :replace)
padding = widths[i] - Unicode::DisplayWidth.of(content.show) - additional_chars_per_item
end
print line << "\n"
end
def file_color(file, key)
color_key = case
when file.chardev? then :chardev
when file.blockdev? then :blockdev
when file.socket? then :socket
when file.executable? then :executable_file
when file.hidden? then :hidden
when @files.key?(key) then :recognized_file
else :unrecognized_file
end
@colors[color_key]
end
def options(content)
if content.directory?
options_directory(content).values_at(:key, :color, :group)
else
options_file(content).values_at(:key, :color, :group)
end
end
def options_directory(content)
key = content.name.downcase.to_sym
key = @folder_aliases[key] unless @folders.key?(key)
key = :folder if key.nil?
color = content.hidden? ? @colors[:hidden_dir] : @colors[:dir]
{key: key, color: color, group: :folders}
end
def options_file(content)
key = File.extname(content.name).delete_prefix('.').downcase.to_sym
key = @file_aliases[key] unless @files.key?(key)
color = file_color(content, key)
group = @files.key?(key) ? :recognized_files : :unrecognized_files
key = :file if key.nil?
{key: key, color: color, group: group}
end
def tree_contents(path)
@contents = Dir.entries(path, encoding: ColorLS.file_encoding)
filter_hidden_contents
@contents.map! { |e| FileInfo.dir_entry(path, e, link_info: @long) }
filter_contents if @show
sort_contents if @sort
group_contents if @group
@contents
end
def tree_traverse(path, prespace, depth, indent)
contents = tree_contents(path)
contents.each do |content|
icon = content == contents.last || content.directory? ? ' └──' : ' ├──'
print tree_branch_preprint(prespace, indent, icon).colorize(@colors[:tree])
print " #{fetch_string(content, *options(content))} \n"
next unless content.directory?
tree_traverse("#{path}/#{content}", prespace + indent, depth + 1, indent) if keep_going(depth)
end
end
def keep_going(depth)
@tree[:depth].nil? || depth < @tree[:depth]
end
def tree_branch_preprint(prespace, indent, prespace_icon)
return prespace_icon if prespace.zero?
(' │ ' * (prespace/indent)) + prespace_icon + ('─' * indent)
end
def make_link(content)
uri = Addressable::URI.convert_path(File.absolute_path(content.path))
"\033]8;;#{uri}\007#{content.show}\033]8;;\007"
end
end
end