Backspace sends DEL instead of ^H.
[spft.git] / els
blob5d9f566a4132b916e5e1d74c3b68a53ac668ae99
1 #!/usr/bin/env python3
3 # This is a wrapper around "ls" that results in tabbed output that's suitable
4 # for elastic tabs.
6 import subprocess, sys, os, re, shutil
8 def display_length(string):
9         num_displayed_chars = 0
10         in_escape_sequence = False
11         for c in string:
12                 if in_escape_sequence:
13                         if ord(c) >= 0x40 and ord(c) <= 0x7E and c != '[':
14                                 in_escape_sequence = False
15                 elif c == '\x1B':
16                         in_escape_sequence = True
17                 else:
18                         num_displayed_chars += 1
19         return num_displayed_chars
21 def decode(raw_bytes):
22         try:
23                 return str(raw_bytes, encoding = "utf-8")
24         except:
25                 return str(raw_bytes, encoding = "latin1")
28 # Columnation, following how "ls" does it.
30 min_column_width = 3
31 column_separator_width_estimate = 5
33 class ColumnInfo:
34         def __init__(self, index):
35                 self.valid_length = True
36                 self.line_length = (index + 1) * min_column_width
37                 self.col_arr = [ min_column_width ] * (index + 1)
39 def columnate(filenames):
40         # Set up for columnation.
41         line_length = shutil.get_terminal_size().columns
42         max_possible_columns = int(line_length / min_column_width)
43         if (line_length % min_column_width) != 0:
44                 max_possible_columns += 1
45         max_cols = min(max_possible_columns, len(filenames))
46         column_info = []
47         for index in range(max_cols):
48                 column_info.append(ColumnInfo(index))
49         
50         # Compute the maximum number of possible columns.
51         # "Compute" here means "try all the possible numbers of columns".
52         num_files = len(filenames)
53         if num_files == 0:
54                 return
55         for which_file in range(num_files):
56                 filename = filenames[which_file]
57                 name_length = display_length(filename)
58                 for cur_num_columns in range(1, max_cols + 1):
59                         cur_column_info = column_info[cur_num_columns - 1]
60                         if cur_column_info.valid_length:
61                                 which_column = int(which_file / ((num_files + cur_num_columns -1) / cur_num_columns))
62                                 real_length = name_length
63                                 if which_column != cur_num_columns - 1:
64                                         real_length += column_separator_width_estimate
65                                 if cur_column_info.col_arr[which_column] < real_length:
66                                         cur_column_info.line_length += real_length - cur_column_info.col_arr[which_column]
67                                         cur_column_info.col_arr[which_column] = real_length
68                                         cur_column_info.valid_length = cur_column_info.line_length < line_length
69         # Find the largest number of columns that fit.
70         num_columns = max_cols
71         while num_columns > 1:
72                 if column_info[num_columns - 1].valid_length:
73                         break
74                 num_columns -= 1
75         
76         # Now output the filenames.
77         num_rows = int(num_files / num_columns)
78         if num_files % num_columns != 0:
79                 num_rows += 1
80         print("\x1B[?5001h", end = '')
81         for which_row in range(num_rows):
82                 which_column = 0
83                 which_file = which_row
84                 while True:
85                         print(filenames[which_file], end = '')
86                         which_file += num_rows
87                         if which_file >= num_files:
88                                 break
89                         print('\t', end = '')
90                 print()
91         print("\x1B[?5001l", end = '')
94 #######
96 ls_args = []
97 long_output = False
98 first_line_summary = False
99 for arg in sys.argv[1:]:
100         if arg[0:2] != "--" and arg[0] == "-":
101                 if "l" in arg:
102                         long_output = True
103                         first_line_summary = True
104                 if "s" in arg:
105                         first_line_summary = True
106         ls_args.append(arg)
108 # Find "ls" in the path.
109 ls_binary = ""
110 for path in os.environ["PATH"].split(":"):
111         file_path = path + "/ls"
112         if os.path.isfile(file_path) and os.access(file_path, os.X_OK):
113                 ls_binary = file_path
114                 break
115 if ls_binary == "":
116         printf(f"Couldn't find \"ls\"!", file = sys.stderr)
117         sys.exit(1)
119 process = subprocess.Popen([ls_binary] + ls_args + ["-1"], stdout = subprocess.PIPE)
120 result = process.communicate()
122 if long_output:
123         long_re = re.compile('([\w-]+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\S+)\s+(\w+\s+\d+\s+[\d:]+)\s+([^\r\n]+)')
124         try:
125                 filenames = result[0].splitlines()
126                 if first_line_summary and len(filenames) > 0:
127                         print(decode(filenames[0]))
128                         filenames = filenames[1:]
129                 print("\x1B[?5001h", end = '')
130                 for line in filenames:
131                         line = decode(line)
132                         match = long_re.match(line)
133                         if match:
134                                 print('\t'.join(match.groups()))
135                         else:
136                                 print(line.strip())
137                 print("\x1B[?5001l", end = '')
138         except BrokenPipeError:
139                 # We can't do anything about BrokenPipeError; just be quiet about it.
140                 pass
141 else:
142         filenames = result[0].splitlines()
143         if first_line_summary and len(filenames) > 0:
144                 print(decode(filenames[0]))
145                 filenames = filenames[1:]
146         cur_filenames = []
147         for line in filenames:
148                 line = decode(line)
149                 if len(line) == 0 or line[-1] == ":":
150                         columnate(cur_filenames)
151                         cur_filenames = []
152                         print(line)
153                 else:
154                         cur_filenames.append(line)
155         try:
156                 columnate(cur_filenames)
157         except BrokenPipeError:
158                 # We can't do anything about BrokenPipeError; just be quiet about it.
159                 pass
161 sys.exit(process.returncode)