@ -1,21 +1,34 @@
package main
package main
import (
import (
"bytes"
"encoding/base64"
"encoding/base64"
"fmt"
"fmt"
"log"
"log"
"net/http"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"strings"
"sync"
"sync/atomic"
"sync/atomic"
)
)
const (
gitRepoURL = "https://git.cz0.cz/czoczo/BetterBash"
localRepoPath = "BetterBashRepo" // Cloned into a subdirectory
bbShellPath = "prompt/bb.sh"
)
var (
var (
// requestCounter stores the number of HTTP requests (excluding /stats).
// requestCounter stores the number of HTTP requests (excluding /stats and /reload ).
requestCounter uint64
requestCounter uint64
// repoMutex protects git operations like clone/pull
repoMutex sync . Mutex
)
)
// colorComponentKeys defines the names for the color components in their encoding order.
// colorComponentKeys defines the names for the color components in their encoding order.
// The encoding process described uses 9 components, but output is only for the first 8.
var colorComponentKeys = [ ] string {
var colorComponentKeys = [ ] string {
"PRIMARY_COLOR" ,
"PRIMARY_COLOR" ,
"SECONDARY_COLOR" ,
"SECONDARY_COLOR" ,
@ -28,81 +41,140 @@ var colorComponentKeys = []string{
// The 9th component (C9, potentially "Reset") is decoded but not listed for output.
// The 9th component (C9, potentially "Reset") is decoded but not listed for output.
}
}
// decodeAndPrintColorsHandler processes requests with encoded color data.
// decodeColorLogic processes the encoded data string and returns color information.
func decodeAndPrintColorsHandler ( w http . ResponseWriter , r * http . Request ) {
// It returns a map of color keys to bash color values, a formatted string list of these, and an error.
encodedData := strings . TrimPrefix ( r . URL . Path , "/" )
func decodeColorLogic ( encodedData string ) ( map [ string ] string , string , error ) {
// 1. Convert URL-safe Base64 characters back to standard Base64.
// The Vue code replaces '+' with '-', '/' with '_', and removes padding.
standardBase64 := strings . ReplaceAll ( encodedData , "-" , "+" )
standardBase64 := strings . ReplaceAll ( encodedData , "-" , "+" )
standardBase64 = strings . ReplaceAll ( standardBase64 , "_" , "/" )
standardBase64 = strings . ReplaceAll ( standardBase64 , "_" , "/" )
// 2. Decode Base64 string to bytes.
// RawStdEncoding handles Base64 without padding.
// The Vue code generates an 8-character string from 6 bytes, which means no padding.
decodedBytes , err := base64 . RawStdEncoding . DecodeString ( standardBase64 )
decodedBytes , err := base64 . RawStdEncoding . DecodeString ( standardBase64 )
if err != nil {
if err != nil {
http . Error ( w , fmt . Sprintf ( "Base64 decoding failed: %v. Input: '%s'" , err , encodedData ) , http . StatusBadRequest )
return nil , "" , fmt . Errorf ( "Base64 decoding failed: %v. Input: '%s'" , err , encodedData )
return
}
}
if len ( decodedBytes ) != 6 {
if len ( decodedBytes ) != 6 {
http . Error ( w , fmt . Sprintf ( "Decoded data must be 6 bytes long, got %d bytes from input '%s'" , len ( decodedBytes ) , encodedData ) , http . StatusBadRequest )
return nil , "" , fmt . Errorf ( "Decoded data must be 6 bytes long, got %d bytes from input '%s'" , len ( decodedBytes ) , encodedData )
return
}
}
// 3. Unpack 6 bytes into nine 5-bit values (C1 to C9).
// Based on the Vue frontend's "Byte Packing" description:
// Byte 1: C1_b4 C1_b3 C1_b2 C1_b1 C1_b0 C2_b4 C2_b3 C2_b2
// Byte 2: C2_b1 C2_b0 C3_b4 C3_b3 C3_b2 C3_b1 C3_b0 C4_b4
// Byte 3: C4_b3 C4_b2 C4_b1 C4_b0 C5_b4 C5_b3 C5_b2 C5_b1
// Byte 4: C5_b0 C6_b4 C6_b3 C6_b2 C6_b1 C6_b0 C7_b4 C7_b3
// Byte 5: C7_b2 C7_b1 C7_b0 C8_b4 C8_b3 C8_b2 C8_b1 C8_b0
// Byte 6: C9_b4 C9_b3 C9_b2 C9_b1 C9_b0 0 0 0 (padding)
fiveBitValues := make ( [ ] byte , 9 )
fiveBitValues := make ( [ ] byte , 9 )
b := decodedBytes
b := decodedBytes
fiveBitValues [ 0 ] = b [ 0 ] >> 3
fiveBitValues [ 1 ] = ( ( b [ 0 ] & 0x07 ) << 2 ) | ( b [ 1 ] >> 6 )
fiveBitValues [ 2 ] = ( b [ 1 ] & 0x3E ) >> 1
fiveBitValues [ 3 ] = ( ( b [ 1 ] & 0x01 ) << 4 ) | ( b [ 2 ] >> 4 )
fiveBitValues [ 4 ] = ( ( b [ 2 ] & 0x0F ) << 1 ) | ( b [ 3 ] >> 7 )
fiveBitValues [ 5 ] = ( b [ 3 ] & 0x7C ) >> 2
fiveBitValues [ 6 ] = ( ( b [ 3 ] & 0x03 ) << 3 ) | ( b [ 4 ] >> 5 )
fiveBitValues [ 7 ] = b [ 4 ] & 0x1F
fiveBitValues [ 8 ] = b [ 5 ] >> 3 // C9, not used in direct output string but decoded
fiveBitValues [ 0 ] = b [ 0 ] >> 3 // C1
colorsMap := make ( map [ string ] string )
fiveBitValues [ 1 ] = ( ( b [ 0 ] & 0x07 ) << 2 ) | ( b [ 1 ] >> 6 ) // C2
var resultList strings . Builder
fiveBitValues [ 2 ] = ( b [ 1 ] & 0x3E ) >> 1 // C3
fiveBitValues [ 3 ] = ( ( b [ 1 ] & 0x01 ) << 4 ) | ( b [ 2 ] >> 4 ) // C4
fiveBitValues [ 4 ] = ( ( b [ 2 ] & 0x0F ) << 1 ) | ( b [ 3 ] >> 7 ) // C5
fiveBitValues [ 5 ] = ( b [ 3 ] & 0x7C ) >> 2 // C6
fiveBitValues [ 6 ] = ( ( b [ 3 ] & 0x03 ) << 3 ) | ( b [ 4 ] >> 5 ) // C7
fiveBitValues [ 7 ] = b [ 4 ] & 0x1F // C8
fiveBitValues [ 8 ] = b [ 5 ] >> 3 // C9
var result strings . Builder
// 4. For each of the first 8 components, generate the bash color code.
for i := 0 ; i < 8 ; i ++ { // Only first 8 components for output
for i := 0 ; i < 8 ; i ++ {
val5bit := fiveBitValues [ i ]
val5bit := fiveBitValues [ i ]
baseColor07 := ( val5bit >> 2 ) & 0x07
lightBit := ( val5bit >> 1 ) & 0x01
boldBit := val5bit & 0x01
// Extract attributes from the 5-bit value:
baseAnsiCode := baseColor07 + 30
// (base_color_0_7 << 2) | (light_bit << 1) | bold_bit
baseColor07 := ( val5bit >> 2 ) & 0x07 // 3 MSB for base color (0-7)
lightBit := ( val5bit >> 1 ) & 0x01 // 1 bit for light state
boldBit := val5bit & 0x01 // 1 bit for bold state
// Convert to ANSI color codes
baseAnsiCode := baseColor07 + 30 // Base colors are 30-37
actualAnsiCode := baseAnsiCode
actualAnsiCode := baseAnsiCode
if lightBit == 1 {
if lightBit == 1 {
actualAnsiCode += 60 // Bright colors are 90-97
actualAnsiCode += 60
}
}
styleAttr := 0
styleAttr := 0 // Style: 0 for normal
if boldBit == 1 {
if boldBit == 1 {
styleAttr = 1 // Style: 1 for bold
styleAttr = 1
}
}
// Format as bash-compatible string: \[\033[<style>;<color>m\]
bashColor := fmt . Sprintf ( ` \[\033[%d;%dm\] ` , styleAttr , actualAnsiCode )
bashColor := fmt . Sprintf ( ` \[\033[%d;%dm\] ` , styleAttr , actualAnsiCode )
result . WriteString ( fmt . Sprintf ( "%s=%s\n" , colorComponentKeys [ i ] , bashColor ) )
colorsMap [ colorComponentKeys [ i ] ] = bashColor
resultList . WriteString ( fmt . Sprintf ( "%s='%s'\n" , colorComponentKeys [ i ] , bashColor ) )
}
}
return colorsMap , strings . TrimSpace ( resultList . String ( ) ) , nil
}
// serveDecodedColorsOnlyHandler serves only the decoded color definitions.
func serveDecodedColorsOnlyHandler ( w http . ResponseWriter , r * http . Request , encodedData string ) {
_ , formattedOutput , err := decodeColorLogic ( encodedData )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
fmt . Fprint ( w , result . String ( ) )
fmt . Fprint ( w , formattedOutput )
}
// serveFileWithColorsHandler serves files from the repo, potentially modifying bb.sh.
func serveFileWithColorsHandler ( w http . ResponseWriter , r * http . Request , encodedData string , requestedFilePath string ) {
colorsMap , _ , err := decodeColorLogic ( encodedData )
if err != nil {
http . Error ( w , fmt . Sprintf ( "Failed to decode colors: %v" , err ) , http . StatusBadRequest )
return
}
// Sanitize file path
cleanFilePath := filepath . Clean ( requestedFilePath )
if strings . HasPrefix ( cleanFilePath , ".." ) || strings . HasPrefix ( cleanFilePath , "/" ) {
http . Error ( w , "Invalid file path." , http . StatusBadRequest )
return
}
fullPath := filepath . Join ( localRepoPath , cleanFilePath )
// Security check: ensure the path is still within the localRepoPath
absRepoPath , _ := filepath . Abs ( localRepoPath )
absFilePath , _ := filepath . Abs ( fullPath )
if ! strings . HasPrefix ( absFilePath , absRepoPath ) {
http . Error ( w , "Access to file path denied." , http . StatusForbidden )
return
}
fileInfo , err := os . Stat ( fullPath )
if os . IsNotExist ( err ) {
http . Error ( w , fmt . Sprintf ( "File not found: %s" , requestedFilePath ) , http . StatusNotFound )
return
}
if err != nil {
http . Error ( w , fmt . Sprintf ( "Error accessing file: %v" , err ) , http . StatusInternalServerError )
return
}
if fileInfo . IsDir ( ) {
http . Error ( w , fmt . Sprintf ( "Requested path is a directory: %s" , requestedFilePath ) , http . StatusBadRequest )
return
}
if cleanFilePath == bbShellPath {
// Special handling for prompt/bb.sh
content , err := os . ReadFile ( fullPath )
if err != nil {
http . Error ( w , fmt . Sprintf ( "Error reading %s: %v" , bbShellPath , err ) , http . StatusInternalServerError )
return
}
modifiedScript := string ( content )
for key , colorValue := range colorsMap {
// Regex to find lines like `KEY='...'`, `export KEY="..."`, or `KEY=value`
// It captures `export ` (if present) and the key name.
// It replaces the whole line.
// We need to be careful with shell syntax. For simplicity, we assume KEY='...'
// For robust parsing, a proper shell parser would be needed.
// This regex looks for `KEY=`, potentially with `export` prefix, and replaces value.
// It assumes the value is single-quoted or simply the rest of the line.
// Example: PRIMARY_COLOR='...' or export PRIMARY_COLOR='...'
regex := regexp . MustCompile ( fmt . Sprintf ( ` ^(export\s+)?%s\s*=\s*('.*?'|".*?"|[^#\s]*) ` , regexp . QuoteMeta ( key ) ) )
replacement := fmt . Sprintf ( "${1}%s='%s'" , key , colorValue )
modifiedScript = regex . ReplaceAllString ( modifiedScript , replacement )
}
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
fmt . Fprint ( w , modifiedScript )
} else {
// Serve other files as is
http . ServeFile ( w , r , fullPath )
}
}
}
// statsReportHandler serves the HTTP request counter.
// statsReportHandler serves the HTTP request counter.
@ -114,36 +186,176 @@ func statsReportHandler(w http.ResponseWriter, r *http.Request) {
// rootPathHandler handles requests to the bare "/" path.
// rootPathHandler handles requests to the bare "/" path.
func rootPathHandler ( w http . ResponseWriter , r * http . Request ) {
func rootPathHandler ( w http . ResponseWriter , r * http . Request ) {
http . Error ( w , "Please provide an encoded string in the URL path, e.g., /YourEncodedString" , http . StatusBadRequest )
http . Error ( w , "Please provide an encoded string in the URL path (e.g., /YourEncodedString) or a file path (e.g. /YourEncodedString/path/to/file.sh)" , http . StatusBadRequest )
}
// runGitCommand executes a git command and logs its output.
func runGitCommand ( dir string , args ... string ) error {
cmd := exec . Command ( "git" , args ... )
if dir != "" {
cmd . Dir = dir
}
var out bytes . Buffer
var stderr bytes . Buffer
cmd . Stdout = & out
cmd . Stderr = & stderr
log . Printf ( "Running git command: git %s in dir '%s'" , strings . Join ( args , " " ) , dir )
err := cmd . Run ( )
if out . Len ( ) > 0 {
log . Printf ( "git stdout: %s" , out . String ( ) )
}
if stderr . Len ( ) > 0 {
log . Printf ( "git stderr: %s" , stderr . String ( ) )
}
if err != nil {
return fmt . Errorf ( "git %s failed: %v\nstderr: %s" , strings . Join ( args , " " ) , err , stderr . String ( ) )
}
return nil
}
// setupRepo clones the repository if it doesn't exist locally.
func setupRepo ( ) error {
repoMutex . Lock ( )
defer repoMutex . Unlock ( )
if _ , err := os . Stat ( localRepoPath ) ; os . IsNotExist ( err ) {
log . Printf ( "Local repository not found at %s. Cloning %s..." , localRepoPath , gitRepoURL )
if err := runGitCommand ( "" , "clone" , gitRepoURL , localRepoPath ) ; err != nil {
return fmt . Errorf ( "failed to clone repository: %w" , err )
}
log . Printf ( "Repository cloned successfully into %s." , localRepoPath )
} else {
log . Printf ( "Local repository found at %s. Skipping clone." , localRepoPath )
// Optionally, pull latest on startup too
// log.Printf("Pulling latest changes for %s...", localRepoPath)
// if err := runGitCommand(localRepoPath, "pull", "origin", "master"); err != nil {
// return fmt.Errorf("failed to pull repository on startup: %w", err)
// }
// log.Printf("Repository updated successfully.")
}
return nil
}
// reloadRepoHandler handles the /reload endpoint to pull the latest from git.
func reloadRepoHandler ( w http . ResponseWriter , r * http . Request ) {
repoMutex . Lock ( )
defer repoMutex . Unlock ( )
log . Printf ( "Attempting to pull latest changes for repository at %s" , localRepoPath )
// Ensure the repo exists before trying to pull
if _ , err := os . Stat ( localRepoPath ) ; os . IsNotExist ( err ) {
log . Printf ( "Local repository at %s does not exist. Cloning first." , localRepoPath )
if err := runGitCommand ( "" , "clone" , gitRepoURL , localRepoPath ) ; err != nil {
http . Error ( w , fmt . Sprintf ( "Failed to clone repository during reload: %v" , err ) , http . StatusInternalServerError )
return
}
log . Printf ( "Repository cloned successfully during reload." )
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
fmt . Fprintln ( w , "Repository was missing, cloned successfully." )
return
}
// Attempt to reset any local changes and then pull
// This is a forceful way to ensure it matches origin/master
// Be cautious with `git reset --hard` if local changes could be important,
// but for a cache/mirror this is often desired.
err := runGitCommand ( localRepoPath , "fetch" , "origin" )
if err == nil {
err = runGitCommand ( localRepoPath , "reset" , "--hard" , "origin/master" )
}
// Fallback to simple pull if reset fails or as primary strategy
if err != nil {
log . Printf ( "Hard reset failed (or fetch failed): %v. Attempting simple pull." , err )
err = runGitCommand ( localRepoPath , "pull" , "origin" , "master" )
}
if err != nil {
errMsg := fmt . Sprintf ( "Failed to pull latest changes: %v" , err )
log . Println ( errMsg )
http . Error ( w , errMsg , http . StatusInternalServerError )
return
}
log . Println ( "Repository reloaded successfully." )
w . Header ( ) . Set ( "Content-Type" , "text/plain; charset=utf-8" )
fmt . Fprintln ( w , "Repository reloaded successfully." )
}
}
// mainRouter is the primary HTTP handler.
// mainRouter is the primary HTTP handler.
// It routes requests to appropriate handlers and manages request counting.
func mainRouter ( w http . ResponseWriter , r * http . Request ) {
func mainRouter ( w http . ResponseWriter , r * http . Request ) {
// Handle /stats endpoint separately; it does not increment the counter.
if r . URL . Path == "/stats" {
if r . URL . Path == "/stats" {
statsReportHandler ( w , r )
statsReportHandler ( w , r )
return
return
}
}
// For all other paths, increment the request counter.
// Increment request counter for non-stats paths.
// Reload is also a functional path so we count it.
atomic . AddUint64 ( & requestCounter , 1 )
atomic . AddUint64 ( & requestCounter , 1 )
// Handle the root path "/"
if r . URL . Path == "/reload" {
reloadRepoHandler ( w , r )
return
}
if r . URL . Path == "/" {
if r . URL . Path == "/" {
rootPathHandler ( w , r )
rootPathHandler ( w , r )
} else {
return
// Assume any other path contains encoded data.
}
decodeAndPrintColorsHandler ( w , r )
// Path structure: /<encoded_data>[/<file_path>]
trimmedPath := strings . TrimPrefix ( r . URL . Path , "/" )
parts := strings . SplitN ( trimmedPath , "/" , 2 )
encodedData := parts [ 0 ]
if encodedData == "" { // Path was just "/" or "//..."
rootPathHandler ( w , r ) // Or a more specific error
return
}
if len ( parts ) == 1 {
// Only /<encoded_data>
serveDecodedColorsOnlyHandler ( w , r , encodedData )
} else if len ( parts ) == 2 {
// /<encoded_data>/<file_path>
filePath := parts [ 1 ]
if filePath == "" { // e.g. /encodedData/
http . Error ( w , "File path cannot be empty if a second slash is provided." , http . StatusBadRequest )
return
}
serveFileWithColorsHandler ( w , r , encodedData , filePath )
}
}
// SplitN with 2 parts will always result in len 1 or 2 if input is not empty.
// If trimmedPath was empty, parts would be {""}, len 1. This is caught by encodedData==""
}
}
func main ( ) {
func main ( ) {
// Clone the repository at startup if it doesn't exist
if err := setupRepo ( ) ; err != nil {
log . Fatalf ( "❌ Failed to setup repository: %s\n" , err )
// Depending on strictness, you might want to os.Exit(1) or try to run without the repo functionality
}
http . HandleFunc ( "/" , mainRouter )
http . HandleFunc ( "/" , mainRouter )
port := "8080"
port := "8080"
log . Printf ( "🎨 Color Theme Backend starting on port %s (e.g., http://localhost:%s)\n" , port , port )
if p := os . Getenv ( "PORT" ) ; p != "" {
// Listen on all available network interfaces.
port = p
}
log . Printf ( "🎨 Color Theme Backend & File Server starting on port %s (e.g., http://localhost:%s)\n" , port , port )
log . Printf ( "Git Repo URL: %s" , gitRepoURL )
log . Printf ( "Local Repo Path: ./%s" , localRepoPath )
log . Printf ( "Special file for color replacement: %s" , bbShellPath )
log . Printf ( "Endpoints:" )
log . Printf ( " GET /<encoded_color_data> - Show color definitions" )
log . Printf ( " GET /<encoded_color_data>/<path> - Serve file from repo (e.g., /VcrS_H8A/removebb.sh)" )
log . Printf ( " Special: /<encoded_color_data>/%s for dynamic colors" , bbShellPath )
log . Printf ( " GET /reload - Pull latest from git master branch" )
log . Printf ( " GET /stats - Show request count" )
log . Printf ( " GET / - Show usage instructions" )
if err := http . ListenAndServe ( ":" + port , nil ) ; err != nil {
if err := http . ListenAndServe ( ":" + port , nil ) ; err != nil {
log . Fatalf ( "❌ Failed to start server: %s\n" , err )
log . Fatalf ( "❌ Failed to start server: %s\n" , err )
}
}