package session import ( "crypto/sha256" "encoding/hex" "path/filepath" "runtime" "strings" ) // SessionName returns a stable tmux session name for the given workspace. // Format: ctask---. // // category and slug are sanitized to [A-Za-z0-9_-]; characters outside that // set become '_'; runs of '_' collapse; both are lowercased. slug is // truncated to 30 characters maximum (after sanitization). hash6 is the // first six lowercase-hex characters of sha256(canonical absolute path). // // On Windows the path is lowercased before hashing to match // config.searchRootKey conventions for case-insensitive filesystems. func SessionName(category, slug, absWsPath string) string { cat := sanitizeNameComponent(category) sl := sanitizeNameComponent(slug) if len(sl) > 30 { sl = sl[:30] sl = strings.TrimRight(sl, "_") } clean := filepath.Clean(absWsPath) if runtime.GOOS == "windows" { clean = strings.ToLower(clean) } sum := sha256.Sum256([]byte(clean)) hash := hex.EncodeToString(sum[:])[:6] return "ctask-" + cat + "-" + sl + "-" + hash } // sanitizeNameComponent replaces every char outside [A-Za-z0-9_-] with '_', // collapses runs of '_', trims leading/trailing '_' and '-', and lowercases. func sanitizeNameComponent(s string) string { if s == "" { return "_" } var b strings.Builder b.Grow(len(s)) for _, r := range s { switch { case r >= 'a' && r <= 'z': b.WriteRune(r) case r >= 'A' && r <= 'Z': b.WriteRune(r + ('a' - 'A')) case r >= '0' && r <= '9': b.WriteRune(r) case r == '_' || r == '-': b.WriteRune(r) default: b.WriteByte('_') } } out := b.String() for strings.Contains(out, "__") { out = strings.ReplaceAll(out, "__", "_") } out = strings.Trim(out, "_-") if out == "" { return "_" } return out }