diff --git a/README.md b/README.md index 06f5438..3f53115 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ -# Curse πŸ•―οΈ - -![Made with TypeScript](https://img.shields.io/badge/Made%20with-TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) -![Runs on Bun](https://img.shields.io/badge/Runs%20on-Bun-f9f1e1?style=flat-square&logo=bun&logoColor=black) -![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=flat-square) -![Females Attracted](https://img.shields.io/badge/Females%20Attracted-0-red?style=flat-square) +# Curse Curse is a dead simple Terminal UI for running processes, configured through a single `curse.toml` file. -γ‚Ήγ‚―γƒͺγƒΌγƒ³γ‚·γƒ§γƒƒγƒˆ 2025-09-27 午後7 49 04 +Curse TUI screenshot ## Installation & Setup diff --git a/bun.lock b/bun.lock index d70fae6..cb422e0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,11 +5,12 @@ "": { "name": "curse", "dependencies": { + "@types/react": "19.1.5", "arktype": "^2.1.22", "gunshi": "^0.26.3", - "ink": "^6.6.0", + "ink": "6.6.0", "minisearch": "^7.1.2", - "react": "^19.1.1", + "react": "19.1.1", "strip-ansi": "^7.1.2", "toml": "^3.0.0", }, @@ -25,7 +26,7 @@ }, }, "packages": { - "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA=="], + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], "@ark/schema": ["@ark/schema@0.49.0", "", { "dependencies": { "@ark/util": "0.49.0" } }, "sha512-GphZBLpW72iS0v4YkeUtV3YIno35Gimd7+ezbPO9GwEi9kzdUrPVjvf6aXSBAfHikaFc/9pqZOpv3pOXnC71tw=="], @@ -111,9 +112,9 @@ "@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], - "@types/react": ["@types/react@19.1.14", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-ukd93VGzaNPMAUPy0gRDSC57UuQbnH9Kussp7HBjM06YFi9uZTFhOvMSO2OKqXm1rSgzOE+pVx1k1PYHGwlc8Q=="], + "@types/react": ["@types/react@19.1.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g=="], - "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -133,23 +134,23 @@ "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], - "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], + "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - "es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="], + "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "gunshi": ["gunshi@0.26.3", "", { "dependencies": { "args-tokens": "^0.20.1" } }, "sha512-x/CaxovzDA3KMkrjqow7M1dnsO1CxGMNabBaeGTGdVNtjOMN1G/eK7vjKeJ+zHdFIsWHmo47YQ2DQIESUut2+A=="], @@ -191,7 +192,7 @@ "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -213,7 +214,11 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "ink/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=="], + "cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + + "ink/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + + "is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/curse.png b/curse.png new file mode 100644 index 0000000..faf537f Binary files /dev/null and b/curse.png differ diff --git a/package.json b/package.json index 5d25ad8..6a5e57d 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ "type": "module", "module": "src/index.ts", "dependencies": { + "@types/react": "19.1.5", "arktype": "^2.1.22", "gunshi": "^0.26.3", - "ink": "^6.6.0", + "ink": "6.6.0", "minisearch": "^7.1.2", - "react": "^19.1.1", + "react": "19.1.1", "strip-ansi": "^7.1.2", "toml": "^3.0.0" }, diff --git a/src/lib/Colors.ts b/src/lib/Colors.ts index 9f18374..eadacdc 100644 --- a/src/lib/Colors.ts +++ b/src/lib/Colors.ts @@ -1,11 +1,12 @@ export const Colors = { - primary: "#8b5cf6", // violet + white: "#ffffff", darkGray: "#4b5563", mutedCyan: "#5f8a8b", - indigo: "#6366f1", - purple: "#a78bfa", - green: "#10b981", - brightGreen: "#34d399", - brightPink: "#ec4899", - brightOrange: "#f97316", + gray: "#6b7280", + lightGray: "#e5e7eb", + mutedGreen: "#6b9e7e", + softGreen: "#7dab8e", + silver: "#9ca3af", + amber: "#d4a054", + mutedRed: "#b07070", }; diff --git a/src/ui/View.tsx b/src/ui/View.tsx index 88f0e99..fd73c40 100644 --- a/src/ui/View.tsx +++ b/src/ui/View.tsx @@ -66,19 +66,17 @@ function View(props: { config: CurseConfig }) { {displayMode === "normal" && ( - - Curse πŸ•― + + Curse v{version} - {status === ProgramStatus.Quitting && ( - Quitting... - )} + {status === ProgramStatus.Quitting && Quitting...} )} {displayMode === "normal" && ( - Config: + Config: {props.config.fileName} )} diff --git a/src/ui/components/AggregatedSummary.tsx b/src/ui/components/AggregatedSummary.tsx index 04b2f5c..049e435 100644 --- a/src/ui/components/AggregatedSummary.tsx +++ b/src/ui/components/AggregatedSummary.tsx @@ -5,13 +5,13 @@ import { useRenderTick } from "../../hooks/useRenderTick"; import { Colors } from "../../lib/Colors"; const statusColorMap: Record = { - running: Colors.green, - starting: Colors.indigo, + running: Colors.mutedGreen, + starting: Colors.gray, pending: Colors.darkGray, - error: "red", + error: Colors.mutedRed, success: Colors.darkGray, - killing: Colors.brightOrange, - killed: Colors.brightOrange, + killing: Colors.amber, + killed: Colors.amber, }; export function AggregatedSummary(props: { numberPrefix: string; waitingForSecondG: boolean }) { @@ -65,12 +65,12 @@ export function AggregatedSummary(props: { numberPrefix: string; waitingForSecon )} - + {" "} {selectedDetail}{" "} - {props.numberPrefix && [{props.numberPrefix}]} - {props.waitingForSecondG && [g]} + {props.numberPrefix && [{props.numberPrefix}]} + {props.waitingForSecondG && [g]} ); diff --git a/src/ui/components/LogTailPreview.tsx b/src/ui/components/LogTailPreview.tsx index cf7667a..3385d77 100644 --- a/src/ui/components/LogTailPreview.tsx +++ b/src/ui/components/LogTailPreview.tsx @@ -46,7 +46,10 @@ export function LogTailPreview(props: { height: number }) { ) : ( logs.map((log: string, index: number) => ( - + {preprocessLog(log)} diff --git a/src/ui/views/LogPage.tsx b/src/ui/views/LogPage.tsx index 7051947..8082091 100644 --- a/src/ui/views/LogPage.tsx +++ b/src/ui/views/LogPage.tsx @@ -588,9 +588,9 @@ function LogTable(props: { // Helper functions for text colors const getTextColor = (isCursor: boolean, isSelected: boolean, log: string) => { if (isCursor) return "white"; - if (isSelected) return Colors.purple; - if (log.includes("stderr")) return "red"; - return Colors.purple; + if (isSelected) return Colors.lightGray; + if (log.includes("stderr")) return Colors.mutedRed; + return Colors.lightGray; }; const getPartTextColor = ( @@ -599,7 +599,7 @@ function LogTable(props: { isSelected: boolean, log: string, ) => { - if (isHighlight) return Colors.brightOrange; + if (isHighlight) return Colors.amber; return getTextColor(isCursor, isSelected, log); }; @@ -614,20 +614,18 @@ function LogTable(props: { Autoscroll: - + {autoScroll ? "on" : "off"} - {positionLost && ( - (position lost, returned to tail) - )} - {numberPrefix && [{numberPrefix}]} - {waitingForSecondG && [g]} - {isSelectMode && [SELECT]} - {isSearchMode && </{searchQuery}>} + {positionLost && (position lost, returned to tail)} + {numberPrefix && [{numberPrefix}]} + {waitingForSecondG && [g]} + {isSelectMode && [SELECT]} + {isSearchMode && </{searchQuery}>} {!isSearchMode && appliedSearchQuery && ( - </{appliedSearchQuery}> + </{appliedSearchQuery}> )} - {showCopyIndicator && βœ“ {copyIndicatorText}} + {showCopyIndicator && βœ“ {copyIndicatorText}} {logs.map((log, index) => { @@ -637,7 +635,7 @@ function LogTable(props: { // Determine background color: cursor takes priority, then selection let backgroundColor; if (isCursor) { - backgroundColor = Colors.indigo; // Cursor color + backgroundColor = Colors.gray; // Cursor color } else if (isSelected) { backgroundColor = Colors.darkGray; // Charcoal for selection } @@ -800,23 +798,23 @@ export function LogPage() { <> - Logs - ( - + Logs + ( + {selectedProcess.name} - ) - [ - tail - ] + ) + [ + tail + ] {isSearchMode && ( - Search: + Search: {searchQuery} - β–ˆ + β–ˆ )} diff --git a/src/ui/views/MainPage.tsx b/src/ui/views/MainPage.tsx index 4280ffe..34a155a 100644 --- a/src/ui/views/MainPage.tsx +++ b/src/ui/views/MainPage.tsx @@ -33,12 +33,12 @@ function getReadinessDisplay(process: Process): { const color = char === "x" || char === "βœ—" - ? "red" + ? Colors.mutedRed : char === "βœ“" - ? Colors.green + ? Colors.mutedGreen : char === "-" ? Colors.darkGray - : Colors.indigo; + : Colors.gray; return { char, color }; } @@ -76,8 +76,8 @@ function ProcessTable(props: { READY - {props.numberPrefix && [{props.numberPrefix}]} - {props.waitingForSecondG && [g]} + {props.numberPrefix && [{props.numberPrefix}]} + {props.waitingForSecondG && [g]} {processes.map((process: Process, index: number) => { @@ -93,13 +93,13 @@ function ProcessTable(props: { ? Colors.darkGray : isShutdownHookPending ? Colors.mutedCyan - : Colors.indigo; + : Colors.gray; return ( @@ -157,8 +157,8 @@ function ProcessTable(props: { CPU - {props.numberPrefix && [{props.numberPrefix}]} - {props.waitingForSecondG && [g]} + {props.numberPrefix && [{props.numberPrefix}]} + {props.waitingForSecondG && [g]} {processes.map((process: Process, index: number) => { @@ -174,13 +174,13 @@ function ProcessTable(props: { ? Colors.darkGray : isShutdownHookPending ? Colors.mutedCyan - : Colors.indigo; + : Colors.gray; return ( @@ -237,7 +237,7 @@ function ProcessTable(props: { : isSuccess ? Colors.darkGray : process.profile?.memoryUsageMB !== undefined - ? Colors.indigo + ? Colors.gray : Colors.darkGray } bold={isSelected} @@ -255,7 +255,7 @@ function ProcessTable(props: { : isSuccess ? Colors.darkGray : process.profile?.cpuUsagePercent !== undefined - ? Colors.indigo + ? Colors.gray : Colors.darkGray } bold={isSelected}