@tooee/view 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +15 -15
  2. package/dist/components/subviews/{CodeSubview.d.ts → code-subview.d.ts} +2 -2
  3. package/dist/components/subviews/code-subview.d.ts.map +1 -0
  4. package/dist/components/subviews/{CodeSubview.js → code-subview.js} +8 -8
  5. package/dist/components/subviews/code-subview.js.map +1 -0
  6. package/dist/components/subviews/custom-subview.d.ts +9 -0
  7. package/dist/components/subviews/custom-subview.d.ts.map +1 -0
  8. package/dist/components/subviews/{CustomSubview.js → custom-subview.js} +7 -7
  9. package/dist/components/subviews/custom-subview.js.map +1 -0
  10. package/dist/components/subviews/index.d.ts +4 -4
  11. package/dist/components/subviews/index.d.ts.map +1 -1
  12. package/dist/components/subviews/index.js +4 -4
  13. package/dist/components/subviews/index.js.map +1 -1
  14. package/dist/components/subviews/{MarkdownSubview.d.ts → markdown-subview.d.ts} +3 -3
  15. package/dist/components/subviews/markdown-subview.d.ts.map +1 -0
  16. package/dist/components/subviews/{MarkdownSubview.js → markdown-subview.js} +27 -25
  17. package/dist/components/subviews/markdown-subview.js.map +1 -0
  18. package/dist/components/subviews/table-subview.d.ts +8 -0
  19. package/dist/components/subviews/table-subview.d.ts.map +1 -0
  20. package/dist/components/subviews/{TableSubview.js → table-subview.js} +40 -22
  21. package/dist/components/subviews/table-subview.js.map +1 -0
  22. package/dist/components/subviews/types.d.ts.map +1 -1
  23. package/dist/components/{ViewScreen.d.ts → view-screen.d.ts} +3 -3
  24. package/dist/components/view-screen.d.ts.map +1 -0
  25. package/dist/components/{ViewScreen.js → view-screen.js} +11 -11
  26. package/dist/components/view-screen.js.map +1 -0
  27. package/dist/content-loader-store.d.ts +11 -7
  28. package/dist/content-loader-store.d.ts.map +1 -1
  29. package/dist/content-loader-store.js +70 -50
  30. package/dist/content-loader-store.js.map +1 -1
  31. package/dist/default-provider.d.ts +4 -4
  32. package/dist/default-provider.d.ts.map +1 -1
  33. package/dist/default-provider.js +48 -39
  34. package/dist/default-provider.js.map +1 -1
  35. package/dist/directory-provider.d.ts +2 -2
  36. package/dist/directory-provider.d.ts.map +1 -1
  37. package/dist/directory-provider.js +13 -10
  38. package/dist/directory-provider.js.map +1 -1
  39. package/dist/directory-view.d.ts +8 -0
  40. package/dist/directory-view.d.ts.map +1 -0
  41. package/dist/{DirectoryView.js → directory-view.js} +18 -12
  42. package/dist/directory-view.js.map +1 -0
  43. package/dist/hooks/index.d.ts +4 -4
  44. package/dist/hooks/index.d.ts.map +1 -1
  45. package/dist/hooks/index.js +3 -3
  46. package/dist/hooks/index.js.map +1 -1
  47. package/dist/hooks/{useContentCommands.d.ts → use-content-commands.d.ts} +2 -2
  48. package/dist/hooks/use-content-commands.d.ts.map +1 -0
  49. package/dist/hooks/{useContentCommands.js → use-content-commands.js} +6 -4
  50. package/dist/hooks/use-content-commands.js.map +1 -0
  51. package/dist/hooks/{useContentLoader.d.ts → use-content-loader.d.ts} +4 -4
  52. package/dist/hooks/use-content-loader.d.ts.map +1 -0
  53. package/dist/hooks/use-content-loader.js +81 -0
  54. package/dist/hooks/use-content-loader.js.map +1 -0
  55. package/dist/hooks/{useViewCommandContext.d.ts → use-view-command-context.d.ts} +3 -3
  56. package/dist/hooks/use-view-command-context.d.ts.map +1 -0
  57. package/dist/hooks/{useViewCommandContext.js → use-view-command-context.js} +15 -13
  58. package/dist/hooks/use-view-command-context.js.map +1 -0
  59. package/dist/index.d.ts +4 -4
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +3 -3
  62. package/dist/index.js.map +1 -1
  63. package/dist/launch.d.ts +2 -2
  64. package/dist/launch.d.ts.map +1 -1
  65. package/dist/launch.js +6 -6
  66. package/dist/launch.js.map +1 -1
  67. package/dist/types.d.ts +4 -4
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +46 -24
  70. package/dist/types.js.map +1 -1
  71. package/dist/{View.d.ts → view.d.ts} +3 -3
  72. package/dist/view.d.ts.map +1 -0
  73. package/dist/{View.js → view.js} +19 -13
  74. package/dist/view.js.map +1 -0
  75. package/package.json +10 -10
  76. package/src/components/subviews/{CodeSubview.tsx → code-subview.tsx} +25 -19
  77. package/src/components/subviews/{CustomSubview.tsx → custom-subview.tsx} +23 -21
  78. package/src/components/subviews/index.ts +5 -5
  79. package/src/components/subviews/{MarkdownSubview.tsx → markdown-subview.tsx} +53 -54
  80. package/src/components/subviews/table-subview.tsx +95 -0
  81. package/src/components/subviews/types.ts +14 -14
  82. package/src/components/{ViewScreen.tsx → view-screen.tsx} +33 -32
  83. package/src/content-loader-store.ts +131 -99
  84. package/src/default-provider.ts +84 -64
  85. package/src/directory-provider.ts +32 -24
  86. package/src/directory-view.tsx +79 -0
  87. package/src/hooks/index.ts +7 -4
  88. package/src/hooks/{useContentCommands.ts → use-content-commands.ts} +19 -14
  89. package/src/hooks/use-content-loader.ts +92 -0
  90. package/src/hooks/{useViewCommandContext.ts → use-view-command-context.ts} +40 -38
  91. package/src/index.ts +14 -14
  92. package/src/launch.tsx +20 -18
  93. package/src/types.ts +107 -78
  94. package/src/{View.tsx → view.tsx} +51 -44
  95. package/dist/DirectoryView.d.ts +0 -7
  96. package/dist/DirectoryView.d.ts.map +0 -1
  97. package/dist/DirectoryView.js.map +0 -1
  98. package/dist/View.d.ts.map +0 -1
  99. package/dist/View.js.map +0 -1
  100. package/dist/components/ViewScreen.d.ts.map +0 -1
  101. package/dist/components/ViewScreen.js.map +0 -1
  102. package/dist/components/subviews/CodeSubview.d.ts.map +0 -1
  103. package/dist/components/subviews/CodeSubview.js.map +0 -1
  104. package/dist/components/subviews/CustomSubview.d.ts +0 -9
  105. package/dist/components/subviews/CustomSubview.d.ts.map +0 -1
  106. package/dist/components/subviews/CustomSubview.js.map +0 -1
  107. package/dist/components/subviews/MarkdownSubview.d.ts.map +0 -1
  108. package/dist/components/subviews/MarkdownSubview.js.map +0 -1
  109. package/dist/components/subviews/TableSubview.d.ts +0 -8
  110. package/dist/components/subviews/TableSubview.d.ts.map +0 -1
  111. package/dist/components/subviews/TableSubview.js.map +0 -1
  112. package/dist/hooks/useContentCommands.d.ts.map +0 -1
  113. package/dist/hooks/useContentCommands.js.map +0 -1
  114. package/dist/hooks/useContentLoader.d.ts.map +0 -1
  115. package/dist/hooks/useContentLoader.js +0 -55
  116. package/dist/hooks/useContentLoader.js.map +0 -1
  117. package/dist/hooks/useViewCommandContext.d.ts.map +0 -1
  118. package/dist/hooks/useViewCommandContext.js.map +0 -1
  119. package/src/DirectoryView.tsx +0 -68
  120. package/src/components/subviews/TableSubview.tsx +0 -65
  121. package/src/hooks/useContentLoader.ts +0 -72
