@tooee/view 0.1.20 → 0.1.21

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 (45) hide show
  1. package/dist/View.d.ts +7 -1
  2. package/dist/View.d.ts.map +1 -1
  3. package/dist/View.js +2 -2
  4. package/dist/View.js.map +1 -1
  5. package/dist/components/subviews/CodeSubview.d.ts.map +1 -1
  6. package/dist/components/subviews/CodeSubview.js +28 -3
  7. package/dist/components/subviews/CodeSubview.js.map +1 -1
  8. package/dist/components/subviews/CustomSubview.d.ts.map +1 -1
  9. package/dist/components/subviews/CustomSubview.js +15 -1
  10. package/dist/components/subviews/CustomSubview.js.map +1 -1
  11. package/dist/components/subviews/MarkdownSubview.d.ts +3 -1
  12. package/dist/components/subviews/MarkdownSubview.d.ts.map +1 -1
  13. package/dist/components/subviews/MarkdownSubview.js +62 -5
  14. package/dist/components/subviews/MarkdownSubview.js.map +1 -1
  15. package/dist/components/subviews/TableSubview.d.ts.map +1 -1
  16. package/dist/components/subviews/TableSubview.js +30 -3
  17. package/dist/components/subviews/TableSubview.js.map +1 -1
  18. package/dist/hooks/useContentLoader.d.ts.map +1 -1
  19. package/dist/hooks/useContentLoader.js +13 -7
  20. package/dist/hooks/useContentLoader.js.map +1 -1
  21. package/dist/hooks/useViewCommandContext.d.ts +28 -0
  22. package/dist/hooks/useViewCommandContext.d.ts.map +1 -1
  23. package/dist/hooks/useViewCommandContext.js.map +1 -1
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/launch.d.ts +7 -0
  29. package/dist/launch.d.ts.map +1 -1
  30. package/dist/launch.js +1 -1
  31. package/dist/launch.js.map +1 -1
  32. package/dist/types.d.ts +10 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/types.js.map +1 -1
  35. package/package.json +10 -10
  36. package/src/View.tsx +10 -2
  37. package/src/components/subviews/CodeSubview.tsx +40 -3
  38. package/src/components/subviews/CustomSubview.tsx +18 -1
  39. package/src/components/subviews/MarkdownSubview.tsx +84 -3
  40. package/src/components/subviews/TableSubview.tsx +42 -3
  41. package/src/hooks/useContentLoader.ts +15 -7
  42. package/src/hooks/useViewCommandContext.ts +27 -0
  43. package/src/index.ts +2 -0
  44. package/src/launch.tsx +8 -0
  45. package/src/types.ts +10 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAErD,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAgB5D,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAErD,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAgB5D,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA"}
package/dist/launch.d.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  import type { ActionDefinition } from "@tooee/commands";
2
+ import type { CodeBlockRenderer } from "@tooee/renderers";
2
3
  import type { ContentProvider, ContentRenderer } from "./types.js";
3
4
  export interface ViewLaunchOptions {
4
5
  contentProvider: ContentProvider;
5
6
  actions?: ActionDefinition[];
6
7
  renderers?: Record<string, ContentRenderer>;
8
+ /**
9
+ * Custom renderers for fenced code blocks in markdown content, keyed by
10
+ * fence type (first word of the fence info string, case-insensitive).
11
+ * Unmatched types fall back to the default syntax-highlighted code block.
12
+ */
13
+ codeBlockRenderers?: Record<string, CodeBlockRenderer>;
7
14
  }
