diff --git a/.bashrc b/.bashrc index 96d7c69..0302d1e 100644 --- a/.bashrc +++ b/.bashrc @@ -181,6 +181,6 @@ if [ -f $HOME/.bashrc.local ]; then fi # Display a welcome message for interactive shells -if [ -t 1 ]; then - dotf hello +if [ -t 1 ] && command -v helloworld &> /dev/null; then + helloworld fi diff --git a/config/ansible/tasks/global/utils/helloworld.go b/config/ansible/tasks/global/utils/helloworld.go new file mode 100644 index 0000000..58396a7 --- /dev/null +++ b/config/ansible/tasks/global/utils/helloworld.go @@ -0,0 +1,365 @@ +package main + +import ( + "fmt" + "math" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// ANSI color codes +var colors = map[string]string{ + "black": "\033[0;30m", + "red": "\033[0;31m", + "green": "\033[0;32m", + "yellow": "\033[0;33m", + "blue": "\033[0;34m", + "purple": "\033[0;35m", + "cyan": "\033[0;36m", + "white": "\033[0;37m", + "grey": "\033[0;90m", + "reset": "\033[0m", +} + +// DistroIcon represents a distro icon and color +type DistroIcon struct { + Icon string + Color string +} + +// DotfilesStatus represents the git status of dotfiles +type DotfilesStatus struct { + IsDirty bool + Untracked int + Modified int + Staged int + CommitHash string + Unpushed int +} + +func main() { + welcome() +} + +func rainbowColor(text string, freq float64, offset float64) string { + var result strings.Builder + for i, char := range text { + if strings.TrimSpace(string(char)) != "" { // Only color non-whitespace characters + // Calculate RGB values using sine waves with phase shifts + r := int(127*math.Sin(freq*float64(i)+offset+0) + 128) + g := int(127*math.Sin(freq*float64(i)+offset+2*math.Pi/3) + 128) + b := int(127*math.Sin(freq*float64(i)+offset+4*math.Pi/3) + 128) + + // Apply the RGB color to the character + result.WriteString(fmt.Sprintf("\033[38;2;%d;%d;%dm%c\033[0m", r, g, b, char)) + } else { + result.WriteRune(char) + } + } + return result.String() +} + +func printLogo() { + logo := ` __ ___ _ ____ __ _____ __ + / |/ /__ ____ ____ ____ ( )_____ / __ \____ / /_/ __(_) /__ _____ + / /|_/ / _ \/ __ \/ __ \/ __ \|// ___/ / / / / __ \/ __/ /_/ / / _ \/ ___/ + / / / / __/ / / / / / / /_/ / (__ ) / /_/ / /_/ / /_/ __/ / / __(__ ) +/_/ /_/\___/_/ /_/_/ /_/\____/ /____/ /_____/\____/\__/_/ /_/_/\___/____/` + + lines := strings.Split(logo, "\n") + for _, line := range lines { + if strings.TrimSpace(line) != "" { + fmt.Println(rainbowColor(line, 0.1, 0)) + } else { + fmt.Println() + } + } + fmt.Println() +} + +func getLastSSHLogin() string { + user := os.Getenv("USER") + if user == "" { + user = os.Getenv("USERNAME") + } + if user == "" { + return "" + } + + // Try lastlog first + cmd := exec.Command("lastlog", "-u", user) + output, err := cmd.CombinedOutput() + if err != nil { + // Try lastlog2 + cmd = exec.Command("lastlog2", user) + output, err = cmd.CombinedOutput() + if err != nil { + return "" + } + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(lines) >= 2 { + parts := strings.Fields(lines[1]) + if len(parts) >= 7 && strings.Contains(parts[1], "ssh") { + ip := parts[2] + timeStr := strings.Join(parts[3:], " ") + return fmt.Sprintf("%sLast SSH login%s%s %s%s from%s %s", + colors["cyan"], colors["reset"], colors["yellow"], timeStr, colors["cyan"], colors["yellow"], ip) + } + } + return "" +} + +func checkDotfilesStatus() *DotfilesStatus { + dotfilesPath := os.Getenv("DOTFILES_PATH") + if dotfilesPath == "" { + homeDir, _ := os.UserHomeDir() + dotfilesPath = filepath.Join(homeDir, ".dotfiles") + } + + gitPath := filepath.Join(dotfilesPath, ".git") + if _, err := os.Stat(gitPath); os.IsNotExist(err) { + return nil + } + + status := &DotfilesStatus{} + + // Check git status + cmd := exec.Command("git", "status", "--porcelain") + cmd.Dir = dotfilesPath + output, err := cmd.Output() + if err == nil && strings.TrimSpace(string(output)) != "" { + status.IsDirty = true + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "??") { + status.Untracked++ + } + if strings.HasPrefix(line, " M") || strings.HasPrefix(line, "MM") { + status.Modified++ + } + if strings.HasPrefix(line, "M ") || strings.HasPrefix(line, "A ") { + status.Staged++ + } + } + } + + // Get commit hash + cmd = exec.Command("git", "rev-parse", "--short", "HEAD") + cmd.Dir = dotfilesPath + output, err = cmd.Output() + if err == nil { + status.CommitHash = strings.TrimSpace(string(output)) + } + + // Count unpushed commits + cmd = exec.Command("git", "log", "--oneline", "@{u}..") + cmd.Dir = dotfilesPath + output, err = cmd.Output() + if err == nil { + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(lines) > 0 && lines[0] != "" { + status.Unpushed = len(lines) + } + } + + return status +} + +func getCondensedStatus() (string, string) { + var statusParts []string + var hashInfo string + + // Check trash status + homeDir, _ := os.UserHomeDir() + trashPath := filepath.Join(homeDir, ".local", "share", "Trash", "files") + if entries, err := os.ReadDir(trashPath); err == nil { + count := len(entries) + if count > 0 { + statusParts = append(statusParts, fmt.Sprintf("[!] %d file(s) in trash", count)) + } + } + + // Check dotfiles status + dotfilesStatus := checkDotfilesStatus() + if dotfilesStatus != nil { + if dotfilesStatus.IsDirty { + statusParts = append(statusParts, fmt.Sprintf("%sdotfiles is dirty%s", colors["yellow"], colors["reset"])) + statusParts = append(statusParts, fmt.Sprintf("%s[%d] untracked%s", colors["red"], dotfilesStatus.Untracked, colors["reset"])) + statusParts = append(statusParts, fmt.Sprintf("%s[%d] modified%s", colors["yellow"], dotfilesStatus.Modified, colors["reset"])) + statusParts = append(statusParts, fmt.Sprintf("%s[%d] staged%s", colors["green"], dotfilesStatus.Staged, colors["reset"])) + } + + if dotfilesStatus.CommitHash != "" { + hashInfo = fmt.Sprintf("%s[%s%s%s]%s", colors["white"], colors["blue"], dotfilesStatus.CommitHash, colors["white"], colors["reset"]) + if dotfilesStatus.IsDirty { + statusParts = append(statusParts, hashInfo) + hashInfo = "" + } + } + + if dotfilesStatus.Unpushed > 0 { + statusParts = append(statusParts, fmt.Sprintf("%s[!] You have %d commit(s) to push%s", colors["yellow"], dotfilesStatus.Unpushed, colors["reset"])) + } + } else { + statusParts = append(statusParts, "Unable to check dotfiles status") + } + + statusLine := "" + if len(statusParts) > 0 { + statusLine = strings.Join(statusParts, " - ") + } + return statusLine, hashInfo +} + +func runDotfilesCommand(args ...string) (string, error) { + cmd := exec.Command("dotfiles", args...) + output, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(output)), nil +} + +func getDistroIcon() (string, string) { + distroIcons := map[string]DistroIcon{ + "windows": {"\uf17a", colors["blue"]}, // blue + "linux": {"\uf17c", colors["yellow"]}, // yellow + "ubuntu": {"\uf31b", "\033[38;5;208m"}, // orange (ANSI 208) + "debian": {"\uf306", colors["red"]}, // red + "arch": {"\uf303", colors["cyan"]}, // cyan + "fedora": {"\uf30a", colors["blue"]}, // blue + "alpine": {"\uf300", colors["cyan"]}, // cyan + "macos": {"\uf179", colors["white"]}, // white + "darwin": {"\uf179", colors["white"]}, // white + "osx": {"\uf179", colors["white"]}, // white + } + + distro, err := runDotfilesCommand("variables", "get", "Platform.Distro", "--format", "raw") + if err != nil { + distro = strings.ToLower(runtime.GOOS) + } else { + distro = strings.ToLower(distro) + } + + if icon, exists := distroIcons[distro]; exists { + return icon.Icon, icon.Color + } + + // Try partial match + for key, icon := range distroIcons { + if strings.Contains(distro, key) { + return icon.Icon, icon.Color + } + } + + return "", "" +} + +func detectShell() string { + // Check for PowerShell profile + if os.Getenv("PROFILE") != "" || os.Getenv("PW_SH_PROFILE") != "" || os.Getenv("PSModulePath") != "" { + return "powershell" + } + + if shell := os.Getenv("SHELL"); shell != "" { + return filepath.Base(shell) + } + + if comspec := os.Getenv("COMSPEC"); comspec != "" { + if strings.HasSuffix(strings.ToLower(comspec), "cmd.exe") { + if os.Getenv("PROFILE") != "" { + return "Powershell" + } + return "CMD" + } + return filepath.Base(comspec) + } + + return "unknown" +} + +func welcome() { + printLogo() + + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown-host" + } + + // Get distro icon + distroIcon, iconColor := getDistroIcon() + + // Get username + username := os.Getenv("USER") + if username == "" { + username = os.Getenv("USERNAME") + } + if username == "" { + username = "user" + } + + // Get SSH login info + sshLogin := getLastSSHLogin() + + // Get shell and arch + shell := detectShell() + arch := runtime.GOARCH + + // Capitalize shell and arch for display + shellDisp := strings.Title(shell) + archDisp := strings.ToUpper(arch) + + // Get package managers + pkgMgrs, err := runDotfilesCommand("variables", "get", "Platform.AvailablePackageManagers", "--format", "raw") + if err != nil { + pkgMgrs = "" + } + + // Compact single line: user@hostname with icon, shell, arch + fmt.Printf("%s%s%s@%s%s", colors["green"], username, colors["cyan"], colors["yellow"], hostname) + if distroIcon != "" { + fmt.Printf(" %s%s", iconColor, distroIcon) + } + fmt.Printf("%s running %s%s%s/%s%s", colors["cyan"], colors["blue"], shellDisp, colors["cyan"], colors["purple"], archDisp) + + if pkgMgrs != "" { + // Parse and color package managers + pkgMgrs = strings.Trim(pkgMgrs, "[]") + pmList := strings.Fields(strings.ReplaceAll(pkgMgrs, ",", "")) + pmColors := []string{colors["yellow"], colors["green"], colors["cyan"], colors["red"], colors["blue"]} + var coloredPMs []string + + for i, pm := range pmList { + color := pmColors[i%len(pmColors)] + coloredPMs = append(coloredPMs, fmt.Sprintf("%s%s", color, pm)) + } + + fmt.Printf("%s [%s%s]", colors["cyan"], strings.Join(coloredPMs, colors["cyan"]+"/"), colors["reset"]) + } else { + fmt.Printf("%s", colors["reset"]) + } + + // Get status info + condensedStatus, hashInfo := getCondensedStatus() + + // Add hash to same line if dotfiles is clean + if hashInfo != "" { + fmt.Printf(" %s", hashInfo) + } + fmt.Println() + + // Display last SSH login info if available + if sshLogin != "" { + fmt.Printf("%s%s\n", sshLogin, colors["reset"]) + } + + // Display condensed status line only if there are issues + if condensedStatus != "" { + fmt.Printf("%s%s%s\n", colors["yellow"], condensedStatus, colors["reset"]) + } +}