@@ -1,154 +1,186 @@
1
- import { createStore } from "@xstate/store"
2
- import type { MarkSet } from "@tooee/marks"
3
- import type { AnyContent, Content, ContentChunk, ContentFormat, CustomContent } from "./types.js"
1
+ import { createStore } from "@xstate/store";
2
+ import type { MarkSet } from "@tooee/marks";
3
+ import type { AnyContent, Content, ContentChunk, ContentFormat } from "./types.js";
4
4
 
5
- export type ContentLoaderStatus = "idle" | "loading" | "streaming" | "ready" | "error"
5
+ export type ContentLoaderStatus = "idle" | "loading" | "streaming" | "ready" | "error";
6
6
 
7
7
  export interface ContentLoaderContext {
8
- status: ContentLoaderStatus
9
- requestId: number
10
- loadSeq: number
11
- content: AnyContent | null
12
- error: string | null
13
- providerMarks: MarkSet[]
14
- title: string | undefined
8
+ status: ContentLoaderStatus;
9
+ requestId: number;
10
+ loadSeq: number;
11
+ content: AnyContent | null;
12
+ error: string | null;
13
+ providerMarks: MarkSet[];
14
+ title: string | undefined;
15
15
  }
16
16
 
17
- export type ContentLoaderEvents = {
18
- loadStarted: { marks: MarkSet[]; title?: string }
19
- streamStarted: { requestId: number; format: string }
20
- chunkReceived: { requestId: number; chunk: ContentChunk }
21
- loaded: { requestId: number; content: AnyContent }
22
- streamEnded: { requestId: number }
23
- loadFailed: { requestId: number; error: string }
24
- loadCancelled: { requestId: number }
25
- reloadRequested: {}
17
+ interface ContentLoaderEventPayloads {
18
+ loadStarted: { marks: MarkSet[]; title?: string };
19
+ streamStarted: { requestId: number; format: string };
20
+ chunkReceived: { requestId: number; chunk: ContentChunk };
21
+ loaded: { requestId: number; content: AnyContent };
22
+ streamEnded: { requestId: number };
23
+ loadFailed: { requestId: number; error: string };
24
+ loadCancelled: { requestId: number };
25
+ reloadRequested: Record<PropertyKey, never>;
26
26
  }
27
27
 
28
- export function isAsyncIterable(value: unknown): value is AsyncIterable<ContentChunk> {
29
- return value != null && typeof value === "object" && Symbol.asyncIterator in value
30
- }
28
+ export type ContentLoaderEvents = {
29
+ [Event in keyof ContentLoaderEventPayloads]: ContentLoaderEventPayloads[Event];
30
+ };
31
+
32
+ export const isAsyncIterable = function isAsyncIterable(
33
+ value: unknown,
34
+ ): value is AsyncIterable<ContentChunk> {
35
+ return (
36
+ value !== null &&
37
+ value !== undefined &&
38
+ typeof value === "object" &&
39
+ Symbol.asyncIterator in value
40
+ );
41
+ };
31
42
 
32
- export function createEmptyContent(format: string, title?: string): AnyContent {
43
+ export const createEmptyContent = function createEmptyContent(
44
+ format: string,
45
+ title?: string,
46
+ ): AnyContent {
33
47
  switch (format) {
34
- case "markdown":
35
- return { format, markdown: "", title }
36
- case "code":
37
- return { format, code: "", title }
38
- case "text":
39
- return { format, text: "", title }
40
- case "table":
41
- return { format, columns: [], rows: [], title }
42
- default:
43
- return { format, data: undefined, title } as CustomContent
48
+ case "markdown": {
49
+ return { format, markdown: "", title };
50
+ }
51
+ case "code": {
52
+ return { code: "", format, title };
53
+ }
54
+ case "text": {
55
+ return { format, text: "", title };
56
+ }
57
+ case "table": {
58
+ return { columns: [], format, rows: [], title };
59
+ }
60
+ default: {
61
+ return { data: undefined, format, title };
62
+ }
44
63
  }
45
- }
64
+ };
46
65
 
47
- function ensureContentFormat<F extends ContentFormat>(
66
+ const ensureContentFormat = function ensureContentFormat<F extends ContentFormat>(
48
67
  current: AnyContent | null,
49
68
  format: F,
50
69
  title?: string,
51
70
  ): Extract<Content, { format: F }> {
52
71
  if (!current || current.format !== format) {
53
- return createEmptyContent(format, title) as Extract<Content, { format: F }>
72
+ // Deferred(lint-sweep): add schema-based validation for custom and streamed content formats.
73
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- format-dependent content is trusted at this provider boundary
74
+ return createEmptyContent(format, title) as Extract<Content, { format: F }>;
54
75
  }
55
- return current as Extract<Content, { format: F }>
56
- }
76
+ // Deferred(lint-sweep): add schema-based validation for custom and streamed content formats.
77
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the provider contract supplies the matching format
78
+ return current as Extract<Content, { format: F }>;
79
+ };
57
80
 
58
- export function applyContentChunk(
81
+ export const applyContentChunk = function applyContentChunk(
59
82
  current: AnyContent | null,
60
83
  chunk: ContentChunk,
61
84
  title?: string,
62
85
  ): AnyContent {
63
86
  switch (chunk.type) {
64
- case "replace":
65
- return chunk.content
66
- case "append":
87
+ case "replace": {
88
+ return chunk.content;
89
+ }
90
+ case "append": {
67
91
  if (chunk.format === "markdown") {
68
- const target = ensureContentFormat(current, "markdown", title)
69
- return { ...target, markdown: target.markdown + chunk.data }
92
+ const target = ensureContentFormat(current, "markdown", title);
93
+ return { ...target, markdown: target.markdown + chunk.data };
70
94
  }
71
95
  if (chunk.format === "code") {
72
- const target = ensureContentFormat(current, "code", title)
96
+ const target = ensureContentFormat(current, "code", title);
73
97
  return {
74
98
  ...target,
75
99
  code: target.code + chunk.data,
76
100
  language: chunk.language ?? target.language,
77
- }
101
+ };
78
102
  }
79
103
  {
80
- const target = ensureContentFormat(current, "text", title)
81
- return { ...target, text: target.text + chunk.data }
104
+ const target = ensureContentFormat(current, "text", title);
105
+ return { ...target, text: target.text + chunk.data };
82
106
  }
83
- case "patch":
84
- return chunk.apply(current)
85
- default:
86
- return current ?? createEmptyContent("markdown", title)
107
+ }
108
+ case "patch": {
109
+ return chunk.apply(current);
110
+ }
111
+ case "marks": {
112
+ return current ?? createEmptyContent("markdown", title);
113
+ }
114
+ default: {
115
+ return current ?? createEmptyContent("markdown", title);
116
+ }
87
117
  }
88
- }
118
+ };
89
119
 
