| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- import type { RGBA } from "@opentui/core"
- import { createMemo } from "solid-js"
- import { stringWidth } from "../util/string-width"
- const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" })
- export interface FilePathProps {
- value: string
- maxWidth: number
- fg?: RGBA
- basenameFg?: RGBA
- }
- export function FilePath(props: FilePathProps) {
- const display = createMemo(() => {
- const value = truncateFilePath(props.value, props.maxWidth)
- const index = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"))
- return {
- parent: value.slice(0, index + 1),
- basename: value.slice(index + 1),
- }
- })
- return (
- <text fg={props.fg} wrapMode="none" truncate>
- <span style={{ fg: props.fg }}>{display().parent}</span>
- <span style={{ fg: props.basenameFg ?? props.fg }}>{display().basename}</span>
- </text>
- )
- }
- export function truncateFilePath(value: string, maxWidth: number) {
- if (maxWidth <= 0) return ""
- if (stringWidth(value) <= maxWidth) return value
- const drive = value.match(/^([A-Za-z]:)([\\/])/)
- const unc = value.match(/^(\\\\|\/\/)([^\\/]+)[\\/]([^\\/]+)(?:[\\/]|$)/)
- const windows = drive !== null || unc !== null || (!value.includes("/") && value.includes("\\"))
- const separator = drive?.[2] ?? (unc?.[1] === "//" ? "/" : windows ? "\\" : "/")
- const root = drive
- ? drive[1] + separator
- : unc
- ? unc[1] + unc[2] + separator + unc[3] + separator
- : value.startsWith("/")
- ? "/"
- : ""
- const source = value.slice(drive?.[0].length ?? unc?.[0].length ?? root.length)
- const segments = source.split(windows ? /[\\/]/ : separator).filter(Boolean)
- const basename = segments.at(-1) ?? value
- if (segments.length < 2) {
- const rootWidth = stringWidth(root)
- if (rootWidth >= maxWidth) return takeStart(root, maxWidth)
- return root + truncateBasename(basename, maxWidth - rootWidth)
- }
- const prefix = `${root}…${separator}`
- const basenameWidth = maxWidth - stringWidth(prefix)
- if (basenameWidth <= 0) return takeStart(prefix, maxWidth)
- const compact = truncateBasename(basename, basenameWidth)
- if (compact !== basename) return prefix + compact
- const selected = [basename]
- const separatorWidth = stringWidth(separator)
- let width = stringWidth(prefix + basename)
- for (let index = segments.length - 2; index >= 0; index--) {
- const segment = segments[index]!
- const next = stringWidth(segment) + separatorWidth
- if (width + next > maxWidth) {
- const available = maxWidth - width - separatorWidth
- if (available > 1) selected.unshift(takeStart(segment, available - 1) + "…")
- break
- }
- selected.unshift(segment)
- width += next
- }
- return prefix + selected.join(separator)
- }
- function truncateBasename(value: string, maxWidth: number) {
- if (stringWidth(value) <= maxWidth) return value
- if (maxWidth <= 1) return takeStart("…", maxWidth)
- const dot = value.lastIndexOf(".")
- const extension = dot > 0 ? value.slice(dot) : ""
- const extensionWidth = stringWidth(extension)
- if (extensionWidth >= maxWidth) return "…" + takeEnd(extension, maxWidth - 1)
- const stem = extension ? value.slice(0, dot) : value
- return takeStart(stem, maxWidth - extensionWidth - 1) + "…" + extension
- }
- function takeStart(value: string, maxWidth: number) {
- return take(value, maxWidth, false)
- }
- function takeEnd(value: string, maxWidth: number) {
- return take(value, maxWidth, true)
- }
- function take(value: string, maxWidth: number, reverse: boolean) {
- const segments = Array.from(graphemeSegmenter.segment(value), (item) => item.segment)
- if (reverse) segments.reverse()
- const selected: string[] = []
- let width = 0
- for (const segment of segments) {
- const next = stringWidth(segment)
- if (width + next > maxWidth) break
- selected.push(segment)
- width += next
- }
- if (reverse) selected.reverse()
- return selected.join("")
- }
|