feat: 階段 1 骨架(插件、角色啟動/離開、persona、/config、狀態列)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
export type Color = 'green' | 'yellow' | 'red' | 'cyan' | undefined
|
||||
export type Part = { text: string; color?: Color; dim?: boolean }
|
||||
export type Segment = { id: string; parts: Part[] }
|
||||
|
||||
export const BAR_LEN = 8
|
||||
|
||||
export const textWidth = (s: string): number => {
|
||||
let w = 0
|
||||
for (const ch of s) {
|
||||
const c = ch.codePointAt(0)!
|
||||
w += c >= 0x1100 && (c <= 0x115f || (c >= 0x2e80 && c <= 0xa4cf) || (c >= 0xac00 && c <= 0xd7a3) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0xfe30 && c <= 0xfe6f) || (c >= 0xff00 && c <= 0xff60) || (c >= 0xffe0 && c <= 0xffe6)) ? 2 : 1
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
export const bar = (pct: number, style: 'block' | 'dot' = 'block'): string => {
|
||||
const n = Math.round((Math.max(0, Math.min(100, pct)) / 100) * BAR_LEN)
|
||||
const [on, off] = style === 'dot' ? ['●', '○'] : ['█', '░']
|
||||
return on.repeat(n) + off.repeat(BAR_LEN - n)
|
||||
}
|
||||
|
||||
// 使用率:越高越紅
|
||||
export const usedColor = (pct: number, warn: number, danger: number): Color =>
|
||||
pct >= danger ? 'red' : pct >= warn ? 'yellow' : 'green'
|
||||
|
||||
// 剩餘:量表越少越紅
|
||||
export const remainColor = (remain: number, warn: number, danger: number): Color =>
|
||||
usedColor(100 - remain, warn, danger)
|
||||
|
||||
export const FIELD_IDS = ['character', 'model', 'dir', 'branch', '5h', '7d', 'ctx', 'sys'] as const
|
||||
// 窄終端機時整欄隱藏的順序
|
||||
export const HIDE_ORDER = ['sys', '7d', 'dir', 'branch'] as const
|
||||
|
||||
export const parseFields = (s: string): string[] => {
|
||||
const ids = s.split(',').map(x => x.trim()).filter(x => (FIELD_IDS as readonly string[]).includes(x))
|
||||
return ids.length ? [...new Set(ids)] : [...FIELD_IDS]
|
||||
}
|
||||
|
||||
const SEP = ' │ '
|
||||
|
||||
export const segmentWidth = (s: Segment): number => s.parts.reduce((n, p) => n + textWidth(p.text), 0)
|
||||
|
||||
// 整欄隱藏,絕不截斷。
|
||||
export const layout = (segments: Segment[], columns: number): Segment[] => {
|
||||
const list = [...segments]
|
||||
const total = () => list.reduce((n, s) => n + segmentWidth(s), 0) + Math.max(0, list.length - 1) * textWidth(SEP)
|
||||
for (const id of HIDE_ORDER) {
|
||||
if (total() <= columns) break
|
||||
const i = list.findIndex(s => s.id === id)
|
||||
if (i >= 0) list.splice(i, 1)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export const SEPARATOR = SEP
|
||||
|
||||
export const meter = (label: string, pct: number, color: Color, style: 'block' | 'dot', suffix: string): Segment['parts'] => [
|
||||
{ text: `${label} ` },
|
||||
{ text: bar(pct, style), color },
|
||||
{ text: ` ${Math.round(pct)}%${suffix}` },
|
||||
]
|
||||
Reference in New Issue
Block a user