Files
u-tabs/internal/util.go
T
lxy 45e32193af 修复: 缓存原子写与 fork 关系持久化,优化摘要重试
- 原子写: 新增 atomicWriteFile,缓存/收藏/forks 改为临时文件+rename,避免崩溃损坏
- fork 关系: executeFork 记录待绑定项,扫描后按 cwd+时间窗口匹配新会话并持久化
- 摘要重试: 去掉 needDetail 反复刷新逻辑,仅对未生成摘要的会话触发,避免浪费 claude -p 调用
- 脚本转义: pwsh/bash 的 Prompt 回显段转义双引号与 $,防止注入和语法错误
- 输入限制: 工作空间数字输入限制 4 位,避免无限增长
- 品牌统一: 底部版本号文案 绝尘 → U-TABS
2026-07-01 01:29:01 +08:00

180 lines
3.5 KiB
Go

package internal
import (
"crypto/rand"
"encoding/base64"
"math/big"
"os"
"path/filepath"
"runtime"
"strings"
"unicode/utf16"
"unicode/utf8"
)
// atomicWriteFile 原子写文件:先写临时文件再 rename,避免写入中途崩溃导致目标文件损坏
func atomicWriteFile(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return err
}
tmpName := tmp.Name()
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(tmpName)
return err
}
if err := tmp.Sync(); err != nil {
tmp.Close()
os.Remove(tmpName)
return err
}
tmp.Close()
if err := os.Chmod(tmpName, perm); err != nil {
os.Remove(tmpName)
return err
}
return os.Rename(tmpName, path)
}
// 平台信息(启动时确定)
var (
isWindows = runtime.GOOS == "windows"
platformKey = runtime.GOOS + "-" + runtime.GOARCH
)
// toWinPath 将路径中的 / 替换为 \
func toWinPath(s string) string {
return strings.ReplaceAll(s, "/", "\\")
}
// --- 分隔线缓存 ---
var sepCache = make(map[int]string)
// sepRunes 返回 width 个 "─" 字符,结果按 width 缓存。
func sepRunes(w int) string {
if s, ok := sepCache[w]; ok {
return s
}
s := strings.Repeat("─", w)
sepCache[w] = s
return s
}
// --- 字符串宽度 ---
func stringWidth(s string) int {
w := 0
for _, r := range s {
w += runeWidth(r)
}
return w
}
func runeWidth(r rune) int {
if r >= 0x1100 &&
(r <= 0x115F || r == 0x2329 || r == 0x232A ||
(r >= 0x2E80 && r <= 0xA4CF && r != 0x303F) ||
(r >= 0xAC00 && r <= 0xD7A3) ||
(r >= 0xF900 && r <= 0xFAFF) ||
(r >= 0xFE10 && r <= 0xFE19) ||
(r >= 0xFE30 && r <= 0xFE6F) ||
(r >= 0xFF01 && r <= 0xFF60) ||
(r >= 0xFFE0 && r <= 0xFFE6) ||
(r >= 0x20000 && r <= 0x2FFFD) ||
(r >= 0x30000 && r <= 0x3FFFD)) {
return 2
}
return 1
}
func padRightByWidth(s string, targetW int) string {
w := stringWidth(s)
if w >= targetW {
return s
}
return s + strings.Repeat(" ", targetW-w)
}
func truncateByWidth(s string, maxW int) string {
w := 0
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
rw := runeWidth(r)
if w+rw > maxW {
return s[:i]
}
w += rw
i += size
}
return s
}
func wrapByWidth(s string, maxW int) []string {
if maxW <= 0 || s == "" {
return []string{s}
}
var lines []string
for s != "" {
cut := truncateByWidth(s, maxW)
lines = append(lines, cut)
s = s[len(cut):]
}
return lines
}
func fitWidth(s string, w int) string {
return padRightByWidth(truncateByWidth(s, w), w)
}
// --- 视口 ---
func viewport(cursor, total, height int) (int, int) {
if total <= height {
return 0, total
}
half := height / 2
start := cursor - half
if start < 0 {
start = 0
}
end := start + height
if end > total {
end = total
start = end - height
if start < 0 {
start = 0
}
}
return start, end
}
func padLinesTo(lines []string, w, target int) string {
for len(lines) < target {
lines = append(lines, strings.Repeat(" ", w))
}
return strings.Join(lines, "\n")
}
// --- Windows Terminal ---
func encodePSCommand(script string) string {
u16 := utf16.Encode([]rune(script))
b := make([]byte, len(u16)*2)
for i, r := range u16 {
b[i*2] = byte(r)
b[i*2+1] = byte(r >> 8)
}
return base64.StdEncoding.EncodeToString(b)
}
func randRange(min, max int) int {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min)))
return min + int(n.Int64())
}