8
15
  export interface DirectoryLaunchOptions {
9
16
  dirPath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAGvD,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAElE,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,eAAe,CAAA;IAChC,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAC5C;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtE;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpF"}
1
+ {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAElE,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,eAAe,CAAA;IAChC,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC3C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;CACvD;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAStE;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpF"}
package/dist/launch.js CHANGED
@@ -3,7 +3,7 @@ import { launchCli } from "@tooee/shell";
3
3
  import { View } from "./View.js";
4
4
  import { DirectoryView } from "./DirectoryView.js";
5
5
  export async function launch(options) {
6
- await launchCli(_jsx(View, { contentProvider: options.contentProvider, actions: options.actions, renderers: options.renderers }));
6
+ await launchCli(_jsx(View, { contentProvider: options.contentProvider, actions: options.actions, renderers: options.renderers, codeBlockRenderers: options.codeBlockRenderers }));
7
7
  }
8
8
  export async function launchDirectory(options) {
9
9
  await launchCli(_jsx(DirectoryView, { dirPath: options.dirPath, actions: options.actions }));
@@ -1 +1 @@
1
- {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAclD,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,OAA0B;IACrD,MAAM,SAAS,CACb,KAAC,IAAI,IACH,eAAe,EAAE,OAAO,CAAC,eAAe,EACxC,OAAO,EAAE,OAAO,CAAC,OAAO,EACxB,SAAS,EAAE,OAAO,CAAC,SAAS,GAC5B,CACH,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAA+B;IACnE,MAAM,SAAS,CAAC,KAAC,aAAa,IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,GAAI,CAAC,CAAA;AACxF,CAAC"}
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAoBlD,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,OAA0B;IACrD,MAAM,SAAS,CACb,KAAC,IAAI,IACH,eAAe,EAAE,OAAO,CAAC,eAAe,EACxC,OAAO,EAAE,OAAO,CAAC,OAAO,EACxB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,GAC9C,CACH,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAA+B;IACnE,MAAM,SAAS,CAAC,KAAC,aAAa,IAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,GAAI,CAAC,CAAA;AACxF,CAAC"}
package/dist/types.d.ts CHANGED
@@ -41,6 +41,16 @@ export interface ContentRendererProps {
41
41
  selectionEnd?: number;
42
42
  /** Combined mark state (nav + provider + user marks) */
43
43
  marks?: MarkState;
44
+ /**
45
+ * Select a row by index — the mouse equivalent of the keyboard cursor move.
46
+ * A custom renderer that has a notion of rows should call this from its own
47
+ * `onMouseDown`/`onRowClick` handlers to make left-click select a row, the
48
+ * same way the built-in code/markdown/table views do. Selection is a no-op
49
+ * while a modal overlay is open (the host guards it), so renderers can wire
50
+ * it unconditionally. Renderers that own a `RowDocumentRenderable` can map a
51
+ * click's screen-Y to a row via its `getRowAtScreenY(event.y)` helper.
52
+ */
53
+ onSelectRow?: (index: number) => void;
44
54
  }
45
55
  export type ContentRenderer = (props: ContentRendererProps) => ReactNode;
46
56
  export type AppendableFormat = Extract<ContentFormat, "markdown" | "code" | "text">;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAuGA,oBAAoB;AAEpB,MAAM,eAAe,GAAgB,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEnF,MAAM,UAAU,gBAAgB,CAAC,OAAmB;IAClD,OAAO,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAmB;IACjD,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAmB;IAChD,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAC,cAAc,EAAE,CAAA;IACjC,CAAC;IAED,IAAI,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,UAAU;YACb,OAAO,OAAO,CAAC,QAAQ,CAAA;QACzB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,IAAI,CAAA;QACrB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,IAAI,CAAA;QACrB,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,CAAA;YAC5E,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACxC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAC3E,CAAA;YACD,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrD,CAAC;QACD;YACE,OAAO,EAAE,CAAA;IACb,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAA;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACjF,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAiHA,oBAAoB;AAEpB,MAAM,eAAe,GAAgB,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEnF,MAAM,UAAU,gBAAgB,CAAC,OAAmB;IAClD,OAAO,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAmB;IACjD,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAmB;IAChD,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAC,cAAc,EAAE,CAAA;IACjC,CAAC;IAED,IAAI,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,UAAU;YACb,OAAO,OAAO,CAAC,QAAQ,CAAA;QACzB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,IAAI,CAAA;QACrB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,IAAI,CAAA;QACrB,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,CAAA;YAC5E,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACxC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAC3E,CAAA;YACD,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrD,CAAC;QACD;YACE,OAAO,EAAE,CAAA;IACb,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,EAAE,CAAA;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACjF,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACtB,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tooee/view",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Terminal content viewer for markdown, code, text, and tables",
5
5
  "keywords": [
6
6
  "cli",
@@ -37,15 +37,15 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@tooee/commands": "0.1.20",
41
- "@tooee/config": "0.1.20",
42
- "@tooee/layout": "0.1.20",
43
- "@tooee/marks": "0.1.20",
44
- "@tooee/overlays": "0.1.20",
45
- "@tooee/renderers": "0.1.20",
46
- "@tooee/search": "0.1.20",
47
- "@tooee/shell": "0.1.20",
48
- "@tooee/themes": "0.1.20",
40
+ "@tooee/commands": "0.1.21",
41
+ "@tooee/config": "0.1.21",
42
+ "@tooee/layout": "0.1.21",
43
+ "@tooee/marks": "0.1.21",
44
+ "@tooee/overlays": "0.1.21",
45
+ "@tooee/renderers": "0.1.21",
46
+ "@tooee/search": "0.1.21",
47
+ "@tooee/shell": "0.1.21",
48
+ "@tooee/themes": "0.1.21",
49
49
  "marked": "^17.0.4"
50
50
  },
51
51
  "devDependencies": {
package/src/View.tsx CHANGED
@@ -2,6 +2,7 @@ import { useState, useCallback } from "react"
2
2
  import { useTheme } from "@tooee/themes"
3
3
  import type { ActionDefinition } from "@tooee/commands"
4
4
  import type { MarkSet } from "@tooee/marks"
5
+ import type { CodeBlockRenderer } from "@tooee/renderers"
5
6
  import { isCustomContent, type ContentProvider, type ContentRenderer } from "./types.js"
6
7
  import { useContentLoader } from "./hooks/useContentLoader.js"
7
8
  import {
@@ -15,9 +16,14 @@ interface ViewProps {
15
16
  contentProvider: ContentProvider
16
17
  actions?: ActionDefinition[]
17
18
  renderers?: Record<string, ContentRenderer>
19
+ /**
20
+ * Custom renderers for fenced code blocks in markdown content, keyed by
21
+ * fence type (first word of the fence info string, case-insensitive).
22
+ */
23
+ codeBlockRenderers?: Record<string, CodeBlockRenderer>
18
24
  }
19
25
 
20
- export function View({ contentProvider, actions, renderers }: ViewProps) {
26
+ export function View({ contentProvider, actions, renderers, codeBlockRenderers }: ViewProps) {
21
27
  const { theme } = useTheme()
22
28
  const [reloadTrigger, setReloadTrigger] = useState(0)
23
29
  const reload = useCallback(() => setReloadTrigger((n) => n + 1), [])
@@ -77,7 +83,9 @@ export function View({ contentProvider, actions, renderers }: ViewProps) {
77
83
 
78
84
  switch (content.format) {
79
85
  case "markdown":
80
- return <MarkdownSubview content={content} {...shared} />
86
+ return (
87
+ <MarkdownSubview content={content} codeBlockRenderers={codeBlockRenderers} {...shared} />
88
+ )
81
89
  case "code":
82
90
  case "text":
83
91
  return <CodeSubview content={content} {...shared} />
@@ -1,8 +1,10 @@
1
- import { useEffect, useMemo, useRef } from "react"
2
- import { CodeView, type RowDocumentRenderable } from "@tooee/renderers"
1
+ import { useCallback, useEffect, useMemo, useRef } from "react"
2
+ import { CodeView, type ContextMenuEntry, type RowDocumentRenderable } from "@tooee/renderers"
3
3
  import { useTheme } from "@tooee/themes"
4
+ import { useCommandContext } from "@tooee/commands"
5
+ import { useHasModalOverlay } from "@tooee/overlays"
4
6
  import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
5
- import { useCopy, useNavigation } from "@tooee/shell"
7
+ import { useContextMenu, useCopy, useNavigation } from "@tooee/shell"
6
8
  import { findMatchingLines, useSearch } from "@tooee/search"
7
9
  import type { CodeContent, TextContent } from "../../types.js"
8
10
  import { useMarkState } from "../../hooks/useMarkState.js"
@@ -27,6 +29,9 @@ export function CodeSubview({
27
29
  }: CodeSubviewProps) {
28
30
  const { theme } = useTheme()
29
31
  const docRef = useRef<RowDocumentRenderable>(null)
32
+ const hasModalOverlay = useHasModalOverlay()
33
+ const contextMenu = useContextMenu()
34
+ const { invoke } = useCommandContext()
30
35
  const textContent = content.format === "code" ? content.code : content.text
31
36
  const lines = useMemo(() => textContent.split("\n"), [textContent])
32
37
  const lineCount = lines.length
@@ -76,6 +81,36 @@ export function CodeSubview({
76
81
 
77
82
  const text = content.format === "code" ? content.code : content.text
78
83
 
84
+ const menuEntries = useMemo<ContextMenuEntry[]>(
85
+ () =>
86
+ (actions ?? [])
87
+ .filter((action) => !action.hidden)
88
+ .map((action) => ({ id: action.id, title: action.title, hotkey: action.hotkey })),
89
+ [actions],
90
+ )
91
+
92
+ // Left-click selects the clicked line; right-click selects it and opens the
93
+ // same app-provided action menu as table rows. Both stand down while a modal
94
+ // overlay is up (theme picker, command palette, Ask/Choose), matching the
95
+ // keyboard stand-down and avoiding covered-app mutations.
96
+ const setCursor = nav.setCursor
97
+ const openContextMenu = contextMenu.open
98
+ const handleRowClick = useCallback(
99
+ (index: number) => {
100
+ if (hasModalOverlay) return
101
+ setCursor(index)
102
+ },
103
+ [hasModalOverlay, setCursor],
104
+ )
105
+ const handleRowContextMenu = useCallback(
106
+ (index: number, x: number, y: number) => {
107
+ if (hasModalOverlay) return
108
+ setCursor(index)
109
+ openContextMenu(x, y, menuEntries, invoke)
110
+ },
111
+ [hasModalOverlay, setCursor, openContextMenu, menuEntries, invoke],
112
+ )
113
+
79
114
  const extraStatusItems = useMemo(() => {
80
115
  const selectionCount = nav.selection != null ? nav.selection.end - nav.selection.start + 1 : 0
81
116
  const toggledCount = nav.toggledIndices.size
@@ -106,6 +141,8 @@ export function CodeSubview({
106
141
  showLineNumbers={showLineNumbers}
107
142
  marks={markState}
108
143
  docRef={docRef}
144
+ onRowClick={handleRowClick}
145
+ onRowContextMenu={handleRowContextMenu}
109
146
  />
110
147
  </SubviewLayout>
111
148
  )
@@ -1,6 +1,7 @@
1
- import { useEffect, useMemo, useRef } from "react"
1
+ import { useCallback, useEffect, useMemo, useRef } from "react"
2
2
  import { CodeView, type RowDocumentRenderable } from "@tooee/renderers"
3
3
  import { useTheme } from "@tooee/themes"
4
+ import { useHasModalOverlay } from "@tooee/overlays"
4
5
  import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
5
6
  import { useCopy, useNavigation } from "@tooee/shell"
6
7
  import { findMatchingLines, useSearch } from "@tooee/search"
@@ -29,6 +30,7 @@ export function CustomSubview({
29
30
  }: CustomSubviewProps) {
30
31
  const { theme } = useTheme()
31
32
  const docRef = useRef<RowDocumentRenderable>(null)
33
+ const hasModalOverlay = useHasModalOverlay()
32
34
  const textContent = useMemo(() => getTextContent(content), [content])
33
35
  const lines = useMemo(() => textContent.split("\n"), [textContent])
34
36
  const lineCount = lines.length
@@ -92,6 +94,20 @@ export function CustomSubview({
92
94
  ]
93
95
  }, [content.format, lineCount, nav.selection, nav.toggledIndices])
94
96
 
97
+ // Left-click selection for custom renderers. The framework does not own the
98
+ // renderer's markup, so it cannot map clicks to rows itself; instead it hands
99
+ // the renderer this callback (the mouse equivalent of the keyboard cursor
100
+ // move) to call from its own handlers. Guarded like the built-in views so a
101
+ // click never mutates the covered app while a modal overlay is up.
102
+ const setCursor = nav.setCursor
103
+ const handleSelectRow = useCallback(
104
+ (index: number) => {
105
+ if (hasModalOverlay) return
106
+ setCursor(index)
107
+ },
108
+ [hasModalOverlay, setCursor],
109
+ )
110
+
95
111
  const customRenderer = renderers?.[content.format]
96
112
  if (customRenderer) {
97
113
  const cursorLine = nav.cursor ?? undefined
@@ -113,6 +129,7 @@ export function CustomSubview({
113
129
  selectionStart,
114
130
  selectionEnd,
115
131
  marks: markState,
132
+ onSelectRow: handleSelectRow,
116
133
  })}
117
134
  </SubviewLayout>
118
135
  )
@@ -1,9 +1,18 @@
1
- import { useEffect, useMemo, useRef } from "react"
1
+ import { useCallback, useEffect, useMemo, useRef } from "react"
2
2
  import { marked } from "marked"
3
- import { MarkdownView, flattenTokens, type RowDocumentRenderable } from "@tooee/renderers"
3
+ import type { TextBufferRenderable } from "@opentui/core"
4
+ import {
5
+ MarkdownView,
6
+ flattenTokens,
7
+ type CodeBlockRenderer,
8
+ type ContextMenuEntry,
9
+ type RowDocumentRenderable,
10
+ } from "@tooee/renderers"
4
11
  import { useTheme } from "@tooee/themes"
12
+ import { useCommand, useCommandContext } from "@tooee/commands"
13
+ import { useHasModalOverlay } from "@tooee/overlays"
5
14
  import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
6
- import { useCopy, useNavigation } from "@tooee/shell"
15
+ import { useContextMenu, useCopy, useNavigation } from "@tooee/shell"
7
16
  import { useSearch } from "@tooee/search"
8
17
  import type { MarkdownContent } from "../../types.js"
9
18
  import { useMarkState } from "../../hooks/useMarkState.js"
@@ -13,10 +22,15 @@ import type { SubviewProps } from "./types.js"
13
22
 
14
23
  interface MarkdownSubviewProps extends SubviewProps {
15
24
  content: MarkdownContent
25
+ codeBlockRenderers?: Record<string, CodeBlockRenderer>
16
26
  }
17
27
 
28
+ /** Columns moved per h/l press when scrolling a wide block horizontally. */
29
+ const BLOCK_HSCROLL_STEP = 4
30
+
18
31
  export function MarkdownSubview({
19
32
  content,
33
+ codeBlockRenderers,
20
34
  providerMarks,
21
35
  userMarks,
22
36
  setMarkSet,
@@ -28,6 +42,9 @@ export function MarkdownSubview({
28
42
  }: MarkdownSubviewProps) {
29
43
  const { theme } = useTheme()
30
44
  const docRef = useRef<RowDocumentRenderable>(null)
45
+ const hasModalOverlay = useHasModalOverlay()
46
+ const contextMenu = useContextMenu()
47
+ const { invoke } = useCommandContext()
31
48
  const textContent = content.markdown
32
49
  const lineCount = useMemo(() => textContent.split("\n").length, [textContent])
33
50
  const blocks = useMemo(() => flattenTokens(marked.lexer(content.markdown)), [content.markdown])
@@ -66,6 +83,66 @@ export function MarkdownSubview({
66
83
  }
67
84
  }, [nav.cursor])
68
85
 
86
+ // Horizontal panning for wide blocks (mermaid diagrams and code blocks).
87
+ // Blocks that can overflow horizontally register their text-buffer
88
+ // renderable by block index; h/l in cursor mode pans the block under the
89
+ // nav cursor via `scrollX` (the setter clamps to the content width).
90
+ const hScrollableBlocksRef = useRef<Map<number, TextBufferRenderable>>(new Map())
91
+ const cursorScrollable = () =>
92
+ nav.cursor !== null ? hScrollableBlocksRef.current.get(nav.cursor) : undefined
93
+ useCommand({
94
+ id: "block-scroll-left",
95
+ title: "Scroll block left",
96
+ hotkey: "h",
97
+ modes: ["cursor"],
98
+ when: () => cursorScrollable() != null,
99
+ handler: () => {
100
+ const target = cursorScrollable()
101
+ if (target) target.scrollX -= BLOCK_HSCROLL_STEP
102
+ },
103
+ })
104
+ useCommand({
105
+ id: "block-scroll-right",
106
+ title: "Scroll block right",
107
+ hotkey: "l",
108
+ modes: ["cursor"],
109
+ when: () => cursorScrollable() != null,
110
+ handler: () => {
111
+ const target = cursorScrollable()
112
+ if (target) target.scrollX += BLOCK_HSCROLL_STEP
113
+ },
114
+ })
115
+
116
+ const menuEntries = useMemo<ContextMenuEntry[]>(
117
+ () =>
118
+ (actions ?? [])
119
+ .filter((action) => !action.hidden)
120
+ .map((action) => ({ id: action.id, title: action.title, hotkey: action.hotkey })),
121
+ [actions],
122
+ )
123
+
124
+ // Left-click selects the clicked block; right-click selects it and opens the
125
+ // same app-provided action menu as table/code rows. Both stand down while a
126
+ // modal overlay is up (theme picker, command palette, Ask/Choose), matching
127
+ // keyboard stand-down semantics.
128
+ const setCursor = nav.setCursor
129
+ const openContextMenu = contextMenu.open
130
+ const handleRowClick = useCallback(
131
+ (index: number) => {
132
+ if (hasModalOverlay) return
133
+ setCursor(index)
134
+ },
135
+ [hasModalOverlay, setCursor],
136
+ )
137
+ const handleRowContextMenu = useCallback(
138
+ (index: number, x: number, y: number) => {
139
+ if (hasModalOverlay) return
140
+ setCursor(index)
141
+ openContextMenu(x, y, menuEntries, invoke)
142
+ },
143
+ [hasModalOverlay, setCursor, openContextMenu, menuEntries, invoke],
144
+ )
145
+
69
146
  const { themeName, showLineNumbers } = useViewCommands({ content, textContent, actions })
70
147
 
71
148
  const markState = useMarkState({
@@ -116,6 +193,10 @@ export function MarkdownSubview({
116
193
  showLineNumbers={showLineNumbers}
117
194
  marks={markState}
118
195
  docRef={docRef}
196
+ hScrollableBlocksRef={hScrollableBlocksRef}
197
+ codeBlockRenderers={codeBlockRenderers}
198
+ onRowClick={handleRowClick}
199
+ onRowContextMenu={handleRowContextMenu}
119
200
  />
120
201
  </SubviewLayout>
121
202
  )
@@ -1,8 +1,10 @@
1
- import { useEffect, useMemo, useRef } from "react"
2
- import { Table, type RowDocumentRenderable } from "@tooee/renderers"
1
+ import { useCallback, useEffect, useMemo, useRef } from "react"
2
+ import { Table, type RowDocumentRenderable, type ContextMenuEntry } from "@tooee/renderers"
3
3
  import { useTheme } from "@tooee/themes"
4
+ import { useCommandContext } from "@tooee/commands"
5
+ import { useHasModalOverlay } from "@tooee/overlays"
4
6
  import { useViewCommandContext } from "../../hooks/useViewCommandContext.js"
5
- import { useCopy, useNavigation } from "@tooee/shell"
7
+ import { useContextMenu, useCopy, useNavigation } from "@tooee/shell"
6
8
  import { useSearch } from "@tooee/search"
7
9
  import { getTextContent, type TableContent } from "../../types.js"
8
10
  import { useMarkState } from "../../hooks/useMarkState.js"
@@ -28,6 +30,9 @@ export function TableSubview({
28
30
  const { theme } = useTheme()
29
31
  const docRef = useRef<RowDocumentRenderable>(null)
30
32
  const textContent = useMemo(() => getTextContent(content), [content])
33
+ const contextMenu = useContextMenu()
34
+ const { invoke } = useCommandContext()
35
+ const hasModalOverlay = useHasModalOverlay()
31
36
 
32
37
  const nav = useNavigation({
33
38
  rowCount: content.rows.length,
@@ -107,6 +112,38 @@ export function TableSubview({
107
112
  },
108
113
  })
109
114
 
115
+ // Row-scoped context-menu entries come from the app-provided actions.
116
+ const menuEntries = useMemo<ContextMenuEntry[]>(
117
+ () =>
118
+ (actions ?? [])
119
+ .filter((action) => !action.hidden)
120
+ .map((action) => ({ id: action.id, title: action.title, hotkey: action.hotkey })),
121
+ [actions],
122
+ )
123
+
124
+ // Row mouse handlers stand down while a modal overlay (theme picker, command
125
+ // palette, Ask/Choose overlays, the context menu itself) is up: centered
126
+ // overlays leave clickable margins around them, and mouse events route
127
+ // through the hit-grid, bypassing command-surface arbitration. Memoized so
128
+ // Table's rowElements memo keeps doing its job.
129
+ const setCursor = nav.setCursor
130
+ const openContextMenu = contextMenu.open
131
+ const handleRowClick = useCallback(
132
+ (index: number) => {
133
+ if (hasModalOverlay) return
134
+ setCursor(index)
135
+ },
136
+ [hasModalOverlay, setCursor],
137
+ )
138
+ const handleRowContextMenu = useCallback(
139
+ (index: number, x: number, y: number) => {
140
+ if (hasModalOverlay) return
141
+ setCursor(index)
142
+ openContextMenu(x, y, menuEntries, invoke)
143
+ },
144
+ [hasModalOverlay, setCursor, openContextMenu, menuEntries, invoke],
145
+ )
146
+
110
147
  const extraStatusItems = useMemo(() => {
111
148
  const selectionCount = nav.selection != null ? nav.selection.end - nav.selection.start + 1 : 0
112
149
  const toggledCount = nav.toggledIndices.size
@@ -144,6 +181,8 @@ export function TableSubview({
144
181
  showLineNumbers={showLineNumbers}
145
182
  marks={markState}
146
183
  docRef={docRef}
184
+ onRowClick={handleRowClick}
185
+ onRowContextMenu={handleRowContextMenu}
147
186
  />
148
187
  </SubviewLayout>
149
188
  )
@@ -92,12 +92,18 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
92
92
  setContent(current)
93
93
  setStreaming(true)
94
94
 
95
+ // Hold the iterator explicitly so cleanup can close it: a cleanup that
96
+ // only flips `cancelled` leaves a pending next() (and the provider's
97
+ // subprocess/file handle/network stream) alive until the next chunk.
98
+ const iterator = loaded[Symbol.asyncIterator]()
99
+
95
100
  ;(async () => {
96
101
  try {
97
- for await (const chunk of loaded) {
98
- if (cancelled) break
102
+ while (true) {
103
+ const result = await iterator.next()
104
+ if (cancelled || result.done) break
105
+ const chunk = result.value
99
106
  if (chunk.type === "marks") {
100
- if (cancelled) break
101
107
  // Streamed mark set: merge by replacing any existing set with same namespace
102
108
  setProviderMarks((prev) => {
103
109
  const filtered = prev.filter((s) => s.namespace !== chunk.set.namespace)
@@ -109,8 +115,8 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
109
115
  setContent(current)
110
116
  }
111
117
  } catch (err) {
112
- if (!cancelled && err instanceof Error) {
113
- setError(err.message)
118
+ if (!cancelled) {
119
+ setError(err instanceof Error ? err.message : String(err))
114
120
  }
115
121
  } finally {
116
122
  if (!cancelled) {
@@ -121,6 +127,8 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
121
127
 
122
128
  return () => {
123
129
  cancelled = true
130
+ // Close the iterator to release provider resources mid-stream.
131
+ Promise.resolve(iterator.return?.()).catch(() => {})
124
132
  }
125
133
  }
126
134
 
@@ -130,8 +138,8 @@ export function useContentLoader(contentProvider: ContentProvider, reloadTrigger
130
138
  .then((value) => {
131
139
  if (active) setContent(value)
132
140
  })
133
- .catch((err: Error) => {
134
- if (active) setError(err.message)
141
+ .catch((err: unknown) => {
142
+ if (active) setError(err instanceof Error ? err.message : String(err))
135
143
  })
136
144
  return () => {
137
145
  active = false
@@ -1,8 +1,35 @@
1
1
  import { useProvideCommandContext, useMode } from "@tooee/commands"
2
+ import type { Mode } from "@tooee/commands"
2
3
  import type { NavigationState } from "@tooee/shell"
3
4
  import type { MarkSet } from "@tooee/marks"
4
5
  import type { AnyContent } from "../types.js"
5
6
 
7
+ /** The `view` field contributed to the command context (see augmentation below). */
8
+ export interface ViewCommandContext {
9
+ content: AnyContent
10
+ format: string
11
+ cursor: number | null
12
+ selection: { start: number; end: number } | null
13
+ mode: Mode
14
+ toggledIndices: Set<number>
15
+ reload: () => void
16
+ marks: {
17
+ setMarkSet: (set: MarkSet) => void
18
+ clearNamespace: (namespace: string) => void
19
+ clearAll: () => void
20
+ userMarks: MarkSet[]
21
+ providerMarks: MarkSet[]
22
+ }
23
+ /** Subview extras (e.g. activeRow/selectedRows for tables). */
24
+ [key: string]: unknown
25
+ }
26
+
27
+ declare module "@tooee/commands" {
28
+ interface CommandContext {
29
+ view: ViewCommandContext
30
+ }
31
+ }
32
+
6
33
  interface UseViewCommandContextParams {
7
34
  content: AnyContent
8
35
  nav: NavigationState
package/src/index.ts CHANGED
@@ -25,3 +25,5 @@ export type {
25
25
  TableRow,
26
26
  } from "./types.js"
27
27
  export { getTextContent, isBuiltinContent, isCustomContent } from "./types.js"
28
+ export { CodeBlockChrome } from "@tooee/renderers"
29
+ export type { CodeBlockRenderer, CodeBlockRendererProps, CodeBlockHScroll } from "@tooee/renderers"
package/src/launch.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { launchCli } from "@tooee/shell"
2
2
  import type { ActionDefinition } from "@tooee/commands"
3
+ import type { CodeBlockRenderer } from "@tooee/renderers"
3
4
  import { View } from "./View.js"
4
5
  import { DirectoryView } from "./DirectoryView.js"
5
6
  import type { ContentProvider, ContentRenderer } from "./types.js"
@@ -8,6 +9,12 @@ export interface ViewLaunchOptions {
8
9
  contentProvider: ContentProvider
9
10
  actions?: ActionDefinition[]
10
11
  renderers?: Record<string, ContentRenderer>
12
+ /**
13
+ * Custom renderers for fenced code blocks in markdown content, keyed by
14
+ * fence type (first word of the fence info string, case-insensitive).
15
+ * Unmatched types fall back to the default syntax-highlighted code block.
16
+ */
17
+ codeBlockRenderers?: Record<string, CodeBlockRenderer>
11
18
  }
12
19
 
13
20
  export interface DirectoryLaunchOptions {
@@ -21,6 +28,7 @@ export async function launch(options: ViewLaunchOptions): Promise<void> {
21
28
  contentProvider={options.contentProvider}
22
29
  actions={options.actions}
23
30
  renderers={options.renderers}
31
+ codeBlockRenderers={options.codeBlockRenderers}
24
32
  />,
25
33
  )
26
34
  }
package/src/types.ts CHANGED
@@ -57,6 +57,16 @@ export interface ContentRendererProps {
57
57
  selectionEnd?: number
58
58
  /** Combined mark state (nav + provider + user marks) */
59
59
  marks?: MarkState
60
+ /**
61
+ * Select a row by index — the mouse equivalent of the keyboard cursor move.
62
+ * A custom renderer that has a notion of rows should call this from its own
63
+ * `onMouseDown`/`onRowClick` handlers to make left-click select a row, the
64
+ * same way the built-in code/markdown/table views do. Selection is a no-op
65
+ * while a modal overlay is open (the host guards it), so renderers can wire
66
+ * it unconditionally. Renderers that own a `RowDocumentRenderable` can map a
67
+ * click's screen-Y to a row via its `getRowAtScreenY(event.y)` helper.
68
+ */
69
+ onSelectRow?: (index: number) => void
60
70
  }
61
71
 
62
72
  export type ContentRenderer = (props: ContentRendererProps) => ReactNode