90
- export function normalizeError(error: unknown): string {
91
- return error instanceof Error ? error.message : String(error)
92
- }
120
+ export const normalizeError = function normalizeError(error: unknown): string {
121
+ return error instanceof Error ? error.message : String(error);
122
+ };
93
123
 
94
- export function createContentLoaderStore() {
124
+ export const createContentLoaderStore = function createContentLoaderStore() {
95
125
  return createStore<ContentLoaderContext, ContentLoaderEvents>({
96
126
  context: {
97
- status: "idle",
98
- requestId: 0,
99
- loadSeq: 0,
100
127
  content: null,
101
128
  error: null,
129
+ loadSeq: 0,
102
130
  providerMarks: [],
131
+ requestId: 0,
132
+ status: "idle",
103
133
  title: undefined,
104
134
  },
105
135
  on: {
106
- loadStarted: (ctx, event) => ({
107
- ...ctx,
108
- status: "loading",
109
- requestId: ctx.requestId + 1,
110
- error: null,
111
- providerMarks: event.marks,
112
- title: event.title,
113
- }),
114
- streamStarted: (ctx, event) =>
115
- event.requestId !== ctx.requestId
116
- ? ctx
117
- : {
118
- ...ctx,
119
- status: "streaming",
120
- content: createEmptyContent(event.format, ctx.title),
121
- },
122
136
  chunkReceived: (ctx, event) => {
123
- if (event.requestId !== ctx.requestId) return ctx
137
+ if (event.requestId !== ctx.requestId) {
138
+ return ctx;
139
+ }
124
140
  if (event.chunk.type === "marks") {
125
- const markSet = event.chunk.set
141
+ const markSet = event.chunk.set;
126
142
  const providerMarks = [
127
143
  ...ctx.providerMarks.filter((set) => set.namespace !== markSet.namespace),
128
144
  markSet,
129
- ]
130
- return { ...ctx, providerMarks }
145
+ ];
146
+ return { ...ctx, providerMarks };
131
147
  }
132
- return { ...ctx, content: applyContentChunk(ctx.content, event.chunk, ctx.title) }
148
+ return { ...ctx, content: applyContentChunk(ctx.content, event.chunk, ctx.title) };
133
149
  },
134
- loaded: (ctx, event) =>
135
- event.requestId !== ctx.requestId
136
- ? ctx
137
- : { ...ctx, status: "ready", content: event.content },
138
- streamEnded: (ctx, event) =>
139
- event.requestId !== ctx.requestId ? ctx : { ...ctx, status: "ready" },
140
- loadFailed: (ctx, event) =>
141
- event.requestId !== ctx.requestId ? ctx : { ...ctx, status: "error", error: event.error },
142
150
  loadCancelled: (ctx, event) =>
143
- event.requestId !== ctx.requestId ? ctx : { ...ctx, requestId: ctx.requestId + 1 },
151
+ event.requestId === ctx.requestId ? { ...ctx, requestId: ctx.requestId + 1 } : ctx,
152
+ loadFailed: (ctx, event) =>
153
+ event.requestId === ctx.requestId ? { ...ctx, error: event.error, status: "error" } : ctx,
154
+ loadStarted: (ctx, event) => ({
155
+ ...ctx,
156
+ error: null,
157
+ providerMarks: event.marks,
158
+ requestId: ctx.requestId + 1,
159
+ status: "loading",
160
+ title: event.title,
161
+ }),
162
+ loaded: (ctx, event) =>
163
+ event.requestId === ctx.requestId
164
+ ? { ...ctx, content: event.content, status: "ready" }
165
+ : ctx,
144
166
  reloadRequested: (ctx) => ({ ...ctx, loadSeq: ctx.loadSeq + 1 }),
167
+ streamEnded: (ctx, event) =>
168
+ event.requestId === ctx.requestId ? { ...ctx, status: "ready" } : ctx,
169
+ streamStarted: (ctx, event) =>
170
+ event.requestId === ctx.requestId
171
+ ? {
172
+ ...ctx,
173
+ content: createEmptyContent(event.format, ctx.title),
174
+ status: "streaming",
175
+ }
176
+ : ctx,
145
177
  },
146
- })
147
- }
178
+ });
179
+ };
148
180
 
