mirror of
https://github.com/athityakumar/colorls.git
synced 2024-09-19 05:31:23 +02:00
488 lines
15 KiB
Ruby
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
|