Skip to content

Commit 5d15993

Browse files
lambdalisueclaude
andcommitted
feat(renderer): add file info renderer
Implements a new renderer that appends file metadata (size, modification time, permissions, type) to item labels. Supports relative time display, field selection, and optional colorization based on file type. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2176900 commit 5d15993

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed

builtin/renderer/file_info.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { format } from "@std/fmt/bytes";
2+
import { relative } from "@std/path/relative";
3+
4+
import { defineRenderer, type Renderer } from "../../renderer.ts";
5+
6+
type Detail = {
7+
path: string;
8+
};
9+
10+
export type FileInfoOptions = {
11+
/**
12+
* Which file information to display.
13+
* @default ["size", "modified"]
14+
*/
15+
fields?: Array<"size" | "modified" | "permissions" | "type">;
16+
17+
/**
18+
* Whether to show relative timestamps (e.g., "2 hours ago").
19+
* @default true
20+
*/
21+
relativeTime?: boolean;
22+
23+
/**
24+
* Width of each field in characters.
25+
* @default { size: 8, modified: 16, permissions: 10, type: 4 }
26+
*/
27+
fieldWidths?: {
28+
size?: number;
29+
modified?: number;
30+
permissions?: number;
31+
type?: number;
32+
};
33+
34+
/**
35+
* Whether to colorize output (using ANSI codes).
36+
* @default false
37+
*/
38+
colorize?: boolean;
39+
};
40+
41+
/**
42+
* Creates a Renderer that appends file information to item labels.
43+
*
44+
* This Renderer adds file metadata such as size, modification time,
45+
* permissions, and file type to each item's label in a formatted manner.
46+
*
47+
* @param options - Options to customize file info display.
48+
* @returns A Renderer that adds file information to item labels.
49+
*/
50+
export function fileInfo(
51+
options: Readonly<FileInfoOptions> = {},
52+
): Renderer<Detail> {
53+
const fields = options.fields ?? ["size", "modified"];
54+
const relativeTime = options.relativeTime ?? true;
55+
const fieldWidths = {
56+
size: 8,
57+
modified: 16,
58+
permissions: 10,
59+
type: 4,
60+
...options.fieldWidths,
61+
};
62+
const colorize = options.colorize ?? false;
63+
64+
return defineRenderer(async (_denops, { items }) => {
65+
// Process items in parallel
66+
await Promise.all(
67+
items.map(async (item) => {
68+
const { path } = item.detail;
69+
const parts: string[] = [];
70+
71+
try {
72+
// Get file stats
73+
const stat = await Deno.stat(path);
74+
75+
// Add requested fields
76+
for (const field of fields) {
77+
switch (field) {
78+
case "size": {
79+
const sizeStr = stat.isFile
80+
? format(stat.size, { minimumFractionDigits: 0 })
81+
: stat.isDirectory
82+
? "-"
83+
: "0B";
84+
parts.push(sizeStr.padStart(fieldWidths.size));
85+
break;
86+
}
87+
88+
case "modified": {
89+
const mtime = stat.mtime;
90+
let timeStr: string;
91+
if (mtime && relativeTime) {
92+
timeStr = formatRelativeTime(mtime);
93+
} else if (mtime) {
94+
timeStr = formatDate(mtime);
95+
} else {
96+
timeStr = "-";
97+
}
98+
parts.push(timeStr.padEnd(fieldWidths.modified));
99+
break;
100+
}
101+
102+
case "permissions": {
103+
const permsStr = formatPermissions(stat.mode);
104+
parts.push(permsStr.padEnd(fieldWidths.permissions));
105+
break;
106+
}
107+
108+
case "type": {
109+
const typeStr = stat.isDirectory
110+
? "dir"
111+
: stat.isFile
112+
? "file"
113+
: stat.isSymlink
114+
? "link"
115+
: "other";
116+
parts.push(typeStr.padEnd(fieldWidths.type));
117+
break;
118+
}
119+
}
120+
}
121+
122+
// Combine parts and append to label
123+
const info = parts.join(" ");
124+
if (colorize) {
125+
// Add color based on file type
126+
if (stat.isDirectory) {
127+
item.label = `${item.label} \x1b[34m${info}\x1b[0m`;
128+
} else if (stat.isSymlink) {
129+
item.label = `${item.label} \x1b[36m${info}\x1b[0m`;
130+
} else if ((stat.mode ?? 0) & 0o111) {
131+
// Executable
132+
item.label = `${item.label} \x1b[32m${info}\x1b[0m`;
133+
} else {
134+
item.label = `${item.label} \x1b[90m${info}\x1b[0m`;
135+
}
136+
} else {
137+
item.label = `${item.label} ${info}`;
138+
}
139+
} catch {
140+
// If stat fails, add placeholder
141+
const placeholder = fields.map((field) => {
142+
const width = fieldWidths[field as keyof typeof fieldWidths] ?? 10;
143+
return "-".padEnd(width);
144+
}).join(" ");
145+
item.label = `${item.label} ${placeholder}`;
146+
}
147+
}),
148+
);
149+
});
150+
}
151+
152+
/**
153+
* Formats a date to a relative time string.
154+
*/
155+
function formatRelativeTime(date: Date): string {
156+
const now = new Date();
157+
const diff = now.getTime() - date.getTime();
158+
const seconds = Math.floor(diff / 1000);
159+
const minutes = Math.floor(seconds / 60);
160+
const hours = Math.floor(minutes / 60);
161+
const days = Math.floor(hours / 24);
162+
const months = Math.floor(days / 30);
163+
const years = Math.floor(days / 365);
164+
165+
if (years > 0) {
166+
return `${years}y ago`;
167+
} else if (months > 0) {
168+
return `${months}mo ago`;
169+
} else if (days > 0) {
170+
return `${days}d ago`;
171+
} else if (hours > 0) {
172+
return `${hours}h ago`;
173+
} else if (minutes > 0) {
174+
return `${minutes}m ago`;
175+
} else {
176+
return "just now";
177+
}
178+
}
179+
180+
/**
181+
* Formats a date to a standard string.
182+
*/
183+
function formatDate(date: Date): string {
184+
const year = date.getFullYear();
185+
const month = String(date.getMonth() + 1).padStart(2, "0");
186+
const day = String(date.getDate()).padStart(2, "0");
187+
const hour = String(date.getHours()).padStart(2, "0");
188+
const minute = String(date.getMinutes()).padStart(2, "0");
189+
return `${year}-${month}-${day} ${hour}:${minute}`;
190+
}
191+
192+
/**
193+
* Formats file permissions to a Unix-style string.
194+
*/
195+
function formatPermissions(mode: number | null): string {
196+
if (mode === null) {
197+
return "---------";
198+
}
199+
200+
const perms = [];
201+
const types = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
202+
203+
// User permissions
204+
perms.push(types[(mode >> 6) & 7]);
205+
// Group permissions
206+
perms.push(types[(mode >> 3) & 7]);
207+
// Other permissions
208+
perms.push(types[mode & 7]);
209+
210+
return perms.join("");
211+
}

builtin/renderer/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// This file is generated by gen-mod.ts
22
export * from "./absolute_path.ts";
3+
export * from "./file_info.ts";
34
export * from "./helptag.ts";
45
export * from "./nerdfont.ts";
56
export * from "./noop.ts";

0 commit comments

Comments
 (0)