149
- export const selectContent = (ctx: ContentLoaderContext) => ctx.content
150
- export const selectStatus = (ctx: ContentLoaderContext) => ctx.status
151
- export const selectStreaming = (ctx: ContentLoaderContext) => ctx.status === "streaming"
152
- export const selectError = (ctx: ContentLoaderContext) => ctx.error
153
- export const selectProviderMarks = (ctx: ContentLoaderContext) => ctx.providerMarks
154
- export const selectLoadSeq = (ctx: ContentLoaderContext) => ctx.loadSeq
181
+ export const selectContent = (ctx: ContentLoaderContext) => ctx.content;
182
+ export const selectStatus = (ctx: ContentLoaderContext) => ctx.status;
183
+ export const selectStreaming = (ctx: ContentLoaderContext) => ctx.status === "streaming";
184
+ export const selectError = (ctx: ContentLoaderContext) => ctx.error;
185
+ export const selectProviderMarks = (ctx: ContentLoaderContext) => ctx.providerMarks;
186
+ export const selectLoadSeq = (ctx: ContentLoaderContext) => ctx.loadSeq;
@@ -1,106 +1,126 @@
1
- import { parseAuto } from "@tooee/renderers"
2
- import type { Content, ContentFormat, ContentProvider } from "./types.js"
1
+ import { parseAuto } from "@tooee/renderers";
2
+ import type { Content, ContentFormat, ContentProvider } from "./types.js";
3
3
 
