Revert "Code deduplication and cleanup, fix forward delete button on text editing, add ability to show separators in color editing menu, fix truncation issues"

This reverts commit 3e2c3eaa90.
This commit is contained in:
Matthew Breedlove
2025-08-12 02:55:41 -04:00
parent 3e2c3eaa90
commit 256b562cca
9 changed files with 1779 additions and 1131 deletions
+3 -141
View File
@@ -4,15 +4,15 @@
"": {
"name": "ccstatusline",
"dependencies": {
"@types/react": "^19.1.9",
"chalk": "^5.5.0",
"ink": "^6.1.0",
"ink-select-input": "^6.2.0",
"ink-text-input": "^6.0.0",
"react": "^19.1.1",
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19.1.9",
"patch-package": "^8.0.0",
},
"peerDependencies": {
"typescript": "^5",
@@ -28,36 +28,18 @@
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
"ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="],
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
@@ -66,136 +48,46 @@
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-toolkit": ["es-toolkit@1.39.8", "", {}, "sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg=="],
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="],
"fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ink": ["ink@6.1.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-YQ+lbMD79y3FBAJXXZnuRajLEgaMFp102361eY5NrBIEVCi9oFo7gNZU4z2LBWlcjZFiTt7jetlkIbKCCH4KJA=="],
"ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="],
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
"klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"patch-package": ["patch-package@8.0.0", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.0.33", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
@@ -204,24 +96,12 @@
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="],
"slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
@@ -230,12 +110,6 @@
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
@@ -244,32 +118,20 @@
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
"patch-package/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="],
"patch-package/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
}
}
+619
View File
@@ -0,0 +1,619 @@
{
"name": "ccstatusline",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ccstatusline",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@types/react": "^19.1.9",
"chalk": "^5.5.0",
"ink": "^6.1.0",
"ink-select-input": "^6.2.0",
"ink-text-input": "^6.0.0",
"react": "^19.1.1"
},
"bin": {
"ccstatusline": "dist/ccstatusline.js"
},
"devDependencies": {
"@types/bun": "latest"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"typescript": "^5"
}
},
"node_modules/@alcalzone/ansi-tokenize": {
"version": "0.1.3",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=14.13.1"
}
},
"node_modules/@types/bun": {
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.19.tgz",
"integrity": "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"bun-types": "1.2.19"
}
},
"node_modules/@types/node": {
"version": "24.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.10.0"
}
},
"node_modules/@types/react": {
"version": "19.1.9",
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/ansi-escapes": {
"version": "7.0.0",
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/auto-bind": {
"version": "5.0.1",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bun-types": {
"version": "1.2.19",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
},
"peerDependencies": {
"@types/react": "^19"
}
},
"node_modules/chalk": {
"version": "5.5.0",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cli-boxes": {
"version": "3.0.0",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"restore-cursor": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/slice-ansi": {
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/code-excerpt": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"convert-to-spaces": "^2.0.1"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/convert-to-spaces": {
"version": "2.0.1",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "10.4.0",
"license": "MIT"
},
"node_modules/environment": {
"version": "1.1.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/es-toolkit": {
"version": "1.39.8",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/escape-string-regexp": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/figures": {
"version": "6.1.0",
"license": "MIT",
"dependencies": {
"is-unicode-supported": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/indent-string": {
"version": "5.0.0",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink": {
"version": "6.1.0",
"license": "MIT",
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.1.3",
"ansi-escapes": "^7.0.0",
"ansi-styles": "^6.2.1",
"auto-bind": "^5.0.1",
"chalk": "^5.3.0",
"cli-boxes": "^3.0.0",
"cli-cursor": "^4.0.0",
"cli-truncate": "^4.0.0",
"code-excerpt": "^4.0.0",
"es-toolkit": "^1.22.0",
"indent-string": "^5.0.0",
"is-in-ci": "^1.0.0",
"patch-console": "^2.0.0",
"react-reconciler": "^0.32.0",
"scheduler": "^0.23.0",
"signal-exit": "^3.0.7",
"slice-ansi": "^7.1.0",
"stack-utils": "^2.0.6",
"string-width": "^7.2.0",
"type-fest": "^4.27.0",
"widest-line": "^5.0.0",
"wrap-ansi": "^9.0.0",
"ws": "^8.18.0",
"yoga-layout": "~3.2.1"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@types/react": ">=19.0.0",
"react": ">=19.0.0",
"react-devtools-core": "^4.19.1"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react-devtools-core": {
"optional": true
}
}
},
"node_modules/ink-select-input": {
"version": "6.2.0",
"license": "MIT",
"dependencies": {
"figures": "^6.1.0",
"to-rotated": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"ink": ">=5.0.0",
"react": ">=18.0.0"
}
},
"node_modules/ink-text-input": {
"version": "6.0.0",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"type-fest": "^4.18.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"ink": ">=5",
"react": ">=18"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-in-ci": {
"version": "1.0.0",
"license": "MIT",
"bin": {
"is-in-ci": "cli.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/patch-console": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/react": {
"version": "19.1.1",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-reconciler": {
"version": "0.32.0",
"license": "MIT",
"dependencies": {
"scheduler": "^0.26.0"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.1.0"
}
},
"node_modules/react-reconciler/node_modules/scheduler": {
"version": "0.26.0",
"license": "MIT"
},
"node_modules/restore-cursor": {
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"license": "ISC"
},
"node_modules/slice-ansi": {
"version": "7.1.0",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/string-width": {
"version": "7.2.0",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/to-rotated": {
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.2",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.10.0",
"dev": true,
"license": "MIT"
},
"node_modules/widest-line": {
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.0",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.18.3",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yoga-layout": {
"version": "3.2.1",
"license": "MIT"
}
}
}
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "1.1.3",
"version": "1.1.4",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
@@ -13,21 +13,20 @@
"scripts": {
"start": "bun run src/ccstatusline.ts",
"build": "rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --packages=external --target-version=14",
"prepublishOnly": "bun run build",
"postinstall": "patch-package"
"prepublishOnly": "bun run build"
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19.1.9",
"patch-package": "^8.0.0"
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@types/react": "^19.1.9",
"chalk": "^5.5.0",
"ink": "^6.1.0",
"ink-select-input": "^6.2.0",
"ink-text-input": "^6.0.0",
"react": "^19.1.1"
},
"keywords": [
-17
View File
@@ -1,17 +0,0 @@
diff --git a/node_modules/ink/build/parse-keypress.js b/node_modules/ink/build/parse-keypress.js
index 1e13e81..e100611 100644
--- a/node_modules/ink/build/parse-keypress.js
+++ b/node_modules/ink/build/parse-keypress.js
@@ -161,9 +161,9 @@ const parseKeypress = (s = '') => {
key.meta = s.charAt(0) === '\x1b';
}
else if (s === '\x7f' || s === '\x1b\x7f') {
- // TODO(vadimdemedes): `enquirer` detects delete key as backspace, but I had to split them up to avoid breaking changes in Ink. Merge them back together in the next major version.
- // delete
- key.name = 'delete';
+ // On macOS, \x7f is what the backspace key sends, not delete
+ // The actual forward delete sends escape sequences like \x1b[3~
+ key.name = 'backspace';
key.meta = s.charAt(0) === '\x1b';
}
else if (s === '\x1b' || s === '\x1b\x1b') {
+752 -23
View File
@@ -1,13 +1,73 @@
#!/usr/bin/env node
import chalk from 'chalk';
import { execSync } from 'child_process';
import { runTUI } from './tui.tsx';
import { loadSettings } from './utils/config';
import {
type StatusJSON,
renderStatusLine,
getTokenMetrics,
getSessionDuration,
type RenderContext
} from './utils/renderer';
import { loadSettings, type StatusItem } from './config';
import * as fs from 'fs';
import { promisify } from 'util';
// Ensure fs.promises compatibility for older Node versions
const readFile = fs.promises?.readFile || promisify(fs.readFile);
// Force chalk to use colors even when piped
chalk.level = 3;
// Helper function to apply foreground, background colors and bold
function applyColors(text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string {
let result = text;
// Standard color application
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
result = fgFunc(result);
}
}
if (backgroundColor && backgroundColor !== 'none') {
const bgFunc = (chalk as any)[backgroundColor];
if (bgFunc) {
result = bgFunc(result);
}
}
if (bold) {
result = chalk.bold(result);
}
return result;
}
interface StatusJSON {
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
version?: string;
}
interface TokenUsage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
}
interface TranscriptLine {
message?: {
usage?: TokenUsage;
};
isSidechain?: boolean;
timestamp?: string;
}
async function readStdin(): Promise<string | null> {
// Check if stdin is a TTY (terminal) - if it is, there's no piped data
@@ -38,11 +98,687 @@ async function readStdin(): Promise<string | null> {
}
}
async function renderMultipleLines(data: StatusJSON) {
// Helper to get default color for an item type
function getItemDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
function getTerminalWidth(): number | null {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}).trim();
// Check if we got a valid tty (not ?? which means no tty)
if (tty && tty !== '??' && tty !== '?') {
// Now get the terminal size
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
} catch {
// Command failed, width detection not available
}
// Fallback: try tput cols which might work in some environments
try {
const width = execSync('tput cols 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
} catch {
// tput also failed
}
return null;
}
function getGitBranch(): string | null {
try {
const branch = execSync('git branch --show-current 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return branch || null;
} catch {
return null;
}
}
function getGitChanges(): { insertions: number; deletions: number } | null {
try {
let totalInsertions = 0;
let totalDeletions = 0;
// Get unstaged changes
const unstagedStat = execSync('git diff --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Get staged changes
const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Parse unstaged changes
if (unstagedStat) {
const insertMatch = unstagedStat.match(/(\d+) insertion/);
const deleteMatch = unstagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Parse staged changes
if (stagedStat) {
const insertMatch = stagedStat.match(/(\d+) insertion/);
const deleteMatch = stagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Always return the changes, even if they're zero
return { insertions: totalInsertions, deletions: totalDeletions };
} catch {
return null;
}
}
async function getSessionDuration(transcriptPath: string): Promise<string | null> {
try {
if (!fs.existsSync(transcriptPath)) {
return null;
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n').filter(line => line.trim());
if (lines.length === 0) {
return null;
}
let firstTimestamp: Date | null = null;
let lastTimestamp: Date | null = null;
// Find first valid timestamp
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.timestamp) {
firstTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
// Find last valid timestamp (iterate backwards)
for (let i = lines.length - 1; i >= 0; i--) {
try {
const data = JSON.parse(lines[i]!);
if (data.timestamp) {
lastTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
if (!firstTimestamp || !lastTimestamp) {
return null;
}
// Calculate duration in milliseconds
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
// Convert to minutes
const totalMinutes = Math.floor(durationMs / (1000 * 60));
if (totalMinutes < 1) {
return '<1m';
}
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours === 0) {
return `${minutes}m`;
} else if (minutes === 0) {
return `${hours}hr`;
} else {
return `${hours}hr ${minutes}m`;
}
} catch {
return null;
}
}
async function getTokenMetrics(transcriptPath: string): Promise<{
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
contextLength: number;
}> {
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(transcriptPath)) {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
let inputTokens = 0;
let outputTokens = 0;
let cachedTokens = 0;
let contextLength = 0;
// Parse each line and sum up token usage for totals
let mostRecentMainChainEntry: TranscriptLine | null = null;
let mostRecentTimestamp: Date | null = null;
for (const line of lines) {
try {
const data: TranscriptLine = JSON.parse(line);
if (data.message?.usage) {
inputTokens += data.message.usage.input_tokens || 0;
outputTokens += data.message.usage.output_tokens || 0;
cachedTokens += data.message.usage.cache_read_input_tokens || 0;
cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
if (data.isSidechain !== true && data.timestamp) {
const entryTime = new Date(data.timestamp);
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
mostRecentTimestamp = entryTime;
mostRecentMainChainEntry = data;
}
}
}
} catch {
// Skip invalid JSON lines
}
}
// Calculate context length from the most recent main chain message
if (mostRecentMainChainEntry?.message?.usage) {
const usage = mostRecentMainChainEntry.message.usage;
contextLength = (usage.input_tokens || 0) +
(usage.cache_read_input_tokens || 0) +
(usage.cache_creation_input_tokens || 0);
}
const totalTokens = inputTokens + outputTokens + cachedTokens;
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
} catch {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
}
function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, tokenMetrics: any, sessionDuration: string | null): string {
// Helper to apply colors with optional background and bold override
const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
// Override foreground color takes precedence over EVERYTHING, including passed foreground color
let fgColor = foregroundColor;
if (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
fgColor = settings.overrideForegroundColor;
}
// Override background color takes precedence over EVERYTHING, including passed background color
let bgColor = backgroundColor;
if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
bgColor = settings.overrideBackgroundColor;
}
const shouldBold = settings.globalBold || bold;
return applyColors(text, fgColor, bgColor, shouldBold);
};
const detectedWidth = getTerminalWidth();
// Calculate terminal width based on flex mode settings
let terminalWidth: number | null = null;
if (detectedWidth) {
const flexMode = settings.flexMode || 'full-minus-40';
if (flexMode === 'full') {
// Use full width minus 4 for terminal padding
terminalWidth = detectedWidth - 4;
} else if (flexMode === 'full-minus-40') {
// Always subtract 40 for auto-compact message
terminalWidth = detectedWidth - 40;
} else if (flexMode === 'full-until-compact') {
// Check context percentage to decide
const threshold = settings.compactThreshold || 60;
const contextPercentage = tokenMetrics ?
Math.min(100, (tokenMetrics.contextLength / 200000) * 100) : 0;
if (contextPercentage >= threshold) {
// Context is high, leave space for auto-compact
terminalWidth = detectedWidth - 40;
} else {
// Context is low, use full width minus 4 for padding
terminalWidth = detectedWidth - 4;
}
}
}
const elements: { content: string, type: string, item?: StatusItem }[] = [];
let hasFlexSeparator = false;
// Helper function to format token counts
const formatTokens = (count: number): string => {
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
};
// Build elements based on configured items
for (const item of items) {
switch (item.type) {
case 'model':
if (data.model) {
const text = item.rawValue ? data.model.display_name : `Model: ${data.model.display_name}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item });
}
break;
case 'git-branch':
const branch = getGitBranch();
if (branch) {
const text = item.rawValue ? branch : `${branch}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item });
}
break;
case 'git-changes':
const changes = getGitChanges();
if (changes !== null) {
// Compact format: (+42,-10)
const changeStr = `(+${changes.insertions},-${changes.deletions})`;
elements.push({ content: applyColorsWithOverride(changeStr, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item });
}
break;
case 'tokens-input':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.inputTokens) : `In: ${formatTokens(tokenMetrics.inputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item });
}
break;
case 'tokens-output':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.outputTokens) : `Out: ${formatTokens(tokenMetrics.outputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item });
}
break;
case 'tokens-cached':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.cachedTokens) : `Cached: ${formatTokens(tokenMetrics.cachedTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item });
}
break;
case 'tokens-total':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.totalTokens) : `Total: ${formatTokens(tokenMetrics.totalTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item });
}
break;
case 'context-length':
if (tokenMetrics) {
const text = item.rawValue ? formatTokens(tokenMetrics.contextLength) : `Ctx: ${formatTokens(tokenMetrics.contextLength)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item });
}
break;
case 'context-percentage':
if (tokenMetrics) {
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item });
}
break;
case 'context-percentage-usable':
if (tokenMetrics) {
// Calculate percentage out of 160,000 (80% of full context for auto-compact)
const percentage = Math.min(100, (tokenMetrics.contextLength / 160000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
}
break;
case 'terminal-width':
const width = terminalWidth || getTerminalWidth();
if (width) {
const text = item.rawValue ? `${width}` : `Term: ${width}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
}
break;
case 'session-clock':
if (sessionDuration) {
const text = item.rawValue ? sessionDuration : `Session: ${sessionDuration}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item });
}
break;
case 'version':
const versionString = data.version || 'Unknown';
const versionText = item.rawValue ? versionString : `Version: ${versionString}`;
elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item });
break;
case 'separator':
// Only add separator if there are already elements and the last one isn't a separator
const lastElement = elements[elements.length - 1];
if (elements.length > 0 && lastElement && lastElement.type !== 'separator') {
const sepChar = item.character || '|';
// Handle special separator cases
let sepText;
if (sepChar === ',') {
sepText = `${sepChar} `;
} else if (sepChar === ' ') {
sepText = ' ';
} else {
sepText = ` ${sepChar} `;
}
// Use item color if specified, otherwise default to gray
const sepContent = applyColorsWithOverride(sepText, item.color || 'gray', item.backgroundColor, item.bold);
elements.push({ content: sepContent, type: 'separator', item });
}
break;
case 'flex-separator':
elements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customText = item.customText || '';
elements.push({ content: applyColorsWithOverride(customText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-text', item });
break;
case 'custom-command':
if (item.commandPath) {
try {
// Execute the command with JSON input via stdin
const timeout = item.timeout || 1000; // Default to 1000ms if not specified
const output = execSync(item.commandPath, {
encoding: 'utf8',
input: JSON.stringify(data),
stdio: ['pipe', 'pipe', 'ignore'],
timeout: timeout
}).trim();
if (output) {
let finalOutput = output;
// Handle max width truncation
if (item.maxWidth && item.maxWidth > 0) {
// Remove ANSI codes to measure actual length
const plainLength = output.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > item.maxWidth) {
// Truncate while preserving ANSI codes
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
for (let i = 0; i < output.length; i++) {
const char = output[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < item.maxWidth) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
finalOutput = truncated;
}
}
// Apply color if not preserving original colors
if (!item.preserveColors) {
// Strip existing ANSI codes and apply new color
const stripped = finalOutput.replace(/\x1b\[[0-9;]*m/g, '');
elements.push({ content: applyColorsWithOverride(stripped, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item });
} else {
// Preserve original colors from command output - ignore any color property
elements.push({ content: finalOutput, type: 'custom-command', item });
}
}
} catch {
// Command failed or timed out - silently skip
}
}
break;
}
}
if (elements.length === 0) return '';
// Remove trailing separators
while (elements.length > 0 && elements[elements.length - 1]?.type === 'separator') {
elements.pop();
}
// Apply default padding and separators
const finalElements: string[] = [];
const padding = settings.defaultPadding || '';
const defaultSep = settings.defaultSeparator || '';
elements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item)
if (defaultSep && index > 0) {
// Check if we should inherit colors from the previous element
if (settings.inheritSeparatorColors && index > 0) {
const prevElem = elements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (already handles override)
// Use the item's color if set, otherwise get the default color for that item type
const itemColor = prevElem.item.color || getItemDefaultColor(prevElem.item.type);
const coloredSep = applyColorsWithOverride(defaultSep, itemColor, prevElem.item.backgroundColor, prevElem.item.bold);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
} else if ((settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none')) {
// Apply override colors even when not inheriting colors
const coloredSep = applyColorsWithOverride(defaultSep, undefined, undefined);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
}
// Add element with padding (separators don't get padding)
if (elem.type === 'separator' || elem.type === 'flex-separator') {
finalElements.push(elem.content);
} else {
// Apply padding with colors (using overrides if set)
const hasColorOverride = (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none');
if (padding && (elem.item?.backgroundColor || hasColorOverride)) {
// Apply colors to padding - applyColorsWithOverride will handle the overrides
const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) +
elem.content +
applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor);
finalElements.push(paddedContent);
} else {
// No colors or no padding
finalElements.push(padding + elem.content + padding);
}
}
});
// Build the final status line
let statusLine = '';
if (hasFlexSeparator && terminalWidth) {
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
for (let i = 0; i < finalElements.length; i++) {
const elem = finalElements[i];
if (elem === 'FLEX' || (elements[i] && elements[i]?.type === 'flex-separator')) {
currentPart++;
parts[currentPart] = [];
} else {
parts[currentPart]?.push(elem!);
}
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
statusLine += part.join('');
}
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
}
} else {
// No flex separator OR no width detected
if (hasFlexSeparator && !terminalWidth) {
// Treat flex separators as normal separators when width detection fails
statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
} else {
// Just join all elements normally
statusLine = finalElements.join('');
}
}
// Truncate if the line exceeds the terminal width
if (detectedWidth && detectedWidth > 0) {
// Remove ANSI escape codes to get actual length
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > detectedWidth) {
// Need to truncate - preserve ANSI codes while truncating
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
const targetLength = detectedWidth - 7; // Truncate to width-7 for proper fit
for (let i = 0; i < statusLine.length; i++) {
const char = statusLine[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < targetLength) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
statusLine = truncated + '...';
}
}
return statusLine;
}
async function renderStatusLine(data: StatusJSON) {
const settings = await loadSettings();
// Get all lines to render (support both old items format and new lines format)
let lines = settings.lines || (settings.items ? [settings.items] : [[]]);
let lines: StatusItem[][] = [];
if (settings.lines) {
lines = settings.lines;
} else if (settings.items) {
// Legacy support for single line
lines = [settings.items];
} else {
lines = [[]];
}
// Get token metrics if needed (check all lines)
const hasTokenItems = lines.some(line =>
@@ -56,28 +792,21 @@ async function renderMultipleLines(data: StatusJSON) {
line.some(item => item.type === 'session-clock')
);
let tokenMetrics = null;
let tokenMetrics: any = null;
if (hasTokenItems && data.transcript_path) {
tokenMetrics = await getTokenMetrics(data.transcript_path);
}
let sessionDuration = null;
let sessionDuration: string | null = null;
if (hasSessionClock && data.transcript_path) {
sessionDuration = await getSessionDuration(data.transcript_path);
}
// Create render context
const context: RenderContext = {
data,
tokenMetrics,
sessionDuration,
isPreview: false
};
// Render each line
for (const lineItems of lines) {
for (let i = 0; i < lines.length; i++) {
const lineItems = lines[i];
if (lineItems && lineItems.length > 0) {
const line = renderStatusLine(lineItems, settings, context);
const line = renderSingleLine(lineItems, settings, data, tokenMetrics, sessionDuration);
// Replace all spaces with non-breaking spaces to prevent VSCode trimming
const outputLine = line.replace(/ /g, '\u00A0');
console.log(outputLine);
@@ -93,7 +822,7 @@ async function main() {
if (input && input.trim() !== '') {
try {
const data: StatusJSON = JSON.parse(input);
await renderMultipleLines(data);
await renderStatusLine(data);
} catch (error) {
console.error('Error parsing JSON:', error);
process.exit(1);
+400 -118
View File
@@ -3,16 +3,10 @@ import { render, Box, Text, useInput, useApp } from 'ink';
import SelectInput from 'ink-select-input';
import chalk from 'chalk';
import { execSync } from 'child_process';
import { loadSettings, saveSettings, type Settings, type StatusItem, type StatusItemType, type FlexMode } from './utils/config';
import { isInstalled, installStatusLine, uninstallStatusLine, getExistingStatusLine } from './utils/claude-settings';
import { loadSettings, saveSettings, type Settings, type StatusItem, type StatusItemType, type FlexMode } from './config';
import { isInstalled, installStatusLine, uninstallStatusLine, getExistingStatusLine } from './claude-settings';
import * as fs from 'fs';
import * as path from 'path';
import {
renderStatusLine as renderLine,
getItemDefaultColor,
applyColors,
type RenderContext
} from './utils/renderer';
// Get package version
function getPackageVersion(): string {
@@ -26,8 +20,27 @@ function getPackageVersion(): string {
}
// Get default color for each item type (matching ccstatusline.ts defaults)
// Re-export from shared renderer module
const getDefaultColor = getItemDefaultColor;
function getDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
// Check if terminal width detection is available
function canDetectTerminalWidth(): boolean {
@@ -79,31 +92,337 @@ interface StatusLinePreviewProps {
settings?: Settings;
}
const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetectionAvailable: boolean, settings?: Settings): string => {
// Create render context for preview
const context: RenderContext = {
terminalWidth,
isPreview: true
};
return renderLine(items, settings || {}, context);
// Helper function to apply foreground, background colors and bold
const applyColors = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
let result = text;
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
result = fgFunc(result);
}
}
if (backgroundColor && backgroundColor !== 'none') {
const bgFunc = (chalk as any)[backgroundColor];
if (bgFunc) {
result = bgFunc(result);
}
}
if (bold) {
result = chalk.bold(result);
}
return result;
};
const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetectionAvailable: boolean, settings?: Settings): string => {
// Helper to apply colors with optional background and foreground override
const applyItemColors = (text: string, item: StatusItem, defaultColor?: string): string => {
// Override foreground color takes precedence over EVERYTHING
let fgColor = item.color || defaultColor;
if (settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
fgColor = settings.overrideForegroundColor;
}
// Override background color takes precedence over EVERYTHING
let bgColor = item.backgroundColor;
if (settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
bgColor = settings.overrideBackgroundColor;
}
const shouldBold = settings?.globalBold || item.bold;
return applyColors(text, fgColor, bgColor, shouldBold);
};
// Calculate effective width based on flex mode settings
let effectiveWidth: number | null = null;
if (widthDetectionAvailable && terminalWidth) {
const flexMode = settings?.flexMode || 'full-minus-40';
if (flexMode === 'full') {
effectiveWidth = terminalWidth - 2; // Subtract 2 for terminal padding in preview
} else if (flexMode === 'full-minus-40') {
effectiveWidth = terminalWidth - 40;
} else if (flexMode === 'full-until-compact') {
// For preview, always show full width
effectiveWidth = terminalWidth - 2; // Subtract 2 for terminal padding in preview
}
}
// Always ensure we have a width for truncation
const width = terminalWidth; // Always use terminal width for truncation
const flexWidth = effectiveWidth; // Use this for flex separator calculations
const rawElements: { content: string; type: string; item?: StatusItem }[] = [];
let hasFlexSeparator = false;
items.forEach((item, index) => {
switch (item.type) {
case 'model':
const modelText = item.rawValue ? 'Claude' : 'Model: Claude';
rawElements.push({ content: applyItemColors(modelText, item, 'cyan'), type: 'model', item });
break;
case 'git-branch':
const branchText = '⎇ main';
rawElements.push({ content: applyItemColors(branchText, item, 'magenta'), type: 'git-branch', item });
break;
case 'git-changes':
const changesText = '(+42,-10)';
rawElements.push({ content: applyItemColors(changesText, item, 'yellow'), type: 'git-changes', item });
break;
case 'tokens-input':
const inputText = item.rawValue ? '15.2k' : 'In: 15.2k';
rawElements.push({ content: applyItemColors(inputText, item, 'blue'), type: 'tokens-input', item });
break;
case 'tokens-output':
const outputText = item.rawValue ? '3.4k' : 'Out: 3.4k';
rawElements.push({ content: applyItemColors(outputText, item, 'white'), type: 'tokens-output', item });
break;
case 'tokens-cached':
const cachedText = item.rawValue ? '12k' : 'Cached: 12k';
rawElements.push({ content: applyItemColors(cachedText, item, 'cyan'), type: 'tokens-cached', item });
break;
case 'tokens-total':
const totalText = item.rawValue ? '30.6k' : 'Total: 30.6k';
rawElements.push({ content: applyItemColors(totalText, item, 'cyan'), type: 'tokens-total', item });
break;
case 'context-length':
const ctxText = item.rawValue ? '18.6k' : 'Ctx: 18.6k';
rawElements.push({ content: applyItemColors(ctxText, item, 'gray'), type: 'context-length', item });
break;
case 'context-percentage':
const ctxPctText = item.rawValue ? '9.3%' : 'Ctx: 9.3%';
rawElements.push({ content: applyItemColors(ctxPctText, item, 'blue'), type: 'context-percentage', item });
break;
case 'context-percentage-usable':
const ctxUsableText = item.rawValue ? '11.6%' : 'Ctx(u): 11.6%';
rawElements.push({ content: applyItemColors(ctxUsableText, item, 'green'), type: 'context-percentage-usable', item });
break;
case 'session-clock':
const sessionText = item.rawValue ? '2hr 15m' : 'Session: 2hr 15m';
rawElements.push({ content: applyItemColors(sessionText, item, 'yellow'), type: 'session-clock', item });
break;
case 'version':
const versionText = item.rawValue ? '1.0.72' : 'Version: 1.0.72';
rawElements.push({ content: applyItemColors(versionText, item, 'green'), type: 'version', item });
break;
case 'terminal-width':
const detectedWidth = canDetectTerminalWidth() ? terminalWidth : '??';
const termText = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
rawElements.push({ content: applyItemColors(termText, item, 'gray'), type: 'terminal-width', item });
break;
case 'separator':
const sepChar = item.character || '|';
// Handle special separator cases
let sepContent;
if (sepChar === ',') {
sepContent = `${sepChar} `;
} else if (sepChar === ' ') {
sepContent = ' ';
} else {
sepContent = ` ${sepChar} `;
}
rawElements.push({ content: applyItemColors(sepContent, item, 'gray'), type: 'separator', item });
break;
case 'flex-separator':
rawElements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customText = item.customText || '';
rawElements.push({ content: applyItemColors(customText, item, 'white'), type: 'custom-text', item });
break;
case 'custom-command':
const cmdText = item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
// Only apply color if not preserving colors
if (!item.preserveColors) {
rawElements.push({ content: applyItemColors(cmdText, item, 'white'), type: 'custom-command', item });
} else {
// When preserving colors, just show the text without applying our colors
rawElements.push({ content: cmdText, type: 'custom-command', item });
}
break;
}
});
// Apply default padding and separators
const elements: string[] = [];
const padding = settings?.defaultPadding || '';
const defaultSep = settings?.defaultSeparator || '';
// Debug: Check if we have a default separator
// console.log('Default separator:', JSON.stringify(defaultSep), 'Length:', defaultSep.length);
rawElements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item)
if (defaultSep && index > 0) {
// Check if we should inherit colors from the previous element
if (settings?.inheritSeparatorColors && index > 0) {
const prevElem = rawElements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (with overrides)
const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none'
? settings.overrideForegroundColor
: (prevElem.item.color || getDefaultColor(prevElem.item.type));
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: prevElem.item.backgroundColor;
const shouldBold = settings?.globalBold || prevElem.item.bold;
const coloredSep = applyColors(defaultSep, fgColor, bgColor, shouldBold);
elements.push(coloredSep);
} else {
elements.push(defaultSep);
}
} else if (settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none' ||
settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
// Apply override colors even when not inheriting colors
const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none'
? settings.overrideForegroundColor
: undefined;
const coloredSep = applyColors(defaultSep, fgColor, settings.overrideBackgroundColor, settings?.globalBold);
elements.push(coloredSep);
} else {
elements.push(defaultSep);
}
}
// Add the element itself
if (elem.type === 'separator' || elem.type === 'flex-separator') {
elements.push(elem.content);
} else {
// Apply padding with the same colors as the item (or overrides)
const fgColor = settings?.overrideForegroundColor && settings.overrideForegroundColor !== 'none'
? settings.overrideForegroundColor
: undefined;
const bgColor = settings?.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none'
? settings.overrideBackgroundColor
: elem.item?.backgroundColor;
if (padding && (bgColor || fgColor)) {
// Apply colors to padding
const paddedContent = applyColors(padding, fgColor, bgColor, settings?.globalBold) +
elem.content +
applyColors(padding, fgColor, bgColor, settings?.globalBold);
elements.push(paddedContent);
} else {
// No colors or no padding
elements.push(padding + elem.content + padding);
}
}
});
// Build the status line with flex separator support
let statusLine = '';
if (hasFlexSeparator && flexWidth) {
const parts: string[][] = [[]];
let currentPart = 0;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item && item.type === 'flex-separator') {
currentPart++;
parts[currentPart] = [];
} else {
const element = elements[i];
if (element !== 'FLEX' && parts[currentPart]) {
parts[currentPart]!.push(element as string);
}
}
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, flexWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
statusLine += part.join('');
}
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
}
} else {
// No width detection available - treat flex separators as normal separators
statusLine = elements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
}
// Truncate if the line exceeds the maximum width
if (width && width > 0) {
// Remove ANSI escape codes to get actual length
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > width) {
// Need to truncate - preserve ANSI codes while truncating
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
const targetLength = width - 5; // Reserve 3 chars for ellipsis + 2 for proper fit in preview
for (let i = 0; i < statusLine.length; i++) {
const char = statusLine[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < targetLength) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
statusLine = truncated + '...';
}
}
return statusLine;
};
const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ lines, terminalWidth, settings }) => {
const widthDetectionAvailable = canDetectTerminalWidth();
// Build the Claude Code input box - account for ink's padding
const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76);
const topLine = chalk.gray('╭' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╮');
const middleLine = chalk.gray('│') + ' > ' + ' '.repeat(Math.max(0, boxWidth - 5)) + chalk.gray('│');
const bottomLine = chalk.gray('╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯');
// Render each configured line
// Pass the full terminal width - the renderer will handle preview adjustments
// Render each configured line - account for 2-space prefix in display
const availableWidth = boxWidth - 2; // Account for 2-space indent
const renderedLines = lines.map(lineItems =>
lineItems.length > 0 ? renderSingleLine(lineItems, terminalWidth, widthDetectionAvailable, settings) : ''
lineItems.length > 0 ? renderSingleLine(lineItems, availableWidth, widthDetectionAvailable, settings) : ''
).filter(line => line !== ''); // Remove empty lines
return (
<Box flexDirection='column'>
<Box borderStyle="round" borderColor="gray" width="100%" paddingLeft={1}>
<Text>&gt;</Text>
</Box>
<Text>{topLine}</Text>
<Text>{middleLine}</Text>
<Text>{bottomLine}</Text>
{renderedLines.map((line, index) => (
<Text key={index}> {line}</Text>
))}
@@ -199,7 +518,7 @@ const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasCha
const items = [
{ label: '📝 Edit Lines', value: 'lines' },
{ label: '🎨 Configure Colors', value: 'colors' },
{ label: '📏 Terminal Width Options', value: 'terminalWidth' },
{ label: '⚡ Flex Options', value: 'flex' },
{ label: '🔧 Global Options', value: 'globalOptions' },
{ label: isClaudeInstalled ? '🗑️ Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install' },
];
@@ -217,9 +536,9 @@ const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasCha
<Box flexDirection='column'>
<Text bold>Main Menu</Text>
<Box marginTop={1}>
<SelectInput
items={items}
onSelect={(item) => onSelect(item.value)}
<SelectInput
items={items}
onSelect={(item) => onSelect(item.value)}
initialIndex={Math.min(initialSelection, items.length - 1)}
/>
</Box>
@@ -278,15 +597,11 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
} else if (key.ctrl && key.rightArrow) {
// Move to end
setTextCursorPos(textInput.length);
} else if (key.backspace) {
} else if (key.backspace || key.delete) {
if (textCursorPos > 0) {
setTextInput(textInput.slice(0, textCursorPos - 1) + textInput.slice(textCursorPos));
setTextCursorPos(textCursorPos - 1);
}
} else if (key.delete) {
if (textCursorPos < textInput.length) {
setTextInput(textInput.slice(0, textCursorPos) + textInput.slice(textCursorPos + 1));
}
} else if (input && input.length === 1) {
setTextInput(textInput.slice(0, textCursorPos) + input + textInput.slice(textCursorPos));
setTextCursorPos(textCursorPos + 1);
@@ -319,15 +634,11 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
} else if (key.ctrl && key.rightArrow) {
// Move to end
setCommandCursorPos(commandInput.length);
} else if (key.backspace) {
} else if (key.backspace || key.delete) {
if (commandCursorPos > 0) {
setCommandInput(commandInput.slice(0, commandCursorPos - 1) + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos - 1);
}
} else if (key.delete) {
if (commandCursorPos < commandInput.length) {
setCommandInput(commandInput.slice(0, commandCursorPos) + commandInput.slice(commandCursorPos + 1));
}
} else if (input) {
setCommandInput(commandInput.slice(0, commandCursorPos) + input + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos + input.length);
@@ -355,10 +666,8 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
// Cancel editing
setEditingMaxWidth(false);
setMaxWidthInput('');
} else if (key.backspace) {
} else if (key.backspace || key.delete) {
setMaxWidthInput(maxWidthInput.slice(0, -1));
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
setMaxWidthInput(maxWidthInput + input);
}
@@ -385,10 +694,8 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
// Cancel editing
setEditingTimeout(false);
setTimeoutInput('');
} else if (key.backspace) {
} else if (key.backspace || key.delete) {
setTimeoutInput(timeoutInput.slice(0, -1));
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
setTimeoutInput(timeoutInput + input);
}
@@ -557,7 +864,7 @@ const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack, line
// Get the color for this item (use custom color if set, otherwise default)
const colorName = item.color || getDefaultColor(item.type);
const colorFunc = (chalk as any)[colorName] || chalk.white;
switch (item.type) {
case 'model':
return colorFunc('Model');
@@ -705,16 +1012,10 @@ interface ColorMenuProps {
}
const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
const [showSeparators, setShowSeparators] = useState(false);
const colorableItems = items.filter(item => {
// Include separators only if showSeparators is true
if (item.type === 'separator') {
return showSeparators;
}
return ['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) &&
!(item.type === 'custom-command' && item.preserveColors); // Exclude custom-command items with preserveColors
});
const colorableItems = items.filter(item =>
['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) &&
!(item.type === 'custom-command' && item.preserveColors) // Exclude custom-command items with preserveColors
);
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(colorableItems[0]?.id || null);
const [editingBackground, setEditingBackground] = useState(false);
@@ -727,7 +1028,7 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
onBack();
return;
}
// Normal keyboard handling when there are items
if (key.escape) {
if (editingBackground) {
@@ -735,20 +1036,6 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
} else {
onBack();
}
} else if (input === 's' || input === 'S') {
// Toggle show separators
setShowSeparators(!showSeparators);
// Reset highlight to first item if there are items
const newColorableItems = items.filter(item => {
if (item.type === 'separator') {
return !showSeparators; // Will be toggled
}
return ['model', 'git-branch', 'git-changes', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable', 'session-clock', 'terminal-width', 'version', 'custom-text', 'custom-command'].includes(item.type) &&
!(item.type === 'custom-command' && item.preserveColors);
});
if (newColorableItems.length > 0) {
setHighlightedItemId(newColorableItems[0]!.id);
}
} else if (input === 'f' || input === 'F') {
if (colorableItems.length > 0) {
setEditingBackground(!editingBackground);
@@ -812,7 +1099,6 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
case 'session-clock': return 'Session Clock';
case 'terminal-width': return 'Terminal Width';
case 'version': return 'Version';
case 'separator': return `Separator (${item.character || '|'})`;
case 'custom-text': return `Custom Text (${item.customText || 'Empty'})`;
case 'custom-command': {
const cmd = item.commandPath ? item.commandPath.substring(0, 20) + (item.commandPath.length > 20 ? '...' : '') : 'No command';
@@ -881,25 +1167,25 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright',
'magentaBright', 'cyanBright', 'whiteBright'];
// Background colors list (includes 'none' as first option)
// Note: chalk doesn't support bgDim, so we exclude it from background colors
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright'];
// Get current color for highlighted item
const selectedItem = highlightedItemId && highlightedItemId !== 'back'
? colorableItems.find(item => item.id === highlightedItemId)
const selectedItem = highlightedItemId && highlightedItemId !== 'back'
? colorableItems.find(item => item.id === highlightedItemId)
: null;
const currentColor = editingBackground
const currentColor = editingBackground
? (selectedItem?.backgroundColor || 'none')
: (selectedItem ? (selectedItem.color || getDefaultColor(selectedItem.type)) : 'white');
const colorList = editingBackground ? bgColors : colors;
const colorIndex = colorList.indexOf(currentColor);
const colorNumber = colorIndex === -1 ? (editingBackground ? 1 : 8) : colorIndex + 1;
let colorDisplay;
if (editingBackground) {
if (currentColor === 'none') {
@@ -917,7 +1203,6 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
<Box flexDirection='column'>
<Text bold>Configure Colors {editingBackground && chalk.yellow('[Background Mode]')}</Text>
<Text dimColor> to select, Enter to cycle {editingBackground ? 'background' : 'foreground'}, (f) to toggle bg/fg, (b)old, (r)eset, ESC to go back</Text>
<Text dimColor>(s)how separators: {showSeparators ? chalk.green('ON') : chalk.gray('OFF')}</Text>
{selectedItem && (
<Box marginTop={1}>
<Text>
@@ -936,19 +1221,21 @@ const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
</Box>
<Box marginTop={1} flexDirection='column'>
<Text color='yellow'> VSCode Users: </Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio"</Text>
<Text dimColor wrap='wrap'>(`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors.</Text>
<Text dimColor wrap='wrap'>You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
</Box>
</Box>
);
};
interface TerminalWidthOptionsProps {
interface FlexOptionsProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
const TerminalWidthOptions: React.FC<TerminalWidthOptionsProps> = ({ settings, onUpdate, onBack }) => {
const FlexOptions: React.FC<FlexOptionsProps> = ({ settings, onUpdate, onBack }) => {
const [selectedOption, setSelectedOption] = useState<FlexMode>(settings.flexMode || 'full-minus-40');
const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold || 60);
const [editingThreshold, setEditingThreshold] = useState(false);
@@ -980,11 +1267,9 @@ const TerminalWidthOptions: React.FC<TerminalWidthOptionsProps> = ({ settings, o
setThresholdInput(String(compactThreshold));
setEditingThreshold(false);
setValidationError(null);
} else if (key.backspace) {
} else if (key.backspace || key.delete) {
setThresholdInput(thresholdInput.slice(0, -1));
setValidationError(null);
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
const newValue = thresholdInput + input;
if (newValue.length <= 2) {
@@ -1007,7 +1292,7 @@ const TerminalWidthOptions: React.FC<TerminalWidthOptionsProps> = ({ settings, o
},
{
value: 'full-minus-40' as FlexMode,
label: 'Full width minus 40 (default)',
label: 'Full width minus 40',
description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.'
},
{
@@ -1045,9 +1330,8 @@ const TerminalWidthOptions: React.FC<TerminalWidthOptionsProps> = ({ settings, o
return (
<Box flexDirection='column'>
<Text bold>Terminal Width Options</Text>
<Text color="white">These settings affect where long lines are truncated, and where right-alignment occurs when using flex separators</Text>
<Text dimColor wrap="wrap">These settings are necessary because claude code does not currently provide an available width variable for the statusline and features like IDE integration, auto-compaction notices, and rate limit messages can all cause the statusline to wrap if we do not truncate it</Text>
<Text bold>Flex Separator Width Options</Text>
<Text dimColor>Select how flex separators calculate available width</Text>
{editingThreshold ? (
<Box marginTop={1} flexDirection='column'>
@@ -1109,13 +1393,13 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
const [separatorInput, setSeparatorInput] = useState(settings.defaultSeparator || '');
const [inheritColors, setInheritColors] = useState(settings.inheritSeparatorColors || false);
const [globalBold, setGlobalBold] = useState(settings.globalBold || false);
// Background color override
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
const bgColors = ['none', 'bgBlack', 'bgRed', 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta',
'bgCyan', 'bgWhite', 'bgGray', 'bgRedBright', 'bgGreenBright', 'bgYellowBright',
'bgBlueBright', 'bgMagentaBright', 'bgCyanBright', 'bgWhiteBright'];
const currentBgIndex = bgColors.indexOf(settings.overrideBackgroundColor || 'none');
// Foreground color override
const fgColors = ['none', 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright',
@@ -1134,10 +1418,8 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
} else if (key.escape) {
setPaddingInput(settings.defaultPadding || '');
setEditingPadding(false);
} else if (key.backspace) {
} else if (key.backspace || key.delete) {
setPaddingInput(paddingInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
setPaddingInput(paddingInput + input);
}
@@ -1152,10 +1434,8 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
} else if (key.escape) {
setSeparatorInput(settings.defaultSeparator || '');
setEditingSeparator(false);
} else if (key.backspace) {
} else if (key.backspace || key.delete) {
setSeparatorInput(separatorInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
setSeparatorInput(separatorInput + input);
}
@@ -1224,7 +1504,7 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
<Text bold>Global Options</Text>
<Text dimColor>Configure automatic padding and separators between items</Text>
<Box marginTop={1} />
{editingPadding ? (
<Box flexDirection="column">
<Box>
@@ -1248,25 +1528,25 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
<Text color="cyan">{settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'}</Text>
<Text dimColor> - Press (p) to edit</Text>
</Box>
<Box>
<Text>Default Separator: </Text>
<Text color="cyan">{settings.defaultSeparator ? `"${settings.defaultSeparator}"` : '(none)'}</Text>
<Text dimColor> - Press (s) to edit</Text>
</Box>
<Box>
<Text> Inherit Colors: </Text>
<Text color={inheritColors ? "green" : "red"}>{inheritColors ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (i) to toggle</Text>
</Box>
<Box>
<Text> Global Bold: </Text>
<Text color={globalBold ? "green" : "red"}>{globalBold ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (o) to toggle</Text>
</Box>
<Box>
<Text>Override BG Color: </Text>
{(() => {
@@ -1282,7 +1562,7 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
})()}
<Text dimColor> - (b) cycle, (c) clear</Text>
</Box>
<Box>
<Text>Override FG Color: </Text>
{(() => {
@@ -1297,11 +1577,11 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
})()}
<Text dimColor> - (f) cycle, (g) clear</Text>
</Box>
<Box marginTop={2}>
<Text dimColor>Press ESC to go back</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
<Text dimColor wrap='wrap'>
Note: These settings are applied during rendering and don't add items to your widget list.
@@ -1317,7 +1597,9 @@ const GlobalOptionsMenu: React.FC<GlobalOptionsMenuProps> = ({ settings, onUpdat
</Text>
<Box marginTop={1} flexDirection='column'>
<Text color='yellow'>⚠ VSCode Users: </Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal Integrated: Minimum Contrast Ratio"</Text>
<Text dimColor wrap='wrap'>(`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors.</Text>
<Text dimColor wrap='wrap'>You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
</Box>
</Box>
</>
@@ -1331,7 +1613,7 @@ const App: React.FC = () => {
const [settings, setSettings] = useState<Settings | null>(null);
const [originalSettings, setOriginalSettings] = useState<Settings | null>(null);
const [hasChanges, setHasChanges] = useState(false);
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'terminalWidth' | 'globalOptions' | 'confirm'>('main');
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colors' | 'flex' | 'globalOptions' | 'confirm'>('main');
const [selectedLine, setSelectedLine] = useState(0);
const [menuSelections, setMenuSelections] = useState<Record<string, number>>({});
const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise<void> } | null>(null);
@@ -1427,8 +1709,8 @@ const App: React.FC = () => {
case 'colors':
setScreen('colors');
break;
case 'terminalWidth':
setScreen('terminalWidth');
case 'flex':
setScreen('flex');
break;
case 'globalOptions':
setScreen('globalOptions');
@@ -1472,14 +1754,14 @@ const App: React.FC = () => {
<Box marginTop={2}>
{screen === 'main' && (
<MainMenu
<MainMenu
onSelect={(value) => {
// Only persist menu selection if not exiting
if (value !== 'save' && value !== 'exit') {
const menuMap: Record<string, number> = {
'lines': 0,
'colors': 1,
'terminalWidth': 2,
'flex': 2,
'globalOptions': 3,
'install': 4
};
@@ -1487,7 +1769,7 @@ const App: React.FC = () => {
}
handleMainMenuSelect(value);
}}
isClaudeInstalled={isClaudeInstalled}
isClaudeInstalled={isClaudeInstalled}
hasChanges={hasChanges}
initialSelection={menuSelections.main || 0}
/>
@@ -1549,14 +1831,14 @@ const App: React.FC = () => {
}}
/>
)}
{screen === 'terminalWidth' && (
<TerminalWidthOptions
{screen === 'flex' && (
<FlexOptions
settings={settings}
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={() => {
// Save that we came from 'terminalWidth' menu (index 2)
// Save that we came from 'flex' menu (index 2)
setMenuSelections({ ...menuSelections, main: 2 });
setScreen('main');
}}
-826
View File
@@ -1,826 +0,0 @@
import chalk from 'chalk';
import { execSync } from 'child_process';
import * as fs from 'fs';
import { promisify } from 'util';
import type { StatusItem } from './config';
// Ensure fs.promises compatibility for older Node versions
const readFile = fs.promises?.readFile || promisify(fs.readFile);
// Force chalk to use colors even when piped
chalk.level = 3;
export interface StatusJSON {
session_id: string;
transcript_path: string;
cwd: string;
model: {
id: string;
display_name: string;
};
workspace: {
current_dir: string;
project_dir: string;
};
version?: string;
}
export interface TokenUsage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
}
export interface TranscriptLine {
message?: {
usage?: TokenUsage;
};
isSidechain?: boolean;
timestamp?: string;
}
export interface TokenMetrics {
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
contextLength: number;
}
export interface RenderContext {
data?: StatusJSON;
tokenMetrics?: TokenMetrics | null;
sessionDuration?: string | null;
gitBranch?: string | null;
gitChanges?: { insertions: number; deletions: number } | null;
terminalWidth?: number | null;
isPreview?: boolean;
}
// Helper function to apply foreground, background colors and bold
export function applyColors(text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string {
let result = text;
// Standard color application
// Ignore 'dim' color - it causes issues with terminal rendering
if (foregroundColor && foregroundColor !== 'dim') {
const fgFunc = (chalk as any)[foregroundColor];
if (fgFunc) {
result = fgFunc(result);
}
}
if (backgroundColor && backgroundColor !== 'none') {
const bgFunc = (chalk as any)[backgroundColor];
if (bgFunc) {
result = bgFunc(result);
}
}
if (bold) {
result = chalk.bold(result);
}
return result;
}
// Helper to get default color for an item type
export function getItemDefaultColor(type: string): string {
switch (type) {
case 'model': return 'cyan';
case 'git-branch': return 'magenta';
case 'git-changes': return 'yellow';
case 'session-clock': return 'yellow';
case 'version': return 'green';
case 'tokens-input': return 'blue';
case 'tokens-output': return 'white';
case 'tokens-cached': return 'cyan';
case 'tokens-total': return 'cyan';
case 'context-length': return 'gray';
case 'context-percentage': return 'blue';
case 'context-percentage-usable': return 'green';
case 'terminal-width': return 'gray';
case 'custom-text': return 'white';
case 'custom-command': return 'white';
case 'separator': return 'gray';
default: return 'white';
}
}
// Helper function to format token counts
export function formatTokens(count: number): string {
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
return count.toString();
}
export function getTerminalWidth(): number | null {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}).trim();
// Check if we got a valid tty (not ?? which means no tty)
if (tty && tty !== '??' && tty !== '?') {
// Now get the terminal size
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
} catch {
// Command failed, width detection not available
}
// Fallback: try tput cols which might work in some environments
try {
const width = execSync('tput cols 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
} catch {
// tput also failed
}
return null;
}
export function getGitBranch(): string | null {
try {
const branch = execSync('git branch --show-current 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return branch || null;
} catch {
return null;
}
}
export function getGitChanges(): { insertions: number; deletions: number } | null {
try {
let totalInsertions = 0;
let totalDeletions = 0;
// Get unstaged changes
const unstagedStat = execSync('git diff --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Get staged changes
const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Parse unstaged changes
if (unstagedStat) {
const insertMatch = unstagedStat.match(/(\d+) insertion/);
const deleteMatch = unstagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Parse staged changes
if (stagedStat) {
const insertMatch = stagedStat.match(/(\d+) insertion/);
const deleteMatch = stagedStat.match(/(\d+) deletion/);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
// Always return the changes, even if they're zero
return { insertions: totalInsertions, deletions: totalDeletions };
} catch {
return null;
}
}
export async function getSessionDuration(transcriptPath: string): Promise<string | null> {
try {
if (!fs.existsSync(transcriptPath)) {
return null;
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n').filter(line => line.trim());
if (lines.length === 0) {
return null;
}
let firstTimestamp: Date | null = null;
let lastTimestamp: Date | null = null;
// Find first valid timestamp
for (const line of lines) {
try {
const data = JSON.parse(line);
if (data.timestamp) {
firstTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
// Find last valid timestamp (iterate backwards)
for (let i = lines.length - 1; i >= 0; i--) {
try {
const data = JSON.parse(lines[i]!);
if (data.timestamp) {
lastTimestamp = new Date(data.timestamp);
break;
}
} catch {
// Skip invalid lines
}
}
if (!firstTimestamp || !lastTimestamp) {
return null;
}
// Calculate duration in milliseconds
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
// Convert to minutes
const totalMinutes = Math.floor(durationMs / (1000 * 60));
if (totalMinutes < 1) {
return '<1m';
}
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours === 0) {
return `${minutes}m`;
} else if (minutes === 0) {
return `${hours}hr`;
} else {
return `${hours}hr ${minutes}m`;
}
} catch {
return null;
}
}
export async function getTokenMetrics(transcriptPath: string): Promise<TokenMetrics> {
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(transcriptPath)) {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
const content = await readFile(transcriptPath, 'utf-8');
const lines = content.trim().split('\n');
let inputTokens = 0;
let outputTokens = 0;
let cachedTokens = 0;
let contextLength = 0;
// Parse each line and sum up token usage for totals
let mostRecentMainChainEntry: TranscriptLine | null = null;
let mostRecentTimestamp: Date | null = null;
for (const line of lines) {
try {
const data: TranscriptLine = JSON.parse(line);
if (data.message?.usage) {
inputTokens += data.message.usage.input_tokens || 0;
outputTokens += data.message.usage.output_tokens || 0;
cachedTokens += data.message.usage.cache_read_input_tokens || 0;
cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
// Track the most recent entry with isSidechain: false (or undefined, which defaults to main chain)
if (data.isSidechain !== true && data.timestamp) {
const entryTime = new Date(data.timestamp);
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
mostRecentTimestamp = entryTime;
mostRecentMainChainEntry = data;
}
}
}
} catch {
// Skip invalid JSON lines
}
}
// Calculate context length from the most recent main chain message
if (mostRecentMainChainEntry?.message?.usage) {
const usage = mostRecentMainChainEntry.message.usage;
contextLength = (usage.input_tokens || 0) +
(usage.cache_read_input_tokens || 0) +
(usage.cache_creation_input_tokens || 0);
}
const totalTokens = inputTokens + outputTokens + cachedTokens;
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
} catch {
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
}
}
export function renderStatusLine(
items: StatusItem[],
settings: any,
context: RenderContext
): string {
// Helper to apply colors with optional background and bold override
const applyColorsWithOverride = (text: string, foregroundColor?: string, backgroundColor?: string, bold?: boolean): string => {
// Override foreground color takes precedence over EVERYTHING, including passed foreground color
let fgColor = foregroundColor;
if (settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none') {
fgColor = settings.overrideForegroundColor;
}
// Override background color takes precedence over EVERYTHING, including passed background color
let bgColor = backgroundColor;
if (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') {
bgColor = settings.overrideBackgroundColor;
}
const shouldBold = settings.globalBold || bold;
return applyColors(text, fgColor, bgColor, shouldBold);
};
const detectedWidth = context.terminalWidth || getTerminalWidth();
// Calculate terminal width based on flex mode settings
let terminalWidth: number | null = null;
if (detectedWidth) {
const flexMode = settings.flexMode || 'full-minus-40';
if (context.isPreview) {
// In preview mode, account for box borders and padding (6 chars total)
if (flexMode === 'full') {
terminalWidth = detectedWidth - 6; // Subtract 6 for box borders and padding in preview
} else if (flexMode === 'full-minus-40') {
terminalWidth = detectedWidth - 43; // -40 for auto-compact + 3 for preview
} else if (flexMode === 'full-until-compact') {
// For preview, always show full width minus preview padding
terminalWidth = detectedWidth - 6;
}
} else {
// In actual rendering mode
if (flexMode === 'full') {
// Use full width minus 4 for terminal padding
terminalWidth = detectedWidth - 4;
} else if (flexMode === 'full-minus-40') {
// Always subtract 41 for auto-compact message
terminalWidth = detectedWidth - 41;
} else if (flexMode === 'full-until-compact') {
// Check context percentage to decide
const threshold = settings.compactThreshold || 60;
const contextPercentage = context.tokenMetrics ?
Math.min(100, (context.tokenMetrics.contextLength / 200000) * 100) : 0;
if (contextPercentage >= threshold) {
// Context is high, leave space for auto-compact
terminalWidth = detectedWidth - 40;
} else {
// Context is low, use full width minus 4 for padding
terminalWidth = detectedWidth - 4;
}
}
}
}
const elements: { content: string, type: string, item?: StatusItem }[] = [];
let hasFlexSeparator = false;
// Build elements based on configured items
for (const item of items) {
switch (item.type) {
case 'model':
if (context.isPreview) {
const modelText = item.rawValue ? 'Claude' : 'Model: Claude';
elements.push({ content: applyColorsWithOverride(modelText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item });
} else if (context.data?.model) {
const text = item.rawValue ? context.data.model.display_name : `Model: ${context.data.model.display_name}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'model', item });
}
break;
case 'git-branch':
if (context.isPreview) {
const branchText = item.rawValue ? 'main' : '⎇ main';
elements.push({ content: applyColorsWithOverride(branchText, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item });
} else {
const branch = context.gitBranch || getGitBranch();
if (branch) {
const text = item.rawValue ? branch : `${branch}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'magenta', item.backgroundColor, item.bold), type: 'git-branch', item });
}
}
break;
case 'git-changes':
if (context.isPreview) {
const changesText = '(+42,-10)';
elements.push({ content: applyColorsWithOverride(changesText, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item });
} else {
const changes = context.gitChanges || getGitChanges();
if (changes !== null) {
const changeStr = `(+${changes.insertions},-${changes.deletions})`;
elements.push({ content: applyColorsWithOverride(changeStr, item.color || 'yellow', item.backgroundColor, item.bold), type: 'git-changes', item });
}
}
break;
case 'tokens-input':
if (context.isPreview) {
const inputText = item.rawValue ? '15.2k' : 'In: 15.2k';
elements.push({ content: applyColorsWithOverride(inputText, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.inputTokens) : `In: ${formatTokens(context.tokenMetrics.inputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'tokens-input', item });
}
break;
case 'tokens-output':
if (context.isPreview) {
const outputText = item.rawValue ? '3.4k' : 'Out: 3.4k';
elements.push({ content: applyColorsWithOverride(outputText, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.outputTokens) : `Out: ${formatTokens(context.tokenMetrics.outputTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'white', item.backgroundColor, item.bold), type: 'tokens-output', item });
}
break;
case 'tokens-cached':
if (context.isPreview) {
const cachedText = item.rawValue ? '12k' : 'Cached: 12k';
elements.push({ content: applyColorsWithOverride(cachedText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.cachedTokens) : `Cached: ${formatTokens(context.tokenMetrics.cachedTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-cached', item });
}
break;
case 'tokens-total':
if (context.isPreview) {
const totalText = item.rawValue ? '30.6k' : 'Total: 30.6k';
elements.push({ content: applyColorsWithOverride(totalText, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.totalTokens) : `Total: ${formatTokens(context.tokenMetrics.totalTokens)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'cyan', item.backgroundColor, item.bold), type: 'tokens-total', item });
}
break;
case 'context-length':
if (context.isPreview) {
const ctxText = item.rawValue ? '18.6k' : 'Ctx: 18.6k';
elements.push({ content: applyColorsWithOverride(ctxText, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item });
} else if (context.tokenMetrics) {
const text = item.rawValue ? formatTokens(context.tokenMetrics.contextLength) : `Ctx: ${formatTokens(context.tokenMetrics.contextLength)}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'context-length', item });
}
break;
case 'context-percentage':
if (context.isPreview) {
const ctxPctText = item.rawValue ? '9.3%' : 'Ctx: 9.3%';
elements.push({ content: applyColorsWithOverride(ctxPctText, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item });
} else if (context.tokenMetrics) {
const percentage = Math.min(100, (context.tokenMetrics.contextLength / 200000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'blue', item.backgroundColor, item.bold), type: 'context-percentage', item });
}
break;
case 'context-percentage-usable':
if (context.isPreview) {
const ctxUsableText = item.rawValue ? '11.6%' : 'Ctx(u): 11.6%';
elements.push({ content: applyColorsWithOverride(ctxUsableText, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
} else if (context.tokenMetrics) {
// Calculate percentage out of 160,000 (80% of full context for auto-compact)
const percentage = Math.min(100, (context.tokenMetrics.contextLength / 160000) * 100);
const text = item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'green', item.backgroundColor, item.bold), type: 'context-percentage-usable', item });
}
break;
case 'terminal-width':
const width = terminalWidth || getTerminalWidth();
if (context.isPreview) {
const detectedWidth = width || '??';
const termText = item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
elements.push({ content: applyColorsWithOverride(termText, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
} else if (width) {
const text = item.rawValue ? `${width}` : `Term: ${width}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'gray', item.backgroundColor, item.bold), type: 'terminal-width', item });
}
break;
case 'session-clock':
if (context.isPreview) {
const sessionText = item.rawValue ? '2hr 15m' : 'Session: 2hr 15m';
elements.push({ content: applyColorsWithOverride(sessionText, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item });
} else if (context.sessionDuration) {
const text = item.rawValue ? context.sessionDuration : `Session: ${context.sessionDuration}`;
elements.push({ content: applyColorsWithOverride(text, item.color || 'yellow', item.backgroundColor, item.bold), type: 'session-clock', item });
}
break;
case 'version':
if (context.isPreview) {
const versionText = item.rawValue ? '1.0.72' : 'Version: 1.0.72';
elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item });
} else if (context.data?.version) {
const versionString = context.data.version || 'Unknown';
const versionText = item.rawValue ? versionString : `Version: ${versionString}`;
elements.push({ content: applyColorsWithOverride(versionText, item.color || 'green', item.backgroundColor, item.bold), type: 'version', item });
}
break;
case 'separator':
// Always add separators - users should be able to add multiple if they want
const sepChar = item.character || '|';
// Handle special separator cases
let sepText;
if (sepChar === ',') {
sepText = `${sepChar} `;
} else if (sepChar === ' ') {
sepText = ' ';
} else {
sepText = ` ${sepChar} `;
}
// Use item color if specified, otherwise default to gray
const sepContent = applyColorsWithOverride(sepText, item.color || 'gray', item.backgroundColor, item.bold);
elements.push({ content: sepContent, type: 'separator', item });
break;
case 'flex-separator':
elements.push({ content: 'FLEX', type: 'flex-separator', item });
hasFlexSeparator = true;
break;
case 'custom-text':
const customText = item.customText || '';
elements.push({ content: applyColorsWithOverride(customText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-text', item });
break;
case 'custom-command':
if (context.isPreview) {
const cmdText = item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
// Only apply color if not preserving colors
if (!item.preserveColors) {
elements.push({ content: applyColorsWithOverride(cmdText, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item });
} else {
// When preserving colors, just show the text without applying our colors
elements.push({ content: cmdText, type: 'custom-command', item });
}
} else if (item.commandPath && context.data) {
try {
// Execute the command with JSON input via stdin
const timeout = item.timeout || 1000; // Default to 1000ms if not specified
const output = execSync(item.commandPath, {
encoding: 'utf8',
input: JSON.stringify(context.data),
stdio: ['pipe', 'pipe', 'ignore'],
timeout: timeout
}).trim();
if (output) {
let finalOutput = output;
// Handle max width truncation
if (item.maxWidth && item.maxWidth > 0) {
// Remove ANSI codes to measure actual length
const plainLength = output.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > item.maxWidth) {
// Truncate while preserving ANSI codes
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
for (let i = 0; i < output.length; i++) {
const char = output[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < item.maxWidth) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
finalOutput = truncated;
}
}
// Apply color if not preserving original colors
if (!item.preserveColors) {
// Strip existing ANSI codes and apply new color
const stripped = finalOutput.replace(/\x1b\[[0-9;]*m/g, '');
elements.push({ content: applyColorsWithOverride(stripped, item.color || 'white', item.backgroundColor, item.bold), type: 'custom-command', item });
} else {
// Preserve original colors from command output - ignore any color property
elements.push({ content: finalOutput, type: 'custom-command', item });
}
}
} catch {
// Command failed or timed out - silently skip
}
}
break;
}
}
if (elements.length === 0) return '';
// Remove trailing separators
while (elements.length > 0 && elements[elements.length - 1]?.type === 'separator') {
elements.pop();
}
// Apply default padding and separators
const finalElements: string[] = [];
const padding = settings.defaultPadding || '';
const defaultSep = settings.defaultSeparator || '';
elements.forEach((elem, index) => {
// Add default separator between any two items (but not before first item, and not around flex separators)
const prevElem = index > 0 ? elements[index - 1] : null;
const shouldAddSeparator = defaultSep && index > 0 &&
elem.type !== 'flex-separator' &&
prevElem?.type !== 'flex-separator';
if (shouldAddSeparator) {
// Check if we should inherit colors from the previous element
if (settings.inheritSeparatorColors && index > 0) {
const prevElem = elements[index - 1];
if (prevElem && prevElem.item) {
// Apply the previous element's colors to the separator (already handles override)
// Use the item's color if set, otherwise get the default color for that item type
const itemColor = prevElem.item.color || getItemDefaultColor(prevElem.item.type);
const coloredSep = applyColorsWithOverride(defaultSep, itemColor, prevElem.item.backgroundColor, prevElem.item.bold);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
} else if ((settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none')) {
// Apply override colors even when not inheriting colors
const coloredSep = applyColorsWithOverride(defaultSep, undefined, undefined);
finalElements.push(coloredSep);
} else {
finalElements.push(defaultSep);
}
}
// Add element with padding (separators don't get padding)
if (elem.type === 'separator' || elem.type === 'flex-separator') {
finalElements.push(elem.content);
} else {
// Apply padding with colors (using overrides if set)
const hasColorOverride = (settings.overrideBackgroundColor && settings.overrideBackgroundColor !== 'none') ||
(settings.overrideForegroundColor && settings.overrideForegroundColor !== 'none');
if (padding && (elem.item?.backgroundColor || hasColorOverride)) {
// Apply colors to padding - applyColorsWithOverride will handle the overrides
const paddedContent = applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor) +
elem.content +
applyColorsWithOverride(padding, undefined, elem.item?.backgroundColor);
finalElements.push(paddedContent);
} else {
// No colors or no padding
finalElements.push(padding + elem.content + padding);
}
}
});
// Build the final status line
let statusLine = '';
if (hasFlexSeparator && terminalWidth) {
// Split elements by flex separators
const parts: string[][] = [[]];
let currentPart = 0;
for (let i = 0; i < finalElements.length; i++) {
const elem = finalElements[i];
if (elem === 'FLEX') {
currentPart++;
parts[currentPart] = [];
} else {
parts[currentPart]?.push(elem!);
}
}
// Calculate total length of all non-flex content
const partLengths = parts.map(part => {
const joined = part.join('');
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
});
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
// Calculate space to distribute among flex separators
const flexCount = parts.length - 1; // Number of flex separators
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
// Build the status line with distributed spacing
statusLine = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part) {
statusLine += part.join('');
}
if (i < parts.length - 1) {
// Add flex spacing
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
statusLine += ' '.repeat(spaces);
}
}
} else {
// No flex separator OR no width detected
if (hasFlexSeparator && !terminalWidth) {
// Treat flex separators as normal separators when width detection fails
statusLine = finalElements.map(e => e === 'FLEX' ? chalk.gray(' | ') : e).join('');
} else {
// Just join all elements normally
statusLine = finalElements.join('');
}
}
// Truncate if the line exceeds the terminal width
// Use terminalWidth if available (already accounts for flex mode adjustments), otherwise use detectedWidth
const maxWidth = terminalWidth || detectedWidth;
if (maxWidth && maxWidth > 0) {
// Remove ANSI escape codes to get actual length
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
if (plainLength > maxWidth) {
// Need to truncate - preserve ANSI codes while truncating
let truncated = '';
let currentLength = 0;
let inAnsiCode = false;
let ansiBuffer = '';
const targetLength = context.isPreview ? maxWidth - 3 : maxWidth - 3; // Reserve 3 chars for ellipsis
for (let i = 0; i < statusLine.length; i++) {
const char = statusLine[i];
if (char === '\x1b') {
inAnsiCode = true;
ansiBuffer = char;
} else if (inAnsiCode) {
ansiBuffer += char;
if (char === 'm') {
truncated += ansiBuffer;
inAnsiCode = false;
ansiBuffer = '';
}
} else {
if (currentLength < targetLength) {
truncated += char;
currentLength++;
} else {
break;
}
}
}
statusLine = truncated + '...';
}
}
return statusLine;
}