Commit 178c4fb

Karn Wong <[email protected]>
2026-01-31 08:03:18
refactor: handle errors outside
1 parent cccf9a9
cmd/generate/generate_key.go
@@ -9,8 +9,8 @@ var generateKeyCmd = &cobra.Command{
 	Use:   "key",
 	Short: "Generate key",
 	Long:  `Generate key. Re-implementation of "openssl rand -base64 48"". Result is copied to clipboard.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		generate.Key()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return generate.Key()
 	},
 }
 
cmd/generate/generate_passphrase.go
@@ -9,8 +9,8 @@ var generatePassphraseCmd = &cobra.Command{
 	Use:   "passphrase",
 	Short: "Generate passphrase",
 	Long:  `Generate passphrase. Result is copied to clipboard.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		generate.Passphrase()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return generate.Passphrase()
 	},
 }
 
cmd/generate/generate_password.go
@@ -9,8 +9,8 @@ var generatePasswordCmd = &cobra.Command{
 	Use:   "password",
 	Short: "Generate password",
 	Long:  `Generate password. Result is copied to clipboard.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		generate.Password()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return generate.Password()
 	},
 }
 
cmd/generate/generate_qrcode.go
@@ -9,8 +9,8 @@ var generateQRCodeCmd = &cobra.Command{
 	Use:   "qrcode",
 	Short: "Generate QR code",
 	Long:  `Generate QR code from URL (either as an arg or from clipboard) and copy resulting image to clipboard.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		generate.QRCode(args)
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return generate.QRCode(args)
 	},
 }
 
cmd/generate/generate_ssh_key.go
@@ -9,8 +9,8 @@ var generateSSHKeyCmd = &cobra.Command{
 	Use:   "ssh-key",
 	Short: "Create SSH key",
 	Long:  `Create SSH key`,
-	Run: func(cmd *cobra.Command, args []string) {
-		generate.SSHKey(args)
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return generate.SSHKey(args)
 	},
 }
 
cmd/get/get_hwinfo.go
@@ -8,8 +8,8 @@ import (
 var getHwInfoCmd = &cobra.Command{
 	Use:   "hwinfo",
 	Short: "Get hardware info",
-	Run: func(cmd *cobra.Command, args []string) {
-		get.HwInfo()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.HwInfo()
 	},
 }
 
cmd/get/get_iface.go
@@ -9,8 +9,8 @@ var getIfaceCmd = &cobra.Command{
 	Use:   "iface",
 	Short: "Get iface",
 	Long:  `Get iface used for public internet access`,
-	Run: func(cmd *cobra.Command, args []string) {
-		get.Iface()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.Iface()
 	},
 }
 
cmd/get/get_ip.go
@@ -9,8 +9,8 @@ var getIPCmd = &cobra.Command{
 	Use:   "ip",
 	Short: "Get IP information",
 	Long:  `Get IP information`,
-	Run: func(cmd *cobra.Command, args []string) {
-		get.IP()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.IP()
 	},
 }
 
cmd/get/get_ipinfo.go
@@ -9,8 +9,8 @@ var getIPInfoCmd = &cobra.Command{
 	Use:   "ipinfo [ip]",
 	Short: "Get detailed IP information (location, ISP, etc.)",
 	Args:  cobra.MaximumNArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		get.IPInfo(args)
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.IPInfo(args)
 	},
 }
 