4
4
  export interface CreateProviderOptions {
5
- renderer?: ContentFormat
5
+ renderer?: ContentFormat;
6
6
  }
7
7
 
8
- function detectFormat(filePath: string): { format: ContentFormat; language?: string } {
9
- const ext = filePath.split(".").pop()?.toLowerCase()
10
- if (!ext) return { format: "text" }
8
+ const detectFormat = function detectFormat(filePath: string): {
9
+ format: ContentFormat;
10
+ language?: string;
11
+ } {
12
+ const ext = filePath.split(".").pop()?.toLowerCase();
13
+ if (ext === undefined || ext === "") {
14
+ return { format: "text" };
15
+ }
11
16
 
12
- const tableExts = new Set(["csv", "tsv"])
13
- if (tableExts.has(ext)) return { format: "table" }
17
+ const tableExts = new Set(["csv", "tsv"]);
18
+ if (tableExts.has(ext)) {
19
+ return { format: "table" };
20
+ }
14
21
 
15
- const markdownExts = new Set(["md", "mdx", "markdown"])
16
- if (markdownExts.has(ext)) return { format: "markdown" }
22
+ const markdownExts = new Set(["md", "mdx", "markdown"]);
23
+ if (markdownExts.has(ext)) {
24
+ return { format: "markdown" };
25
+ }
17
26
 
18
27
  const codeExts: Record<string, string> = {
19
- ts: "typescript",
20
- tsx: "typescript",
21
- js: "javascript",
22
- jsx: "javascript",
23
- py: "python",
24
- rs: "rust",
25
- go: "go",
26
- rb: "ruby",
27
- sh: "bash",
28
28
  bash: "bash",
29
- zsh: "zsh",
30
- json: "json",
31
- yaml: "yaml",
32
- yml: "yaml",
33
- toml: "toml",
34
- css: "css",
35
- html: "html",
36
- sql: "sql",
37
29
  c: "c",
38
30
  cpp: "cpp",
31
+ css: "css",
32
+ go: "go",
39
33
  h: "c",
40
34
  hpp: "cpp",
35
+ html: "html",
41
36
  java: "java",
37
+ js: "javascript",
38
+ json: "json",
39
+ jsx: "javascript",
42
40
  kt: "kotlin",
41
+ py: "python",
42
+ rb: "ruby",
43
+ rs: "rust",
44
+ sh: "bash",
45
+ sql: "sql",
43
46
  swift: "swift",
44
- }
47
+ toml: "toml",
48
+ ts: "typescript",
49
+ tsx: "typescript",
50
+ yaml: "yaml",
51
+ yml: "yaml",
52
+ zsh: "zsh",
53
+ };
45
54
 
46
- const language = codeExts[ext]
47
- if (language) return { format: "code", language }
55
+ const language = codeExts[ext];
56
+ if (typeof language === "string") {
57
+ return { format: "code", language };
58
+ }
48
59
 
49
- return { format: "text" }
50
- }
60
+ return { format: "text" };
61
+ };
51
62
 
