this repo has no description
56
fork

Configure Feed

Select the types of activity you want to include in your feed.

fmt: add column view

+225 -67
+225 -67
src/main.zig
··· 3 3 const ourio = @import("ourio"); 4 4 const zeit = @import("zeit"); 5 5 6 + const assert = std.debug.assert; 6 7 const posix = std.posix; 7 8 8 9 const usage = 9 10 \\Usage: 10 11 \\ els [options] [directory] 11 12 \\ 12 - \\ --help Print this message and exit 13 + \\ --help Print this message and exit 13 14 \\ 14 15 \\DISPLAY OPTIONS 15 - \\ -a, --all Show files that start with a dot (ASCII 0x2E) 16 - \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories 17 - \\ --color=WHEN When to use colors (always, auto, never) 18 - \\ --icons=WHEN When to display icons (always, auto, never) 19 - \\ -l, --long Display extended file metadata 16 + \\ -1, --oneline Print entries one per line 17 + \\ -a, --all Show files that start with a dot (ASCII 0x2E) 18 + \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories 19 + \\ -C, --columns Print the output in columns 20 + \\ --color=WHEN When to use colors (always, auto, never) 21 + \\ --icons=WHEN When to display icons (always, auto, never) 22 + \\ -l, --long Display extended file metadata 20 23 ; 21 24 22 25 const Options = struct { 23 26 all: bool = false, 24 27 @"almost-all": bool = false, 25 28 color: When = .auto, 29 + shortview: enum { columns, oneline } = .oneline, 26 30 @"group-directories-first": bool = true, 27 31 icons: When = .auto, 28 32 long: bool = false, 29 33 30 34 directory: [:0]const u8 = ".", 31 35 32 - isatty: bool = false, 36 + winsize: ?posix.winsize = null, 33 37 colors: Colors = .none, 34 38 35 39 const When = enum { ··· 80 84 switch (self.color) { 81 85 .never => return false, 82 86 .always => return true, 83 - .auto => return self.isatty, 87 + .auto => return self.isatty(), 84 88 } 85 89 } 86 90 ··· 88 92 switch (self.icons) { 89 93 .never => return false, 90 94 .always => return true, 91 - .auto => return self.isatty, 95 + .auto => return self.isatty(), 92 96 } 97 + } 98 + 99 + fn isatty(self: Options) bool { 100 + return self.winsize != null; 93 101 } 94 102 }; 95 103 ··· 113 121 114 122 var cmd: Command = .{ .arena = allocator }; 115 123 124 + cmd.opts.winsize = getWinsize(std.io.getStdOut().handle); 125 + 126 + if (cmd.opts.isatty()) { 127 + cmd.opts.shortview = .columns; 128 + } 129 + 130 + const stdout = std.io.getStdOut().writer(); 131 + const stderr = std.io.getStdErr().writer(); 132 + var bw = std.io.bufferedWriter(stdout); 133 + 116 134 var args = std.process.args(); 117 135 // skip binary 118 136 _ = args.next(); ··· 122 140 const str = arg[1..]; 123 141 for (str) |b| { 124 142 switch (b) { 143 + '1' => cmd.opts.shortview = .oneline, 125 144 'A' => cmd.opts.@"almost-all" = true, 145 + 'C' => cmd.opts.shortview = .columns, 126 146 'a' => cmd.opts.all = true, 127 147 'l' => cmd.opts.long = true, 128 148 else => { 129 - const w = std.io.getStdErr().writer(); 130 - try w.print("Invalid opt: '{c}'", .{b}); 149 + try stderr.print("Invalid opt: '{c}'", .{b}); 131 150 std.process.exit(1); 132 151 }, 133 152 } ··· 138 157 const opt = split.first(); 139 158 const val = split.rest(); 140 159 if (eql(opt, "all")) { 141 - if (val.len == 0 or eql(val, "true")) 142 - cmd.opts.all = true 143 - else if (eql(val, "false")) 144 - cmd.opts.all = false; 160 + cmd.opts.all = parseArgBool(val) orelse { 161 + try stderr.print("Invalid boolean: '{s}'", .{val}); 162 + std.process.exit(1); 163 + }; 145 164 } else if (eql(opt, "long")) { 146 - if (val.len == 0 or eql(val, "true")) 147 - cmd.opts.long = true 148 - else if (eql(val, "false")) 149 - cmd.opts.long = false; 165 + cmd.opts.long = parseArgBool(val) orelse { 166 + try stderr.print("Invalid boolean: '{s}'", .{val}); 167 + std.process.exit(1); 168 + }; 150 169 } else if (eql(opt, "almost-all")) { 151 - if (val.len == 0 or eql(val, "true")) 152 - cmd.opts.@"almost-all" = true 153 - else if (eql(val, "false")) 154 - cmd.opts.@"almost-all" = false; 170 + cmd.opts.@"almost-all" = parseArgBool(val) orelse { 171 + try stderr.print("Invalid boolean: '{s}'", .{val}); 172 + std.process.exit(1); 173 + }; 155 174 } else if (eql(opt, "group-directories-first")) { 156 - if (val.len == 0 or eql(val, "true")) 157 - cmd.opts.@"group-directories-first" = true 158 - else if (eql(val, "false")) 159 - cmd.opts.@"group-directories-first" = false; 175 + cmd.opts.@"group-directories-first" = parseArgBool(val) orelse { 176 + try stderr.print("Invalid boolean: '{s}'", .{val}); 177 + std.process.exit(1); 178 + }; 160 179 } else if (eql(opt, "color")) { 161 180 cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse { 162 - const w = std.io.getStdErr().writer(); 163 - try w.print("Invalid color option: '{s}'", .{val}); 181 + try stderr.print("Invalid color option: '{s}'", .{val}); 164 182 std.process.exit(1); 165 183 }; 166 184 } else if (eql(opt, "icons")) { 167 185 cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { 168 - const w = std.io.getStdErr().writer(); 169 - try w.print("Invalid color option: '{s}'", .{val}); 186 + try stderr.print("Invalid color option: '{s}'", .{val}); 187 + std.process.exit(1); 188 + }; 189 + } else if (eql(opt, "columns")) { 190 + const c = parseArgBool(val) orelse { 191 + try stderr.print("Invalid columns option: '{s}'", .{val}); 192 + std.process.exit(1); 193 + }; 194 + cmd.opts.shortview = if (c) .columns else .oneline; 195 + } else if (eql(opt, "oneline")) { 196 + const o = parseArgBool(val) orelse { 197 + try stderr.print("Invalid oneline option: '{s}'", .{val}); 170 198 std.process.exit(1); 171 199 }; 200 + cmd.opts.shortview = if (o) .oneline else .columns; 172 201 } else if (eql(opt, "help")) { 173 - return std.io.getStdErr().writeAll(usage); 202 + return stderr.writeAll(usage); 174 203 } else { 175 - const w = std.io.getStdErr().writer(); 176 - try w.print("Invalid opt: '{s}'", .{opt}); 204 + try stderr.print("Invalid opt: '{s}'", .{opt}); 177 205 std.process.exit(1); 178 206 } 179 207 }, ··· 181 209 cmd.opts.directory = arg; 182 210 }, 183 211 } 212 + } 213 + 214 + if (cmd.opts.useColor()) { 215 + cmd.opts.colors = .default; 184 216 } 185 217 186 218 var ring: ourio.Ring = try .init(allocator, 256); ··· 210 242 }); 211 243 } 212 244 213 - if (cmd.opts.color == .auto) { 214 - cmd.opts.isatty = posix.isatty(std.io.getStdOut().handle); 215 - } 216 - 217 - if (cmd.opts.useColor()) { 218 - cmd.opts.colors = .default; 219 - } 220 - 221 245 try ring.run(.until_done); 222 246 223 247 std.sort.insertion(Entry, cmd.entries, cmd.opts, Entry.lessThan); 224 248 225 - const stdout = std.io.getStdOut(); 226 - var bw = std.io.bufferedWriter(stdout.writer()); 249 + if (cmd.entries.len == 0) return; 250 + 227 251 if (cmd.opts.long) { 228 252 try printLong(cmd, bw.writer()); 253 + } else if (!cmd.opts.isatty()) { 254 + try printShortOnePerLine(cmd, bw.writer()); 229 255 } else { 230 - try printShort(cmd, bw.writer()); 256 + switch (cmd.opts.shortview) { 257 + .columns => try printShortColumns(cmd, bw.writer()), 258 + .oneline => try printShortOnePerLine(cmd, bw.writer()), 259 + } 231 260 } 232 261 try bw.flush(); 233 262 } 234 263 235 - fn printShort(cmd: Command, writer: anytype) !void { 236 - const colors = cmd.opts.colors; 237 - for (cmd.entries) |entry| { 238 - if (cmd.opts.useIcons()) { 239 - const icon = Icon.get(entry, cmd.opts); 264 + fn printShortColumns(cmd: Command, writer: anytype) !void { 265 + std.log.debug("here 1", .{}); 266 + const ws = cmd.opts.winsize orelse return printShortOnePerLine(cmd, writer); 267 + std.log.debug("here 2", .{}); 268 + if (ws.col == 0) return printShortOnePerLine(cmd, writer); 269 + 270 + std.log.debug("here 3", .{}); 271 + 272 + const icon_width: u2 = if (cmd.opts.useIcons()) 2 else 0; 273 + 274 + var n_cols = @min(ws.col, cmd.entries.len); 275 + 276 + const Column = struct { 277 + width: usize = 0, 278 + entries: []const Entry = &.{}, 279 + }; 280 + 281 + var columns: std.ArrayListUnmanaged(Column) = try .initCapacity(cmd.arena, n_cols); 282 + 283 + outer: while (n_cols > 0) { 284 + std.log.debug("n_cols = {d}", .{n_cols}); 285 + columns.clearRetainingCapacity(); 286 + const n_rows = std.math.divCeil(usize, cmd.entries.len, n_cols) catch unreachable; 287 + const padding = (n_cols - 1) * 2; 288 + 289 + // The number of columns that are short by one entry 290 + const short_cols = n_cols * n_rows - cmd.entries.len; 291 + 292 + var idx: usize = 0; 293 + var line_width: usize = padding + icon_width * n_cols; 294 + 295 + if (line_width > ws.col) { 296 + n_cols -= 1; 297 + continue :outer; 298 + } 240 299 241 - if (cmd.opts.useColor()) { 242 - try writer.writeAll(icon.color); 243 - try writer.writeAll(icon.icon); 244 - try writer.writeAll(colors.reset); 245 - } else { 246 - try writer.writeAll(icon.icon); 300 + for (0..n_cols) |i| { 301 + std.log.debug("items={d}, idx={d}", .{ cmd.entries.len, idx }); 302 + std.log.debug("i={d}, n_cols={d}, short_cols={d}, is_short={}", .{ i, n_cols, short_cols, isShortColumn(i, n_cols, short_cols) }); 303 + const col_entries = if (isShortColumn(i, n_cols, short_cols)) n_rows - 1 else n_rows; 304 + const entries = cmd.entries[idx .. idx + col_entries]; 305 + idx += col_entries; 306 + 307 + var max_width: usize = 0; 308 + for (entries) |entry| { 309 + max_width = @max(max_width, entry.name.len); 247 310 } 248 311 249 - try writer.writeByte(' '); 312 + // line_width already includes all icons and padding 313 + line_width += max_width; 314 + 315 + const col_width = max_width + icon_width + 2; 316 + 317 + columns.appendAssumeCapacity(.{ 318 + .entries = entries, 319 + .width = col_width, 320 + }); 321 + 322 + if (line_width > ws.col) { 323 + n_cols -= 1; 324 + continue :outer; 325 + } 250 326 } 251 - switch (entry.kind) { 252 - .directory => try writer.writeAll(colors.dir), 253 - .sym_link => try writer.writeAll(colors.symlink), 254 - else => { 255 - if (entry.isExecutable()) { 256 - try writer.writeAll(colors.executable); 257 - } 258 - }, 327 + 328 + break :outer; 329 + } 330 + 331 + if (n_cols <= 1) return printShortOnePerLine(cmd, writer); 332 + 333 + std.log.debug("here 4", .{}); 334 + const n_rows = std.math.divCeil(usize, cmd.entries.len, columns.items.len) catch unreachable; 335 + for (0..n_rows) |row| { 336 + for (columns.items, 0..) |column, i| { 337 + if (row >= column.entries.len) continue; 338 + const entry = column.entries[row]; 339 + try printShortEntry(column.entries[row], cmd.opts, writer); 340 + 341 + if (i < columns.items.len - 1) { 342 + const spaces = column.width - (icon_width + entry.name.len); 343 + try writer.writeByteNTimes(' ', spaces); 344 + } 259 345 } 260 - try writer.writeAll(entry.name); 261 - try writer.writeAll(colors.reset); 346 + try writer.writeAll("\r\n"); 347 + } 348 + } 349 + 350 + fn isShortColumn(idx: usize, n_cols: usize, n_short_cols: usize) bool { 351 + return idx + n_short_cols >= n_cols; 352 + } 353 + 354 + fn printShortEntry(entry: Entry, opts: Options, writer: anytype) !void { 355 + const colors = opts.colors; 356 + if (opts.useIcons()) { 357 + const icon = Icon.get(entry, opts); 358 + 359 + if (opts.useColor()) { 360 + try writer.writeAll(icon.color); 361 + try writer.writeAll(icon.icon); 362 + try writer.writeAll(colors.reset); 363 + } else { 364 + try writer.writeAll(icon.icon); 365 + } 366 + 367 + try writer.writeByte(' '); 368 + } 369 + switch (entry.kind) { 370 + .directory => try writer.writeAll(colors.dir), 371 + .sym_link => try writer.writeAll(colors.symlink), 372 + else => { 373 + if (entry.isExecutable()) { 374 + try writer.writeAll(colors.executable); 375 + } 376 + }, 377 + } 378 + try writer.writeAll(entry.name); 379 + try writer.writeAll(colors.reset); 380 + } 381 + 382 + fn printShortOneRow(cmd: Command, writer: anytype) !void { 383 + for (cmd.entries) |entry| { 384 + try printShortEntry(entry, cmd.opts, writer); 385 + try writer.writeAll(" "); 386 + } 387 + try writer.writeAll("\r\n"); 388 + } 389 + 390 + fn printShortOnePerLine(cmd: Command, writer: anytype) !void { 391 + for (cmd.entries) |entry| { 392 + try printShortEntry(entry, cmd.opts, writer); 262 393 try writer.writeAll("\r\n"); 263 394 } 264 395 } ··· 793 924 794 925 fn eql(a: []const u8, b: []const u8) bool { 795 926 return std.mem.eql(u8, a, b); 927 + } 928 + 929 + fn parseArgBool(arg: []const u8) ?bool { 930 + if (arg.len == 0) return true; 931 + 932 + if (std.ascii.eqlIgnoreCase(arg, "true")) return true; 933 + if (std.ascii.eqlIgnoreCase(arg, "false")) return false; 934 + if (std.ascii.eqlIgnoreCase(arg, "1")) return true; 935 + if (std.ascii.eqlIgnoreCase(arg, "0")) return false; 936 + 937 + return null; 938 + } 939 + 940 + /// getWinsize gets the window size of the output. Returns null if output is not a terminal 941 + fn getWinsize(fd: posix.fd_t) ?posix.winsize { 942 + var winsize: posix.winsize = .{ 943 + .row = 0, 944 + .col = 0, 945 + .xpixel = 0, 946 + .ypixel = 0, 947 + }; 948 + 949 + const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); 950 + switch (posix.errno(err)) { 951 + .SUCCESS => return winsize, 952 + else => return null, 953 + } 796 954 } 797 955 798 956 fn optKind(a: []const u8) enum { short, long, positional } {