cmd/get/get_sensors.go
@@ -8,8 +8,8 @@ import (
 var SensorsCmd = &cobra.Command{
 	Use:   "sensors",
 	Short: "Get sensors info",
-	Run: func(cmd *cobra.Command, args []string) {
-		get.Sensors()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.Sensors()
 	},
 }
 
cmd/get/get_siteinfo.go
@@ -9,8 +9,8 @@ var siteinfoCmd = &cobra.Command{
 	Use:   "siteinfo [url]",
 	Short: "Get website technology information",
 	Long:  `Analyze a website and display the technologies it uses, categorized by type`,
-	Run: func(cmd *cobra.Command, args []string) {
-		get.GetSiteInfo(args)
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.GetSiteInfo(args)
 	},
 }
 
cmd/get/get_smart.go
@@ -9,8 +9,8 @@ var getSmartCmd = &cobra.Command{
 	Use:   "smart [disk]",
 	Short: "Get disk SMART info.",
 	Long:  "Get disk SMART info. Equivalent of `sudo smartctl -a /dev/nvme0n1p`",
-	Run: func(cmd *cobra.Command, args []string) {
-		get.Smart(args)
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.Smart(args)
 	},
 }
 
cmd/get/get_sysinfo.go
@@ -8,8 +8,8 @@ import (
 var getSysInfoCmd = &cobra.Command{
 	Use:   "sysinfo",
 	Short: "Get system info",
-	Run: func(cmd *cobra.Command, args []string) {
-		get.SysInfo()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.SysInfo()
 	},
 }
 
cmd/get/get_volumes.go
@@ -8,8 +8,8 @@ import (
 var listVolumesCmd = &cobra.Command{
 	Use:   "volumes",
 	Short: "List volumes",
-	Run: func(cmd *cobra.Command, args []string) {
-		get.Volumes()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return get.Volumes()
 	},
 }
 
cmd/shouldideploytoday.go
@@ -8,8 +8,8 @@ import (
 var ShouldIDeployTodayCmd = &cobra.Command{
 	Use:   "shouldideploytoday",
 	Short: "Should I deploy today?",
-	Run: func(cmd *cobra.Command, args []string) {
-		shouldideploytoday.ShouldIDeployToday()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return shouldideploytoday.ShouldIDeployToday()
 	},
 }
 
cmd/speedtest.go
@@ -8,8 +8,8 @@ import (
 var SpeedTestCmd = &cobra.Command{
 	Use:   "speedtest",
 	Short: "Speedtest",
-	Run: func(cmd *cobra.Command, args []string) {
-		speedtest.SpeedTest()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return speedtest.SpeedTest()
 	},
 }
 
cmd/stopwatch.go
@@ -8,8 +8,8 @@ import (
 var StopwatchCmd = &cobra.Command{
 	Use:   "stopwatch",
 	Short: "Create a stopwatch",
-	Run: func(cmd *cobra.Command, args []string) {
-		stopwatch.Stopwatch()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return stopwatch.Stopwatch()
 	},
 }
 
cmd/timer.go
@@ -8,8 +8,8 @@ import (
 var TimerCmd = &cobra.Command{
 	Use:   "timer",
 	Short: "Create a timer",
-	Run: func(cmd *cobra.Command, args []string) {
-		timer.Timer(args)
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return timer.Timer(args)
 	},
 }
 
internal/generate/key.go
@@ -6,25 +6,32 @@ import (
 	"fmt"
 
 	"github.com/kahnwong/swissknife/internal/utils"
-	"github.com/rs/zerolog/log"
 )
 
-func generateKey(n int) string {
+func generateKey(n int) (string, error) {
 	// https://stackoverflow.com/questions/32349807/how-can-i-generate-a-random-int-using-the-crypto-rand-package/32351471#32351471
 	// generate random bytes
 	b := make([]byte, n)
 	_, err := rand.Read(b)
 	// Note that err == nil only if we read len(b) bytes.
 	if err != nil {
-		log.Fatal().Msg("Failed to generate random bytes")
+		return "", fmt.Errorf("failed to generate random bytes: %w", err)
 	}
 
 	// convert to base64
-	return base64.URLEncoding.EncodeToString(b)
+	return base64.URLEncoding.EncodeToString(b), nil
 }
 
-func Key() {
-	key := generateKey(48)
-	utils.WriteToClipboard(key)
+func Key() error {
+	key, err := generateKey(48)
+	if err != nil {
+		return err
+	}
+
+	if err = utils.WriteToClipboard(key); err != nil {
+		return err
+	}
+
 	fmt.Printf("%s\n", key)
+	return nil
 }
internal/generate/passphrase.go
@@ -5,24 +5,31 @@ import (
 	"strings"
 
 	"github.com/kahnwong/swissknife/internal/utils"
-	"github.com/rs/zerolog/log"
 	"github.com/sethvargo/go-diceware/diceware"
 )
 
-func generatePassphrase() string {
+func generatePassphrase() (string, error) {
 	// Generate 6 words using the diceware algorithm.
 	list, err := diceware.Generate(6)
 	if err != nil {
-		log.Fatal().Msg("Failed to generate passphrases")
+		return "", fmt.Errorf("failed to generate passphrases: %w", err)
 	}
 
 	res := strings.Join(list, "-")
 
-	return res
+	return res, nil
 }
 
-func Passphrase() {
-	passphrase := generatePassphrase()
-	utils.WriteToClipboard(passphrase)
+func Passphrase() error {
+	passphrase, err := generatePassphrase()
+	if err != nil {
+		return err
+	}
+
+	if err = utils.WriteToClipboard(passphrase); err != nil {
+		return err
+	}
+
 	fmt.Printf("%s\n", passphrase)
+	return nil
 }
internal/generate/password.go
@@ -4,23 +4,30 @@ import (
 	"fmt"
 
 	"github.com/kahnwong/swissknife/internal/utils"
-	"github.com/rs/zerolog/log"
 	"github.com/sethvargo/go-password/password"
 )
 
-func generatePassword() string {
+func generatePassword() (string, error) {
 	// Generate a password that is 64 characters long with 10 digits, 10 symbols,
 	// allowing upper and lower case letters, disallowing repeat characters.
 	res, err := password.Generate(32, 10, 0, false, false)
 	if err != nil {
-		log.Fatal().Msg("Failed to generate password")
+		return "", fmt.Errorf("failed to generate password: %w", err)
 	}
 
-	return res
+	return res, nil
 }
 
-func Password() {
-	psswd := generatePassword()
-	utils.WriteToClipboard(psswd)
+func Password() error {
+	psswd, err := generatePassword()
+	if err != nil {
+		return err
+	}
+
+	if err = utils.WriteToClipboard(psswd); err != nil {
+		return err
+	}
+
 	fmt.Printf("%s\n", psswd)
+	return nil
 }
internal/generate/qrcode.go
@@ -4,38 +4,45 @@ import (
 	"fmt"
 
 	"github.com/kahnwong/swissknife/internal/utils"
-	"github.com/rs/zerolog/log"
 	qrcode "github.com/skip2/go-qrcode"
 )
 
-func generateQRCode(url string) ([]byte, string) {
+func generateQRCode(url string) ([]byte, string, error) {
 	// init
 	var q *qrcode.QRCode
 	q, err := qrcode.New(url, qrcode.Medium)
 	if err != nil {
-		log.Fatal().Msg("Failed to initialize QRCode object")
+		return nil, "", fmt.Errorf("failed to initialize QRCode object: %w", err)
 	}
 
 	// generate png
 	png, err := q.PNG(1024)
 	if err != nil {
-		log.Fatal().Msg("Failed to generate QRCode PNG")
+		return nil, "", fmt.Errorf("failed to generate QRCode PNG: %w", err)
 	}
 
 	// for stdout
 	//stdout := q.ToString(false)
 	stdout := q.ToSmallString(false)
 
-	return png, stdout
+	return png, stdout, nil
 }
 
-func QRCode(args []string) {
+func QRCode(args []string) error {
 	// set URL
 	url := utils.SetURL(args)
 	fmt.Println(url)
 
 	// main
-	png, stdout := generateQRCode(url)
-	utils.WriteToClipboardImage(png)
+	png, stdout, err := generateQRCode(url)
+	if err != nil {
+		return err
+	}
+
+	if err = utils.WriteToClipboardImage(png); err != nil {
+		return fmt.Errorf("failed to write to clipboard: %w", err)
+	}
+
 	fmt.Println(stdout)
+	return nil
 }
internal/generate/ssh_key.go
@@ -9,52 +9,51 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/rs/zerolog/log"
 	"golang.org/x/crypto/ssh"
 )
 
 // helpers
-func writeStringToFile(filePath string, data string, permission os.FileMode) {
+func writeStringToFile(filePath string, data string, permission os.FileMode) error {
 	file, err := os.Create(filePath)
 	if err != nil {
-		log.Fatal().Msgf("Failed to create file %s", filePath)
+		return fmt.Errorf("failed to create file %s: %w", filePath, err)
 	}
 
-	_, err = file.WriteString(data)
-	if err != nil {
-		log.Fatal().Msgf("Failed to write to file %s", filePath)
+	if _, err = file.WriteString(data); err != nil {
+		return fmt.Errorf("failed to write to file %s: %w", filePath, err)
 	}
 
-	err = file.Chmod(permission)
-	if err != nil {
-		log.Fatal().Msgf("Failed to change file permissions")
+	if err = file.Chmod(permission); err != nil {
+		return fmt.Errorf("failed to change file permissions: %w", err)
 	}
+
+	return nil
 }
 
-func returnKeyPath(fileName string) string {
+func returnKeyPath(fileName string) (string, error) {
 	currentDir, err := os.Getwd()
 	if err != nil {
-		log.Fatal().Msgf("Failed to get current directory")
+		return "", fmt.Errorf("failed to get current directory: %w", err)
 	}
 
 	keyPath := filepath.Join(currentDir, fileName)
 
-	return keyPath
+	return keyPath, nil
 }
 
 // main
-func generateSSHKeyEDSA() (string, string) {
+func generateSSHKeyEDSA() (string, string, error) {
 	// Generate a new Ed25519 private key
 	//// If rand is nil, crypto/rand.Reader will be used
 	public, private, err := ed25519.GenerateKey(nil)
 	if err != nil {
-		log.Fatal().Msgf("Failed to generate ed25519 key")
+		return "", "", fmt.Errorf("failed to generate ed25519 key: %w", err)
 	}
 
 	// public key
 	publicKey, err := ssh.NewPublicKey(public)
 	if err != nil {
-		log.Fatal().Msgf("Failed to create public key")
+		return "", "", fmt.Errorf("failed to create public key: %w", err)
 	}
 	publicKeyString := fmt.Sprintf("ssh-ed25519 %s", base64.StdEncoding.EncodeToString(publicKey.Marshal()))
 
@@ -62,30 +61,42 @@ func generateSSHKeyEDSA() (string, string) {
 	//// p stands for pem
 	p, err := ssh.MarshalPrivateKey(crypto.PrivateKey(private), "")
 	if err != nil {
-		log.Fatal().Msgf("Failed to marshal private key")
+		return "", "", fmt.Errorf("failed to marshal private key: %w", err)
 	}
 	privateKeyPem := pem.EncodeToMemory(p)
 	privateKeyString := string(privateKeyPem)
 
-	return publicKeyString, privateKeyString
+	return publicKeyString, privateKeyString, nil
 }
 
-func SSHKey(args []string) {
+func SSHKey(args []string) error {
 	//init
 	if len(args) == 0 {
-		fmt.Println("Please specify key name")
-		os.Exit(1)
+		return fmt.Errorf("please specify key name")
 	}
 
 	// main
-	publicKeyString, privateKeyString := generateSSHKeyEDSA()
+	publicKeyString, privateKeyString, err := generateSSHKeyEDSA()
+	if err != nil {
+		return err
+	}
 
 	// write key to file
 	publicKeyFilename := fmt.Sprintf("%s.pub", args[0])
-	writeStringToFile(publicKeyFilename, publicKeyString, 0644)
+	if err = writeStringToFile(publicKeyFilename, publicKeyString, 0644); err != nil {
+		return err
+	}
 
 	privateKeyFilename := args[0]
-	writeStringToFile(privateKeyFilename, privateKeyString, 0600)
+	if err = writeStringToFile(privateKeyFilename, privateKeyString, 0600); err != nil {
+		return err
+	}
+
+	keyPath, err := returnKeyPath(args[0])
+	if err != nil {
+		return err
+	}
 
-	fmt.Printf("SSH key created at: %s\n", returnKeyPath(args[0]))
+	fmt.Printf("SSH key created at: %s\n", keyPath)
+	return nil
 }
internal/get/hwinfo.go
@@ -7,44 +7,46 @@ import (
 
 	"github.com/jaypipes/ghw"
 	"github.com/kahnwong/swissknife/configs/color"
-	"github.com/rs/zerolog/log"
 	"github.com/yumaojun03/dmidecode"
 )
 
-func HwInfo() {
+func HwInfo() error {
 	// need to run as sudo
 	if os.Geteuid() != 0 {
-		log.Fatal().Msg("Need to run as sudo")
+		return fmt.Errorf("need to run as sudo")
 	}
 
 	// cpu
-	cpuModel, cpuThreads := getCpuInfo() // shared with `sysinfo.go`
+	cpuModel, cpuThreads, err := getCpuInfo() // shared with `sysinfo.go`
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s: %s (%v)\n", color.Green("CPU"), cpuModel, cpuThreads)
 
 	// gpu
 	gpu, err := ghw.GPU()
 	if err != nil {
-		fmt.Printf("Error getting GPU info: %v", err)
-	}
+		fmt.Printf("Error getting GPU info: %v\n", err)
+	} else {
+		fmt.Printf("%s:\n", color.Green("GPUs"))
 
-	fmt.Printf("%s:\n", color.Green("GPUs"))
-
-	for _, card := range gpu.GraphicsCards {
-		fmt.Printf("  - %s: %s\n", color.Blue("Vendor"), card.DeviceInfo.Vendor.Name)
-		fmt.Printf("    %s: %s\n", color.Blue("Model"), card.DeviceInfo.Product.Name)
+		for _, card := range gpu.GraphicsCards {
+			fmt.Printf("  - %s: %s\n", color.Blue("Vendor"), card.DeviceInfo.Vendor.Name)
+			fmt.Printf("    %s: %s\n", color.Blue("Model"), card.DeviceInfo.Product.Name)
+		}
 	}
 
 	// memory
 	dmi, err := dmidecode.New()
 	if err != nil {
-		log.Fatal().Msg("Failed to get dmi info")
+		return fmt.Errorf("failed to get dmi info: %w", err)
 	}
 
 	fmt.Printf("%s:\n", color.Green("Memory"))
 
 	memoryDevices, err := dmi.MemoryDevice()
 	if err != nil {
-		log.Fatal().Msg("Failed to get memory info")
+		return fmt.Errorf("failed to get memory info: %w", err)
 	}
 
 	for _, i := range memoryDevices {
@@ -60,7 +62,7 @@ func HwInfo() {
 	// disk
 	block, err := ghw.Block()
 	if err != nil {
-		log.Fatal().Msg("Failed to get block storage info")
+		return fmt.Errorf("failed to get block storage info: %w", err)
 	}
 
 	fmt.Printf("%s:\n", color.Green("Disks"))
@@ -76,10 +78,11 @@ func HwInfo() {
 	//// mainboardInfo, err := dmi.BaseBoard()
 	baseboard, err := ghw.Baseboard()
 	if err != nil {
-		log.Fatal().Msg("Failed to get baseboard info")
+		return fmt.Errorf("failed to get baseboard info: %w", err)
 	}
 
 	fmt.Printf("%s:\n", color.Green("Mainboard"))
 	fmt.Printf("  - %s: %s\n", color.Blue("Manufacturer"), baseboard.Vendor)
 	fmt.Printf("    %s: %s\n", color.Blue("Model"), baseboard.Product)
+	return nil
 }
internal/get/iface.go
@@ -5,30 +5,33 @@ import (
 	"net"
 
 	"github.com/libp2p/go-netroute"
-	"github.com/rs/zerolog/log"
 )
 
 // unix: `netstat -nr -f inet`
 // get iface-owned app: `ifconfig -v utun3 | grep "agent domain"`
 
-func getIface() string {
+func getIface() (string, error) {
 	r, err := netroute.New()
 	if err != nil {
-		log.Fatal().Msg("Error initializing netroute")
+		return "", fmt.Errorf("error initializing netroute: %w", err)
 	}
 
 	iface, _, _, err := r.Route(
 		net.IPv4(104, 16, 133, 229), // cloudflare
 	)
 	if err != nil {
-		log.Fatal().Msg("Error retrieving net route")
+		return "", fmt.Errorf("error retrieving net route: %w", err)
 	}
 	//fmt.Printf("%v, %v, %v, %v\n", iface, gw, src, err)
 
-	return iface.Name
+	return iface.Name, nil
 }
 
-func Iface() {
-	iface := getIface()
+func Iface() error {
+	iface, err := getIface()
+	if err != nil {
+		return err
+	}
 	fmt.Println(iface)
+	return nil
 }
internal/get/ip.go
@@ -21,12 +21,12 @@ type IPLocationResponse struct {
 	RegionName string `json:"regionName"`
 }
 
-func getInternalIP() string {
+func getInternalIP() (string, error) {
 	var internalIP string
 
 	ifaces, err := net.Interfaces()
 	if err != nil {
-		panic(err)
+		return "", fmt.Errorf("failed to get network interfaces: %w", err)
 	}
 
 	for _, iface := range ifaces {
@@ -39,7 +39,7 @@ func getInternalIP() string {
 
 		addrs, err := iface.Addrs()
 		if err != nil {
-			panic(err)
+			return "", fmt.Errorf("failed to get interface addresses: %w", err)
 		}
 
 		for _, addr := range addrs {
@@ -63,14 +63,20 @@ func getInternalIP() string {
 		}
 	}
 
-	return internalIP
+	return internalIP, nil
 }
 
-func getLocalIP() string {
+func getLocalIP() (string, error) {
 	conn, err := net.Dial("udp", "8.8.8.8:80")
 	if err != nil {
-		log.Fatal().Msg("Error on net.Dial")
+		return "", fmt.Errorf("error on net.Dial: %w", err)
 	}
+	defer func(conn net.Conn) {
+		err = conn.Close()
+		if err != nil {
+			log.Error().Err(err).Msg("error closing connection")
+		}
+	}(conn)
 
 	localAddr := conn.LocalAddr().(*net.UDPAddr)
 
@@ -81,13 +87,13 @@ func getLocalIP() string {
 	if len(parts) > 0 {
 		localIP = parts[0]
 	} else {
-		log.Error().Msg("Invalid address format")
+		return "", fmt.Errorf("invalid address format")
 	}
 
-	return localIP
+	return localIP, nil
 }
 
-func getPublicIP() PublicIPResponse {
+func getPublicIP() (PublicIPResponse, error) {
 	var response PublicIPResponse
 	err := requests.
 		URL("https://api.ipify.org?format=json").
@@ -95,13 +101,13 @@ func getPublicIP() PublicIPResponse {
 		Fetch(context.Background())
 
 	if err != nil {
-		log.Fatal().Msg("Error getting public ip")
+		return PublicIPResponse{}, fmt.Errorf("error getting public ip: %w", err)
 	}
 
-	return response
+	return response, nil
 }
 
-func getIPLocation(ip string) IPLocationResponse {
+func getIPLocation(ip string) (IPLocationResponse, error) {
 	var response IPLocationResponse
 	err := requests.
 		URL(fmt.Sprintf("http://ip-api.com/json/%s?fields=query,country,regionName", ip)).
@@ -109,20 +115,34 @@ func getIPLocation(ip string) IPLocationResponse {
 		Fetch(context.Background())
 
 	if err != nil {
-		log.Fatal().Msg("Error getting ip location")
+		return IPLocationResponse{}, fmt.Errorf("error getting ip location: %w", err)
 	}
 
-	return response
+	return response, nil
 }
 
-func IP() {
-	internalIP := getInternalIP()
+func IP() error {
+	internalIP, err := getInternalIP()
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s: %s\n", color.Green("Internal IP"), internalIP)
 
-	localIP := getLocalIP()
+	localIP, err := getLocalIP()
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s: %s\n", color.Green("Local IP"), localIP)
 
-	publicIP := getPublicIP()
-	IPLocation := getIPLocation(publicIP.Ip)
+	publicIP, err := getPublicIP()
+	if err != nil {
+		return err
+	}
+
+	IPLocation, err := getIPLocation(publicIP.Ip)
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s: %s (%s, %s)\n", color.Green("Public IP"), publicIP.Ip, color.Blue(IPLocation.RegionName), color.Blue(IPLocation.Country))
+	return nil
 }
internal/get/ipinfo.go
@@ -7,7 +7,6 @@ import (
 	"github.com/carlmjohnson/requests"
 	"github.com/kahnwong/swissknife/configs/color"
 	"github.com/kahnwong/swissknife/internal/utils"
-	"github.com/rs/zerolog/log"
 )
 
 type IPInfoResponse struct {
@@ -27,7 +26,7 @@ type IPInfoResponse struct {
 	As          string  `json:"as"`
 }
 
-func getIPInfo(ip string) IPInfoResponse {
+func getIPInfo(ip string) (IPInfoResponse, error) {
 	var response IPInfoResponse
 	err := requests.
 		URL(fmt.Sprintf("http://ip-api.com/json/%s", ip)).
@@ -35,27 +34,34 @@ func getIPInfo(ip string) IPInfoResponse {
 		Fetch(context.Background())
 
 	if err != nil {
-		log.Fatal().Msg("Error getting detailed ip info")
+		return IPInfoResponse{}, fmt.Errorf("error getting detailed ip info: %w", err)
 	}
 
-	return response
+	return response, nil
 }
 
-func IPInfo(args []string) {
+func IPInfo(args []string) error {
 	ip := utils.SetIP(args)
 
 	var targetIP string
 	if ip == "" {
-		publicIP := getPublicIP()
+		publicIP, err := getPublicIP()
+		if err != nil {
+			return err
+		}
 		targetIP = publicIP.Ip
 	} else {
 		targetIP = ip
 	}
 
-	ipInfo := getIPInfo(targetIP)
+	ipInfo, err := getIPInfo(targetIP)
+	if err != nil {
+		return err
+	}
 
 	fmt.Printf("%s: %s\n", color.Green("IP Address"), ipInfo.Query)
 	fmt.Printf("%s: %s, %s, %s\n", color.Green("Location"), ipInfo.City, ipInfo.RegionName, color.Blue(ipInfo.Country))
 	fmt.Printf("%s: %s\n", color.Green("ISP"), ipInfo.Isp)
 	fmt.Printf("%s: %s\n", color.Green("Organization"), ipInfo.Org)
+	return nil
 }
internal/get/sensors.go
@@ -12,20 +12,20 @@ import (
 	"fmt"
 
 	"github.com/kahnwong/swissknife/configs/color"
-	"github.com/rs/zerolog/log"
 )
 
-func Sensors() {
+func Sensors() error {
 	result := C.sensors()
 
 	switch result.error {
 	case C.SENSOR_SUCCESS:
 		fmt.Printf("%s: %.2f\n", color.Green("Temperature"), float64(result.temperature))
+		return nil
 	case C.SENSOR_NO_COMPONENTS:
-		log.Fatal().Msg("No components found")
+		return fmt.Errorf("no components found")
 	case C.SENSOR_NO_TEMPERATURE:
-		log.Fatal().Msg("No temperature reading available")
+		return fmt.Errorf("no temperature reading available")
 	default:
-		log.Fatal().Msg("Unknown error occurred")
+		return fmt.Errorf("unknown error occurred")
 	}
 }
internal/get/siteinfo.go
@@ -4,13 +4,13 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"os"
 	"sort"
 	"strings"
 
 	"github.com/kahnwong/swissknife/configs/color"
 	"github.com/kahnwong/swissknife/internal/utils"
 	wappalyzer "github.com/projectdiscovery/wappalyzergo"
-	"github.com/rs/zerolog/log"
 )
 
 // categoryMapping maps wappalyzer category names to our display categories
@@ -101,7 +101,7 @@ var categoryMapping = map[string]string{
 	"Webcams":                         "Media",
 }
 
-func GetSiteInfo(args []string) {
+func GetSiteInfo(args []string) error {
 	// set URL
 	url := utils.SetURL(args)
 	fmt.Println(url)
@@ -109,24 +109,23 @@ func GetSiteInfo(args []string) {
 	// fetch site
 	resp, err := http.DefaultClient.Get(url)
 	if err != nil {
-		log.Fatal().Err(err).Msgf("Failed to fetch URL: %s", url)
+		return fmt.Errorf("failed to fetch URL %s: %w", url, err)
 	}
 	defer func(Body io.ReadCloser) {
-		err = Body.Close()
-		if err != nil {
-			log.Error().Err(err).Msg("Failed to close response body")
+		if err := Body.Close(); err != nil {
+			fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", err)
 		}
 	}(resp.Body)
 
 	data, err := io.ReadAll(resp.Body)
 	if err != nil {
-		log.Fatal().Err(err).Msg("Failed to read response body")
+		return fmt.Errorf("failed to read response body: %w", err)
 	}
 
 	// init wappalyzer
 	wappalyzerClient, err := wappalyzer.New()
 	if err != nil {
-		log.Fatal().Err(err).Msg("Failed to initialize wappalyzer")
+		return fmt.Errorf("failed to initialize wappalyzer: %w", err)
 	}
 
 	appsInfo := wappalyzerClient.FingerprintWithInfo(resp.Header, data)
@@ -158,6 +157,7 @@ func GetSiteInfo(args []string) {
 
 	if len(categoryTechs) == 0 {
 		fmt.Println("No technologies detected")
+		return nil
 	}
 
 	categories := make([]string, 0, len(categoryTechs))
@@ -176,4 +176,5 @@ func GetSiteInfo(args []string) {
 			fmt.Printf("- %s\n", tech)
 		}
 	}
+	return nil
 }
internal/get/smart.go
@@ -1,6 +1,7 @@
 package get
 
 import (
+	"fmt"
 	"os"
 	"reflect"
 	"runtime"
@@ -8,7 +9,6 @@ import (
 	"github.com/anatol/smart.go"
 	"github.com/jedib0t/go-pretty/v6/table"
 	"github.com/kahnwong/swissknife/configs/color"
-	"github.com/rs/zerolog/log"
 	"github.com/shirou/gopsutil/v4/disk"
 )
 
@@ -93,32 +93,38 @@ func sataSmart(device string) error {
 	}
 }
 
-func Smart(args []string) {
-	if runtime.GOOS == "linux" {
-		var device string
-		if len(args) == 0 {
-			device = getRootDiskVolume()
-		} else if len(args) == 1 {
-			device = args[0]
-		} else {
-			log.Error().Msg("Too many arguments")
-		}
-		err := nvmeSmart(device)
+func Smart(args []string) error {
+	if runtime.GOOS != "linux" {
+		return fmt.Errorf("%s is not supported", runtime.GOOS)
+	}
+
+	var device string
+	var err error
+	if len(args) == 0 {
+		device, err = getRootDiskVolume()
 		if err != nil {
-			err = sataSmart(device)
-			if err != nil {
-				log.Error().Err(err).Msg("Unrecognized device type. It's not NVME or SATA, or you didn't run as sudo")
-			}
+			return err
 		}
+	} else if len(args) == 1 {
+		device = args[0]
 	} else {
-		log.Error().Msgf("%s is not supported\n", runtime.GOOS)
+		return fmt.Errorf("too many arguments")
+	}
+
+	err = nvmeSmart(device)
+	if err != nil {
+		err = sataSmart(device)
+		if err != nil {
+			return fmt.Errorf("unrecognized device type. It's not NVME or SATA, or you didn't run as sudo: %w", err)
+		}
 	}
+	return nil
 }
 
-func getRootDiskVolume() string {
+func getRootDiskVolume() (string, error) {
 	partitions, err := disk.Partitions(false)
 	if err != nil {
-		log.Fatal().Msg("Failed to get disk partitions")
+		return "", fmt.Errorf("failed to get disk partitions: %w", err)
 	}
 
 	var volume string
@@ -128,5 +134,5 @@ func getRootDiskVolume() string {
 		}
 	}
 
-	return volume
+	return volume, nil
 }
internal/get/sysinfo.go
@@ -19,7 +19,6 @@ import (
 	"github.com/shirou/gopsutil/v4/cpu"
 
 	"github.com/kahnwong/swissknife/configs/color"
-	"github.com/rs/zerolog/log"
 	"github.com/shirou/gopsutil/v4/disk"
 	"github.com/shirou/gopsutil/v4/host"
 	"github.com/shirou/gopsutil/v4/mem"
@@ -33,10 +32,16 @@ type batteryStruct struct {
 	BatteryTimeToEmpty    uint64
 }
 
-func SysInfo() {
+func SysInfo() error {
 	// host
-	username := getUsername()
-	hostInfo := getHostInfo()
+	username, err := getUsername()
+	if err != nil {
+		return err
+	}
+	hostInfo, err := getHostInfo()
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s@%s\n", color.Green(username), color.Green(hostInfo.Hostname))
 	fmt.Println(strings.Repeat("-", len(username)+len(hostInfo.Hostname)+1))
 
@@ -44,11 +49,17 @@ func SysInfo() {
 	fmt.Printf("%s: %s %s\n", color.Green("OS"), hostInfo.Platform, hostInfo.PlatformVersion)
 
 	// cpu
-	cpuModel, cpuThreads := getCpuInfo()
+	cpuModel, cpuThreads, err := getCpuInfo()
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s: %s (%v)\n", color.Green("CPU"), cpuModel, cpuThreads)
 
 	// memory
-	memoryUsed, memoryTotal, memoryUsedPercent := getMemoryInfo()
+	memoryUsed, memoryTotal, memoryUsedPercent, err := getMemoryInfo()
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s: %.2f GB / %v GB (%s)\n",
 		color.Green("Memory"),
 		convertKBtoGB(memoryUsed, true), convertKBtoGB(memoryTotal, false),
@@ -56,7 +67,10 @@ func SysInfo() {
 	)
 
 	// disk
-	diskUsed, diskTotal, diskUsedPercent := getDiskInfo()
+	diskUsed, diskTotal, diskUsedPercent, err := getDiskInfo()
+	if err != nil {
+		return err
+	}
 	fmt.Printf("%s: %v GB / %v GB (%s)\n",
 		color.Green("Disk"),
 		convertKBtoGiB(diskUsed), convertKBtoGiB(diskTotal),
@@ -64,7 +78,10 @@ func SysInfo() {
 	)
 
 	// battery
-	batteryInfo := getBatteryInfo()
+	batteryInfo, err := getBatteryInfo()
+	if err != nil {
+		return err
+	}
 
 	// only print battery info if is a laptop
 	if batteryInfo.BatteryFull > 0 {
@@ -100,63 +117,63 @@ func SysInfo() {
 			color.Blue(batteryTimeToEmptyFormatted),
 		)
 	}
+	return nil
 }
 
 // ---- functions ----
-func getUsername() string {
+func getUsername() (string, error) {
 	username, err := user.Current()
 	if err != nil {
-		log.Fatal().Msg("Failed to get current user info")
+		return "", fmt.Errorf("failed to get current user info: %w", err)
 	}
-	return username.Username
+	return username.Username, nil
 }
 
-func getHostInfo() *host.InfoStat {
+func getHostInfo() (*host.InfoStat, error) {
 	hostStat, err := host.Info()
 	if err != nil {
-		log.Fatal().Msg("Failed to get host info")
+		return nil, fmt.Errorf("failed to get host info: %w", err)
 	}
-	return hostStat
+	return hostStat, nil
 }
 
-func getCpuInfo() (string, int) {
+func getCpuInfo() (string, int, error) {
 	cpuStat, err := cpu.Info()
 	if err != nil {
-		log.Fatal().Msg("Failed to get cpu info")
+		return "", 0, fmt.Errorf("failed to get cpu info: %w", err)
 	}
 	cpuThreads, err := cpu.Counts(true)
 	if err != nil {
-		log.Fatal().Msg("Failed to get cpu threads info")
+		return "", 0, fmt.Errorf("failed to get cpu threads info: %w", err)
 	}
 
-	return cpuStat[0].ModelName, cpuThreads
+	return cpuStat[0].ModelName, cpuThreads, nil
 }
 
-func getMemoryInfo() (uint64, uint64, int) {
+func getMemoryInfo() (uint64, uint64, int, error) {
 	vmStat, err := mem.VirtualMemory()
 	if err != nil {
-		log.Fatal().Msg("Failed to get memory info")
+		return 0, 0, 0, fmt.Errorf("failed to get memory info: %w", err)
 	}
-	return vmStat.Used, vmStat.Total, convertToPercent(float64(vmStat.Used) / float64(vmStat.Total))
+	return vmStat.Used, vmStat.Total, convertToPercent(float64(vmStat.Used) / float64(vmStat.Total)), nil
 }
 
-func getDiskInfo() (uint64, uint64, int) {
+func getDiskInfo() (uint64, uint64, int, error) {
 	diskStat, err := disk.Usage("/")
 	if err != nil {
-		log.Fatal().Msg("Failed to get disk info")
+		return 0, 0, 0, fmt.Errorf("failed to get disk info: %w", err)
 	}
-	return diskStat.Used, diskStat.Total, convertToPercent(float64(diskStat.Used) / float64(diskStat.Total))
+	return diskStat.Used, diskStat.Total, convertToPercent(float64(diskStat.Used) / float64(diskStat.Total)), nil
 }
 
-func getBatteryInfo() batteryStruct {
+func getBatteryInfo() (batteryStruct, error) {
 	// battery
 	batteries, err := battery.GetAll()
 	if err != nil {
-		if strings.Contains(err.Error(), "no such file or directory") {
-			// ignore this happens on [linux on mac devices]
-		} else {
-			log.Fatal().Msg("Error getting battery info")
+		if !strings.Contains(err.Error(), "no such file or directory") {
+			return batteryStruct{}, fmt.Errorf("error getting battery info: %w", err)
 		}
+		// ignore this happens on [linux on mac devices]
 	}
 
 	//// charge stats
@@ -181,9 +198,9 @@ func getBatteryInfo() batteryStruct {
 	case C.BATTERY_NO_CYCLE_COUNT:
 		batteryCycleCount = 0
 	case C.BATTERY_MANAGER_ERROR:
-		log.Fatal().Msg("Battery manager error")
+		return batteryStruct{}, fmt.Errorf("battery manager error")
 	default:
-		log.Fatal().Msg("Unknown error occurred")
+		return batteryStruct{}, fmt.Errorf("unknown error occurred")
 	}
 
 	//// time to empty
@@ -198,9 +215,9 @@ func getBatteryInfo() batteryStruct {
 	case C.BATTERY_TIME_TO_EMPTY_NO_TIME_TO_EMPTY:
 		batteryTimeToEmpty = 0
 	case C.BATTERY_TIME_TO_EMPTY_MANAGER_ERROR:
-		log.Fatal().Msg("Battery manager error")
+		return batteryStruct{}, fmt.Errorf("battery manager error")
 	default:
-		log.Fatal().Msg("Unknown error occurred")
+		return batteryStruct{}, fmt.Errorf("unknown error occurred")
 	}
 
 	// return
@@ -210,7 +227,7 @@ func getBatteryInfo() batteryStruct {
 		BatteryDesignCapacity: batteryDesignCapacity,
 		BatteryCycleCount:     batteryCycleCount,
 		BatteryTimeToEmpty:    batteryTimeToEmpty,
-	}
+	}, nil
 }
 
 // ---- utils ----
internal/get/volumes.go
@@ -9,11 +9,10 @@ import (
 	human "github.com/dustin/go-humanize"
 	"github.com/jedib0t/go-pretty/v6/table"
 	"github.com/kahnwong/swissknife/configs/color"
-	"github.com/rs/zerolog/log"
 	"github.com/shirou/gopsutil/v4/disk"
 )
 
-func listVolumes() {
+func Volumes() error {
 	// ref: <https://stackoverflow.com/a/64141403>
 	// layout inspired by `duf`
 
@@ -25,7 +24,7 @@ func listVolumes() {
 	// get volumes info
 	partitions, err := disk.Partitions(false)
 	if err != nil {
-		log.Fatal().Msg("Error getting partitions info")
+		return fmt.Errorf("error getting partitions info: %w", err)
 	}
 
 	for _, partition := range partitions {
@@ -41,7 +40,7 @@ func listVolumes() {
 			device := partition.Mountpoint
 			stats, err := disk.Usage(device)
 			if err != nil {
-				log.Fatal().Msg("Error getting disk info")
+				return fmt.Errorf("error getting disk info: %w", err)
 			}
 
 			if stats.Total == 0 {
@@ -94,8 +93,5 @@ func listVolumes() {
 
 	// render
 	t.Render()
-}
-
-func Volumes() {
-	listVolumes()
+	return nil
 }
internal/shouldideploytoday/shouldideploytoday.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/carlmjohnson/requests"
 	"github.com/kahnwong/swissknife/configs/color"
-	"github.com/rs/zerolog/log"
 )
 
 type ShouldIDeploy struct {
@@ -17,7 +16,7 @@ type ShouldIDeploy struct {
 	Message       string    `json:"message"`
 }
 
-func getResponse() ShouldIDeploy {
+func getResponse() (ShouldIDeploy, error) {
 	url := "https://shouldideploy.today"
 
 	var response ShouldIDeploy
@@ -30,18 +29,22 @@ func getResponse() ShouldIDeploy {
 		Fetch(context.Background())
 
 	if err != nil {
-		log.Fatal().Msg("Error calling ShouldIDeploy API")
+		return ShouldIDeploy{}, fmt.Errorf("error calling ShouldIDeploy API: %w", err)
 	}
 
-	return response
+	return response, nil
 }
 
-func ShouldIDeployToday() {
-	response := getResponse()
+func ShouldIDeployToday() error {
+	response, err := getResponse()
+	if err != nil {
+		return err
+	}
 
 	if response.ShouldIDeploy {
 		fmt.Printf("%s\n", color.Green(response.Message))
 	} else if !response.ShouldIDeploy {
 		fmt.Printf("%s\n", color.Red(response.Message))
 	}
+	return nil
 }
internal/speedtest/speedtest.go
@@ -6,11 +6,10 @@ import (
 	"time"
 
 	"github.com/kahnwong/swissknife/configs/color"
-	"github.com/rs/zerolog/log"
 	"github.com/showwin/speedtest-go/speedtest"
 )
 
-func SpeedTest() {
+func SpeedTest() error {
 	// ref: <https://github.com/showwin/speedtest-go#api-usage>
 	var speedtestClient = speedtest.New()
 	serverList, _ := speedtestClient.FetchServers()
@@ -24,6 +23,9 @@ func SpeedTest() {
 	tests := make(chan struct{})
 	ctx, cancel := context.WithCancel(context.Background())
 
+	// Store error from goroutine
+	errChan := make(chan error, 1)
+
 	// -- background progress report -- //
 	go func(ctx context.Context) {
 		for {
@@ -45,29 +47,41 @@ func SpeedTest() {
 			fmt.Print("Pinging")
 			err := s.PingTest(nil)
 			if err != nil {
-				log.Fatal().Msg("Error pinging server")
+				errChan <- fmt.Errorf("error pinging server: %w", err)
+				cancel()
+				return
 			}
 			fmt.Printf("\033[2K\r") // clear line
 
 			fmt.Print("Downloading")
 			err = s.DownloadTest()
 			if err != nil {
-				log.Fatal().Msg("Error testing download speed")
+				errChan <- fmt.Errorf("error testing download speed: %w", err)
+				cancel()
+				return
 			}
 			fmt.Printf("\033[2K\r") // clear line
 
 			fmt.Print("Uploading")
 			err = s.UploadTest()
 			if err != nil {
-				log.Fatal().Msg("Error testing upload speed")
+				errChan <- fmt.Errorf("error testing upload speed: %w", err)
+				cancel()
+				return
 			}
 			fmt.Printf("\033[2K\r") // clear line
 		}
 
+		errChan <- nil
 		cancel()
 	}()
 	<-tests
 
+	// Check for errors
+	if err := <-errChan; err != nil {
+		return err
+	}
+
 	// print results
 	fmt.Print(
 		fmt.Sprintf("%s:  %s\n", color.Green("Latency"), s.Latency.Truncate(time.Millisecond)) +
@@ -76,4 +90,5 @@ func SpeedTest() {
 	)
 
 	s.Context.Reset()
+	return nil
 }
internal/stopwatch/stopwatch.go
@@ -3,7 +3,6 @@ package stopwatch
 
 import (
 	"fmt"
-	"os"
 	"time"
 
 	"github.com/charmbracelet/bubbles/help"
@@ -71,7 +70,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return m, cmd
 }
 
-func Stopwatch() {
+func Stopwatch() error {
 	m := model{
 		stopwatch: stopwatch.NewWithInterval(time.Millisecond),
 		keymap: keymap{
@@ -98,7 +97,7 @@ func Stopwatch() {
 	m.keymap.start.SetEnabled(false)
 
 	if _, err := tea.NewProgram(m).Run(); err != nil {
-		fmt.Println("Oh no, it didn't work:", err)
-		os.Exit(1)
+		return fmt.Errorf("stopwatch error: %w", err)
 	}
+	return nil
 }
internal/timer/timer.go
@@ -3,13 +3,11 @@ package timer
 
 import (
 	"fmt"
-	"os"
 	"strings"
 	"time"
 
 	"github.com/charmbracelet/bubbles/progress"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/rs/zerolog/log"
 
 	tea "github.com/charmbracelet/bubbletea"
 )
@@ -113,24 +111,23 @@ func NewModel() Model {
 	}
 }
 
-func Timer(args []string) {
+func Timer(args []string) error {
 	// check if input exists
 	if len(args) == 0 {
-		fmt.Println("Please provide a duration")
-		os.Exit(1)
+		return fmt.Errorf("please provide a duration")
 	}
 	// parse input
 	duration, err := time.ParseDuration(args[0])
 	if err != nil {
-		log.Fatal().Msg("Error parsing duration")
+		return fmt.Errorf("error parsing duration: %w", err)
 	}
 
 	// timer
 	m := NewModel()
 	m.duration = duration
 
-	_, err = tea.NewProgram(&m).Run()
-	if err != nil {
-		log.Fatal().Err(err)
+	if _, err = tea.NewProgram(&m).Run(); err != nil {
+		return fmt.Errorf("error running timer: %w", err)
 	}
+	return nil
 }
internal/utils/args.go
@@ -10,8 +10,8 @@ import (
 func setValueFromArgsOrClipboard(args []string, validator func(string) bool, errorMsg string, allowEmpty bool) string {
 	var value string
 	if len(args) == 0 {
-		clipboardValue := ReadFromClipboard()
-		if clipboardValue != "" && validator(clipboardValue) {
+		clipboardValue, err := ReadFromClipboard()
+		if err == nil && clipboardValue != "" && validator(clipboardValue) {
 			value = strings.TrimSpace(clipboardValue)
 		}
 	}
internal/utils/clipboard.go
@@ -1,44 +1,47 @@
 package utils
 
 import (
+	"fmt"
 	"os"
 
 	"github.com/atotto/clipboard"
-	"github.com/rs/zerolog/log"
 	clipboardImage "github.com/skanehira/clipboard-image/v2"
 )
 
-func WriteToClipboard(text string) {
-	err := clipboard.WriteAll(text)
-	if err != nil {
-		log.Error().Msg("Failed to write to clipboard")
+func WriteToClipboard(text string) error {
+	if err := clipboard.WriteAll(text); err != nil {
+		return fmt.Errorf("failed to write to clipboard: %w", err)
 	}
+	return nil
 }
 
-func WriteToClipboardImage(bytes []byte) {
+func WriteToClipboardImage(bytes []byte) error {
 	tempFilename := "/tmp/qr-image.png"
-	err := os.WriteFile(tempFilename, bytes, 0644)
-	if err != nil {
-		log.Fatal().Msg("Failed to write temp image for clipboard")
+	if err := os.WriteFile(tempFilename, bytes, 0644); err != nil {
+		return fmt.Errorf("failed to write temp image for clipboard: %w", err)
 	}
 
 	f, err := os.Open(tempFilename)
 	if err != nil {
-		log.Fatal().Msg("Failed to open temp image for clipboard")
+		return fmt.Errorf("failed to open temp image for clipboard: %w", err)
 	}
 	defer func(f *os.File) {
-		err := f.Close()
-		if err != nil {
-			log.Error().Msg("Error opening temp image for clipboard")
+		if err := f.Close(); err != nil {
+			// Log this error but don't override the main return error
+			fmt.Fprintf(os.Stderr, "warning: error closing temp image file: %v\n", err)
 		}
 	}(f)
 
 	if err = clipboardImage.Write(f); err != nil {
-		log.Fatal().Msg("Failed to copy to clipboard")
+		return fmt.Errorf("failed to copy to clipboard: %w", err)
 	}
+	return nil
 }
 
-func ReadFromClipboard() string {
-	text, _ := clipboard.ReadAll()
-	return text
+func ReadFromClipboard() (string, error) {
+	text, err := clipboard.ReadAll()
+	if err != nil {
+		return "", fmt.Errorf("failed to read from clipboard: %w", err)
+	}
+	return text, nil
 }
go.sum
@@ -91,10 +91,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
-github.com/projectdiscovery/wappalyzergo v0.2.62 h1:SMZ70bLCj6jHnFgjanuiaQpqUXY6aiEC3YoM0ZSvYes=
-github.com/projectdiscovery/wappalyzergo v0.2.62/go.mod h1:8FtSVcmPRZU0g1euBpdSYEBHIvB7Zz9MOb754ZqZmfU=
-github.com/projectdiscovery/wappalyzergo v0.2.63 h1:iSIU2rfPkHcpBSTol7S3PqgfBXn+JD56s4BsVEGxJ+o=
-github.com/projectdiscovery/wappalyzergo v0.2.63/go.mod h1:8FtSVcmPRZU0g1euBpdSYEBHIvB7Zz9MOb754ZqZmfU=
 github.com/projectdiscovery/wappalyzergo v0.2.64 h1:Y55sb5qUdFvMtR81m1hr54PdGh/hZ4XtuGPdCFAirEk=
 github.com/projectdiscovery/wappalyzergo v0.2.64/go.mod h1:8FtSVcmPRZU0g1euBpdSYEBHIvB7Zz9MOb754ZqZmfU=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=