52
- function contentFromText(
63
+ const contentFromText = function contentFromText(
53
64
  text: string,
54
65
  format: ContentFormat,
55
66
  title: string | undefined,
56
67
  language?: string,
57
68
  ): Content {
58
69
  switch (format) {
59
- case "markdown":
60
- return { format: "markdown", markdown: text, title }
61
- case "code":
62
- return { format: "code", code: text, language, title }
70
+ case "markdown": {
71
+ return { format: "markdown", markdown: text, title };
72
+ }
73
+ case "code": {
74
+ return { code: text, format: "code", language, title };
75
+ }
63
76
  case "table": {
64
- const parsed = parseAuto(text)
65
- return { format: "table", columns: parsed.columns, rows: parsed.rows, title }
77
+ const parsed = parseAuto(text);
78
+ return { columns: parsed.columns, format: "table", rows: parsed.rows, title };
79
+ }
80
+ case "text": {
81
+ return { format: "text", text, title };
82
+ }
83
+ default: {
84
+ return { format: "text", text, title };
66
85
  }
67
- case "text":
68
- default:
69
- return { format: "text", text, title }
70
86
  }
71
- }
87
+ };
72
88
 
73
- export function createFileProvider(
89
+ export const createFileProvider = function createFileProvider(
74
90
  filePath: string,
75
91
  options: CreateProviderOptions = {},
76
92
  ): ContentProvider {
77
93
  return {
78
94
  async load(): Promise<Content> {
79
- const detected = detectFormat(filePath)
80
- const format = options.renderer ?? detected.format
81
- const title = filePath.split("/").pop()
95
+ const detected = detectFormat(filePath);
96
+ const format = options.renderer ?? detected.format;
97
+ const title = filePath.split("/").pop();
82
98
 
83
- const file = Bun.file(filePath)
84
- const text = await file.text()
85
- return contentFromText(text, format, title, detected.language)
99
+ const file = Bun.file(filePath);
100
+ const text = await file.text();
101
+ return contentFromText(text, format, title, detected.language);
86
102
  },
87
- }
88
- }
103
+ };
104
+ };
89
105
 
