file-path.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import type { RGBA } from "@opentui/core"
  2. import { createMemo } from "solid-js"
  3. import { stringWidth } from "../util/string-width"
  4. const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" })
  5. export interface FilePathProps {
  6. value: string
  7. maxWidth: number
  8. fg?: RGBA
  9. basenameFg?: RGBA
  10. }
  11. export function FilePath(props: FilePathProps) {
  12. const display = createMemo(() => {
  13. const value = truncateFilePath(props.value, props.maxWidth)
  14. const index = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"))
  15. return {
  16. parent: value.slice(0, index + 1),
  17. basename: value.slice(index + 1),
  18. }
  19. })
  20. return (
  21. <text fg={props.fg} wrapMode="none" truncate>
  22. <span style={{ fg: props.fg }}>{display().parent}</span>
  23. <span style={{ fg: props.basenameFg ?? props.fg }}>{display().basename}</span>
  24. </text>
  25. )
  26. }
  27. export function truncateFilePath(value: string, maxWidth: number) {
  28. if (maxWidth <= 0) return ""
  29. if (stringWidth(value) <= maxWidth) return value
  30. const drive = value.match(/^([A-Za-z]:)([\\/])/)
  31. const unc = value.match(/^(\\\\|\/\/)([^\\/]+)[\\/]([^\\/]+)(?:[\\/]|$)/)
  32. const windows = drive !== null || unc !== null || (!value.includes("/") && value.includes("\\"))
  33. const separator = drive?.[2] ?? (unc?.[1] === "//" ? "/" : windows ? "\\" : "/")
  34. const root = drive
  35. ? drive[1] + separator
  36. : unc
  37. ? unc[1] + unc[2] + separator + unc[3] + separator
  38. : value.startsWith("/")
  39. ? "/"
  40. : ""
  41. const source = value.slice(drive?.[0].length ?? unc?.[0].length ?? root.length)
  42. const segments = source.split(windows ? /[\\/]/ : separator).filter(Boolean)
  43. const basename = segments.at(-1) ?? value
  44. if (segments.length < 2) {
  45. const rootWidth = stringWidth(root)
  46. if (rootWidth >= maxWidth) return takeStart(root, maxWidth)
  47. return root + truncateBasename(basename, maxWidth - rootWidth)
  48. }
  49. const prefix = `${root}…${separator}`
  50. const basenameWidth = maxWidth - stringWidth(prefix)
  51. if (basenameWidth <= 0) return takeStart(prefix, maxWidth)
  52. const compact = truncateBasename(basename, basenameWidth)
  53. if (compact !== basename) return prefix + compact
  54. const selected = [basename]
  55. const separatorWidth = stringWidth(separator)
  56. let width = stringWidth(prefix + basename)
  57. for (let index = segments.length - 2; index >= 0; index--) {
  58. const segment = segments[index]!
  59. const next = stringWidth(segment) + separatorWidth
  60. if (width + next > maxWidth) {
  61. const available = maxWidth - width - separatorWidth
  62. if (available > 1) selected.unshift(takeStart(segment, available - 1) + "…")
  63. break
  64. }
  65. selected.unshift(segment)
  66. width += next
  67. }
  68. return prefix + selected.join(separator)
  69. }
  70. function truncateBasename(value: string, maxWidth: number) {
  71. if (stringWidth(value) <= maxWidth) return value
  72. if (maxWidth <= 1) return takeStart("…", maxWidth)
  73. const dot = value.lastIndexOf(".")
  74. const extension = dot > 0 ? value.slice(dot) : ""
  75. const extensionWidth = stringWidth(extension)
  76. if (extensionWidth >= maxWidth) return "…" + takeEnd(extension, maxWidth - 1)
  77. const stem = extension ? value.slice(0, dot) : value
  78. return takeStart(stem, maxWidth - extensionWidth - 1) + "…" + extension
  79. }
  80. function takeStart(value: string, maxWidth: number) {
  81. return take(value, maxWidth, false)
  82. }
  83. function takeEnd(value: string, maxWidth: number) {
  84. return take(value, maxWidth, true)
  85. }
  86. function take(value: string, maxWidth: number, reverse: boolean) {
  87. const segments = Array.from(graphemeSegmenter.segment(value), (item) => item.segment)
  88. if (reverse) segments.reverse()
  89. const selected: string[] = []
  90. let width = 0
  91. for (const segment of segments) {
  92. const next = stringWidth(segment)
  93. if (width + next > maxWidth) break
  94. selected.push(segment)
  95. width += next
  96. }
  97. if (reverse) selected.reverse()
  98. return selected.join("")
  99. }