90
- export function createStdinProvider(options: CreateProviderOptions = {}): ContentProvider {
106
+ export const createStdinProvider = function createStdinProvider(
107
+ options: CreateProviderOptions = {},
108
+ ): ContentProvider {
91
109
  return {
92
110
  async load(): Promise<Content> {
93
- const text = await new Response(Bun.stdin.stream() as unknown as ReadableStream).text()
94
- const format = options.renderer ?? "markdown"
95
- return contentFromText(text, format, "stdin")
111
+ const text = await new Response(Bun.stdin.stream()).text();
112
+ const format = options.renderer ?? "markdown";
113
+ return contentFromText(text, format, "stdin");
96
114
  },
97
- }
98
- }
115
+ };
116
+ };
99
117
 
100
- export function createTableFileProvider(filePath: string): ContentProvider {
101
- return createFileProvider(filePath, { renderer: "table" })
102
- }
118
+ export const createTableFileProvider = function createTableFileProvider(
119
+ filePath: string,
120
+ ): ContentProvider {
121
+ return createFileProvider(filePath, { renderer: "table" });
122
+ };
103
123
 
104
- export function createTableStdinProvider(): ContentProvider {
105
- return createStdinProvider({ renderer: "table" })
106
- }
124
+ export const createTableStdinProvider = function createTableStdinProvider(): ContentProvider {
125
+ return createStdinProvider({ renderer: "table" });
126
+ };
@@ -1,7 +1,7 @@
1
- import { readdirSync, statSync } from "fs"
2
- import { join } from "path"
3
- import type { ContentProvider } from "./types.js"
4
- import { createFileProvider } from "./default-provider.js"
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { ContentProvider } from "./types.js";
4
+ import { createFileProvider } from "./default-provider.js";
5
5
 
6
6
  const SUPPORTED_EXTENSIONS = new Set([
7
7
  "md",
@@ -33,35 +33,43 @@ const SUPPORTED_EXTENSIONS = new Set([
33
33
  "kt",
34
34
  "swift",
35
35
  "sql",
36
- ])
36
+ ]);
37
37
 
38
38
  export interface DirectoryEntry {
39
- name: string
40
- path: string
39
+ name: string;
40
+ path: string;
41
41
  }
42
42
 
43
- export function listDirectoryFiles(dirPath: string): DirectoryEntry[] {
44
- const entries = readdirSync(dirPath)
45
- const files: DirectoryEntry[] = []
43
+ export const listDirectoryFiles = function listDirectoryFiles(dirPath: string): DirectoryEntry[] {
44
+ const entries = readdirSync(dirPath);
45
+ const files: DirectoryEntry[] = [];
46
46
 
47
47
  for (const entry of entries) {
48
- if (entry.startsWith(".")) continue
49
- const fullPath = join(dirPath, entry)
48
+ if (entry.startsWith(".")) {
49
+ continue;
50
+ }
51
+ const fullPath = path.join(dirPath, entry);
50
52
  try {
51
- const stat = statSync(fullPath)
52
- if (!stat.isFile()) continue
53
+ const stat = statSync(fullPath);
54
+ if (!stat.isFile()) {
55
+ continue;
56
+ }
53
57
  } catch {
54
- continue
58
+ continue;
59
+ }
60
+ const ext = entry.split(".").pop()?.toLowerCase();
61
+ if (ext === undefined || ext === "" || !SUPPORTED_EXTENSIONS.has(ext)) {
62
+ continue;
55
63
  }
56
- const ext = entry.split(".").pop()?.toLowerCase()
57
- if (!ext || !SUPPORTED_EXTENSIONS.has(ext)) continue
58
- files.push({ name: entry, path: fullPath })
64
+ files.push({ name: entry, path: fullPath });
59
65
  }
60
66
 
61
- files.sort((a, b) => a.name.localeCompare(b.name))
62
- return files
63
- }
67
+ files.sort((a, b) => a.name.localeCompare(b.name));
68
+ return files;
69
+ };
64
70
 
65
- export function createDirectoryFileProvider(filePath: string): ContentProvider {
66
- return createFileProvider(filePath)
67
- }
71
+ export const createDirectoryFileProvider = function createDirectoryFileProvider(
72
+ filePath: string,
73
+ ): ContentProvider {
74
+ return createFileProvider(filePath);
75
+ };
@@ -0,0 +1,79 @@
1
+ import { useState, useMemo } from "react";
2
+ import { useCommand } from "@tooee/commands";
3
+ import type { ActionDefinition } from "@tooee/commands";
4
+ import { View } from "./view.js";
5
+ import { listDirectoryFiles } from "./directory-provider.js";
6
+ import type { DirectoryEntry } from "./directory-provider.js";
7
+ import type { AnyContent, ContentProvider } from "./types.js";
8
+ import { createFileProvider } from "./default-provider.js";
9
+
10
+ const createDirectoryFileProvider = function createDirectoryFileProvider(
11
+ entry: DirectoryEntry,
12
+ index: number,
13
+ total: number,
14
+ ): ContentProvider {
15
+ const inner = createFileProvider(entry.path);
16
+ return {
17
+ async load(): Promise<AnyContent> {
18
+ const result = inner.load();
19
+ // Deferred(lint-sweep): add schema-based validation for custom and streamed content formats.
20
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- file providers return external content at this boundary
21
+ const content = result instanceof Promise ? await result : (result as AnyContent);
22
+ return {
23
+ ...content,
24
+ title: `${entry.name} (${index + 1}/${total})`,
25
+ };
26
+ },
27
+ };
28
+ };
29
+
30
+ interface DirectoryViewProps {
31
+ dirPath: string;
32
+ actions?: ActionDefinition[];
33
+ }
34
+
35
+ export const DirectoryView = function DirectoryView({
36
+ dirPath,
37
+ actions,
38
+ }: DirectoryViewProps): React.ReactNode {
39
+ const files = useMemo(() => listDirectoryFiles(dirPath), [dirPath]);
40
+ const [currentIndex, setCurrentIndex] = useState(0);
41
+
42
+ const contentProvider = useMemo(
43
+ () =>
44
+ files.length > 0
45
+ ? createDirectoryFileProvider(files[currentIndex], currentIndex, files.length)
46
+ : null,
47
+ [files, currentIndex],
48
+ );
49
+
50
+ useCommand({
51
+ handler: () => {
52
+ setCurrentIndex((i) => Math.min(i + 1, files.length - 1));
53
+ },
54
+ hotkey: "l",
55
+ id: "dir.next-file",
56
+ modes: ["cursor"],
57
+ title: "Next file",
58
+ });
59
+
60
+ useCommand({
61
+ handler: () => {
62
+ setCurrentIndex((i) => Math.max(i - 1, 0));
63
+ },
64
+ hotkey: "h",
65
+ id: "dir.prev-file",
66
+ modes: ["cursor"],
67
+ title: "Previous file",
68
+ });
69
+
70
+ if (files.length === 0 || !contentProvider) {
71
+ return (
72
+ <box>
73
+ <text content="No viewable files in directory" />
74
+ </box>
75
+ );
76
+ }
77
+
78
+ return <View contentProvider={contentProvider} actions={actions} />;
79
+ };
@@ -1,8 +1,11 @@
1
- export { useContentLoader } from "./useContentLoader.js"
2
- export { useContentCommands } from "./useContentCommands.js"
3
- export { createViewCommandContext, useProvideViewCommandContext } from "./useViewCommandContext.js"
1
+ export { useContentLoader } from "./use-content-loader.js";
2
+ export { useContentCommands } from "./use-content-commands.js";
3
+ export {
4
+ createViewCommandContext,
5
+ useProvideViewCommandContext,
6
+ } from "./use-view-command-context.js";
4
7
  export type {
5
8
  CreateViewCommandContextOptions,
6
9
  ProvideViewCommandContextOptions,
7
10
  ViewCommandContext,
8
- } from "./useViewCommandContext.js"
11
+ } from "./use-view-command-context.js";