diff --git a/go.mod b/go.mod index 3806883..2b7603c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,25 @@ module github.com/mdgspace/sysreplicate go 1.24.3 + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum index e69de29..cc7a40a 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,41 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/internal/core/backup/dotfiles_backup.go b/internal/core/backup/dotfiles_backup.go index e69e21d..1448991 100644 --- a/internal/core/backup/dotfiles_backup.go +++ b/internal/core/backup/dotfiles_backup.go @@ -4,7 +4,9 @@ import ( "fmt" "os" "time" - "github.com/mdgspace/sysreplicate/system/output" + + "github.com/mdgspace/sysreplicate/internal/core/generator" + "github.com/mdgspace/sysreplicate/internal/domain" ) type BackupMetadata struct { @@ -28,10 +30,10 @@ func (db *DotfileBackupManager) CreateDotfileBackup(outputTar string) error { hostname, _ := os.Hostname() - // Convert []Dotfile to []output.Dotfile - outputFiles := make([]output.Dotfile, len(files)) + // Convert []Dotfile to []domain.Dotfile + outputFiles := make([]domain.Dotfile, len(files)) for i, file := range files { - outputFiles[i] = output.Dotfile{ + outputFiles[i] = domain.Dotfile{ Path: file.Path, RealPath: file.RealPath, IsDir: file.IsDir, @@ -44,13 +46,13 @@ func (db *DotfileBackupManager) CreateDotfileBackup(outputTar string) error { // Create backup metadata // struct from output - meta := &output.BackupMetadata{ + meta := &domain.BackupMetadata{ Timestamp: time.Now(), Hostname: hostname, Files: outputFiles, } - if err := output.CreateDotfilesBackupTarball(meta, outputTar); err != nil { + if err := generator.CreateDotfilesBackupTarball(meta, outputTar); err != nil { return fmt.Errorf("failed to create backup tarball: %w", err) } diff --git a/internal/core/backup/key.go b/internal/core/backup/key.go index 13aca89..7ae6de8 100644 --- a/internal/core/backup/key.go +++ b/internal/core/backup/key.go @@ -8,7 +8,8 @@ import ( "strings" "time" - "github.com/mdgspace/sysreplicate/system/output" + "github.com/mdgspace/sysreplicate/internal/core/generator" + "github.com/mdgspace/sysreplicate/internal/domain" ) // backup and restore operations @@ -52,10 +53,10 @@ func (bm *BackupManager) CreateBackup(customPaths []string) error { } //create backup data - backupData := &output.BackupData{ + backupData := &domain.BackupData{ Timestamp: time.Now(), SystemInfo: bm.getSystemInfo(), - EncryptedKeys: make(map[string]output.EncryptedKey), + EncryptedKeys: make(map[string]domain.EncryptedKey), EncryptionKey: key, // Store the key in backup data } @@ -73,7 +74,7 @@ func (bm *BackupManager) CreateBackup(customPaths []string) error { fmt.Println("Creating backup tarball...") tarballPath := fmt.Sprintf("dist/key-backup-%s.tar.gz", time.Now().Format("2006-01-02-15-04-05")) - err = output.CreateBackupTarball(backupData, tarballPath) + err = generator.CreateBackupTarball(backupData, tarballPath) if err != nil { return fmt.Errorf("failed to create tarball: %w", err) } @@ -84,7 +85,7 @@ func (bm *BackupManager) CreateBackup(customPaths []string) error { } // processLocation processes a single key location -func (bm *BackupManager) processLocation(location KeyLocation, backupData *output.BackupData) error { +func (bm *BackupManager) processLocation(location KeyLocation, backupData *domain.BackupData) error { for _, filePath := range location.Files { //get file info for permissions fileInfo, err := os.Stat(filePath) @@ -100,7 +101,7 @@ func (bm *BackupManager) processLocation(location KeyLocation, backupData *outpu // store encrypted key keyID := filepath.Base(filePath) + "_" + strings.ReplaceAll(filePath, "/", "_") - backupData.EncryptedKeys[keyID] = output.EncryptedKey{ + backupData.EncryptedKeys[keyID] = domain.EncryptedKey{ OriginalPath: filePath, KeyType: location.Type, EncryptedData: encryptedData, @@ -161,13 +162,13 @@ func (bm *BackupManager) processCustomPaths(customPaths []string) []KeyLocation } // collect basic system information -func (bm *BackupManager) getSystemInfo() output.SystemInfo { +func (bm *BackupManager) getSystemInfo() domain.SystemInfo { hostname, _ := os.Hostname() username := os.Getenv("USER") if username == "" { username = os.Getenv("USERNAME") } - return output.SystemInfo{ + return domain.SystemInfo{ Hostname: hostname, Username: username, OS: "linux", diff --git a/internal/core/backup/restore.go b/internal/core/backup/restore.go index 341728f..58f827c 100644 --- a/internal/core/backup/restore.go +++ b/internal/core/backup/restore.go @@ -13,8 +13,8 @@ import ( "path/filepath" "strings" + "github.com/mdgspace/sysreplicate/internal/core/generator" "github.com/mdgspace/sysreplicate/internal/domain" - "github.com/mdgspace/sysreplicate/system/output" ) type RestoreManager struct { @@ -261,7 +261,7 @@ func (rm *RestoreManager) generateInstallScript() error { return fmt.Errorf("failed to create directory: %w", err) } - return output.GenerateInstallScript(rm.backupData.BaseDistro, rm.backupData.Packages, rm.backupData.Automation, scriptPath) + return generator.GenerateInstallScript(rm.backupData.BaseDistro, rm.backupData.Packages, rm.backupData.Automation, scriptPath) } // decryptData decrypts base64 encoded data using AES-GCM diff --git a/internal/core/backup/unified_backup.go b/internal/core/backup/unified_backup.go index 595526b..0364b48 100644 --- a/internal/core/backup/unified_backup.go +++ b/internal/core/backup/unified_backup.go @@ -13,18 +13,18 @@ import ( "github.com/mdgspace/sysreplicate/internal/domain" "github.com/mdgspace/sysreplicate/system/automation" - "github.com/mdgspace/sysreplicate/system/output" + // "github.com/mdgspace/sysreplicate/system/output" "github.com/mdgspace/sysreplicate/internal/platform" ) // all backup information in one structure type UnifiedBackupData struct { Timestamp time.Time `json:"timestamp"` - SystemInfo output.SystemInfo `json:"system_info"` - EncryptedKeys map[string]output.EncryptedKey `json:"encrypted_keys"` - Dotfiles []output.Dotfile `json:"dotfiles"` + SystemInfo domain.SystemInfo `json:"system_info"` + EncryptedKeys map[string]domain.EncryptedKey `json:"encrypted_keys"` + Dotfiles []domain.Dotfile `json:"dotfiles"` Packages map[string][]string `json:"packages"` - Automation *automation.AutomationData `json:"automation"` + Automation *domain.AutomationData `json:"automation"` EncryptionKey []byte `json:"encryption_key"` Distro string `json:"distro"` BaseDistro string `json:"base_distro"` @@ -78,12 +78,12 @@ func (ubm *UnifiedBackupManager) CreateUnifiedBackup(customPaths []string) error // Create unified backup data backupData := &UnifiedBackupData{ Timestamp: time.Now(), - SystemInfo: output.SystemInfo{ + SystemInfo: domain.SystemInfo{ Hostname: hostname, Username: username, OS: "linux", }, - EncryptedKeys: make(map[string]output.EncryptedKey), + EncryptedKeys: make(map[string]domain.EncryptedKey), EncryptionKey: key, Packages: packages, Distro: distro, @@ -177,7 +177,7 @@ func (ubm *UnifiedBackupManager) backupKeys(customPaths []string, backupData *Un fmt.Printf(" - %s\n", filePath) keyID := filepath.Base(filePath) + "_" + strings.ReplaceAll(filePath, "/", "_") - backupData.EncryptedKeys[keyID] = output.EncryptedKey{ + backupData.EncryptedKeys[keyID] = domain.EncryptedKey{ OriginalPath: filePath, KeyType: location.Type, EncryptedData: encryptedData, @@ -205,7 +205,7 @@ func (ubm *UnifiedBackupManager) backupDotfiles(backupData *UnifiedBackupData) e } // Convert to output format and show details - outputFiles := make([]output.Dotfile, len(files)) + outputFiles := make([]domain.Dotfile, len(files)) dotfileCount := 0 for i, file := range files { @@ -214,7 +214,7 @@ func (ubm *UnifiedBackupManager) backupDotfiles(backupData *UnifiedBackupData) e dotfileCount++ } - outputFiles[i] = output.Dotfile{ + outputFiles[i] = domain.Dotfile{ Path: file.Path, RealPath: file.RealPath, IsDir: file.IsDir, diff --git a/system/output/tarball.go b/internal/core/generator/archive.go similarity index 60% rename from system/output/tarball.go rename to internal/core/generator/archive.go index e1b929c..8b060d6 100644 --- a/system/output/tarball.go +++ b/internal/core/generator/archive.go @@ -1,4 +1,4 @@ -package output +package generator import ( "archive/tar" @@ -7,47 +7,11 @@ import ( "fmt" "io" "os" - "time" + "github.com/mdgspace/sysreplicate/internal/domain" ) -// backupData structure for tarball creation -type BackupData struct { - Timestamp time.Time `json:"timestamp"` - SystemInfo SystemInfo `json:"system_info"` - EncryptedKeys map[string]EncryptedKey `json:"encrypted_keys"` - EncryptionKey []byte `json:"encryption_key"` -} - -type SystemInfo struct { - Hostname string `json:"hostname"` - Username string `json:"username"` - OS string `json:"os"` -} - -type EncryptedKey struct { - OriginalPath string `json:"original_path"` - KeyType string `json:"key_type"` - EncryptedData string `json:"encrypted_data"` - Permissions uint32 `json:"permissions"` -} - -type Dotfile struct { - Path string `json:"path"` - RealPath string `json:"real_path"` - IsDir bool `json:"is_dir"` - IsBinary bool `json:"is_binary"` - Mode os.FileMode `json:"mode"` - Content string `json:"content"` // ignore for the binary files -} - -type BackupMetadata struct { - Timestamp time.Time `json:"timestamp"` - Hostname string `json:"hostname"` - Files []Dotfile `json:"files"` -} - // create a compressed tarball with the backup data -func CreateBackupTarball(backupData *BackupData, tarballPath string) error { +func CreateBackupTarball(backupData *domain.BackupData, tarballPath string) error { //create tarball file file, err := os.Create(tarballPath) if err != nil { @@ -87,7 +51,7 @@ func CreateBackupTarball(backupData *BackupData, tarballPath string) error { return nil } -func CreateDotfilesBackupTarball(meta *BackupMetadata, tarballPath string) error { +func CreateDotfilesBackupTarball(meta *domain.BackupMetadata, tarballPath string) error { // Create the tarball file file, err := os.Create(tarballPath) if err != nil { diff --git a/internal/core/generator/metadata.go b/internal/core/generator/metadata.go new file mode 100644 index 0000000..14c0f26 --- /dev/null +++ b/internal/core/generator/metadata.go @@ -0,0 +1,25 @@ +package generator + +import ( + "encoding/json" + "github.com/mdgspace/sysreplicate/internal/domain" +) + +// BuildSystemJSON creates a well-structured JSON object for the system info and packages. +func GenerateMetadata(osType, distro, baseDistro string, packages map[string][]string) ([]byte, error) { + + // type SystemInfo struct { + // OS string `json:"os"` + // Distro string `json:"distro"` + // BaseDistro string `json:"base_distro"` + // Packages map[string][]string `json:"packages"` + // } + + info := domain.SystemInfoOutput{ + OS: osType, + Distro: distro, + BaseDistro: baseDistro, + Packages: packages, + } + return json.MarshalIndent(info, "", " ") +} diff --git a/system/output/script.go b/internal/core/generator/scripts.go similarity index 97% rename from system/output/script.go rename to internal/core/generator/scripts.go index 94a75be..5b35324 100644 --- a/system/output/script.go +++ b/internal/core/generator/scripts.go @@ -1,4 +1,4 @@ -package output +package generator import ( "fmt" @@ -10,7 +10,7 @@ import ( // generateInstallScript creates a shell script to install all packages for the given distro. // Returns an error if the script cannot be created or written. -func GenerateInstallScript(baseDistro string, packages map[string][]string, automationData *automation.AutomationData, scriptPath string) error { +func GenerateInstallScript(baseDistro string, packages map[string][]string, automationData *domain.AutomationData, scriptPath string) error { f, err := os.Create(scriptPath) if err != nil { return err diff --git a/internal/domain/automation.go b/internal/domain/automation.go new file mode 100644 index 0000000..0578f93 --- /dev/null +++ b/internal/domain/automation.go @@ -0,0 +1,21 @@ +package domain + +type AutomationData struct { + SystemDServices []SystemDUnit `json:"systemd_services"` + SystemDTimers []SystemDUnit `json:"systemd_timers"` + UserCronjobs []Cronjob `json:"user_cronjobs"` + SystemCronjobs []Cronjob `json:"system_cronjobs"` +} +type SystemDUnit struct { + Name string `json:"name"` + Path string `json:"path"` + Content string `json:"content"` + UnitType string `json:"unit_type"` ////saare service, timer and target available ere + IsEnabled bool `json:"is_enabled"` + IsActive bool `json:"is_active"` +} +type Cronjob struct { + Path string `json:"path"` + Content string `json:"content"` + Type string `json:"type"` //.//user, system, cron_d +} \ No newline at end of file diff --git a/internal/domain/metadata.go b/internal/domain/metadata.go new file mode 100644 index 0000000..7d3076d --- /dev/null +++ b/internal/domain/metadata.go @@ -0,0 +1,50 @@ +package domain +import( + "time" + "os" + // "github.com/mdgspace/sysreplicate/system/output" +) + +type SystemInfo struct { //IMP_NOTE!:: this is originally in output/traball.go, but I cannot import output package here as it induces cyclic import error + //so I define the same struct here, and removed it from output/tarball.go + Hostname string `json:"hostname"` + Username string `json:"username"` + OS string `json:"os"` +} + +type SystemInfoOutput struct { // this was in output/json.go + // originally named SystemInfo, which I changed to SystemInfoOutput to avoid conflict with the above SystemInfo + OS string `json:"os"` + Distro string `json:"distro"` + BaseDistro string `json:"base_distro"` + Packages map[string][]string `json:"packages"` +} + +type BackupData struct { + Timestamp time.Time `json:"timestamp"` + SystemInfo SystemInfo `json:"system_info"` + EncryptedKeys map[string]EncryptedKey `json:"encrypted_keys"` + EncryptionKey []byte `json:"encryption_key"` +} + +type EncryptedKey struct { + OriginalPath string `json:"original_path"` + KeyType string `json:"key_type"` + EncryptedData string `json:"encrypted_data"` + Permissions uint32 `json:"permissions"` +} + +type Dotfile struct { + Path string `json:"path"` + RealPath string `json:"real_path"` + IsDir bool `json:"is_dir"` + IsBinary bool `json:"is_binary"` + Mode os.FileMode `json:"mode"` + Content string `json:"content"` // ignore for the binary files +} + +type BackupMetadata struct { + Timestamp time.Time `json:"timestamp"` + Hostname string `json:"hostname"` + Files []Dotfile `json:"files"` +} \ No newline at end of file diff --git a/internal/tui/model.go b/internal/tui/model.go new file mode 100644 index 0000000..e2c784e --- /dev/null +++ b/internal/tui/model.go @@ -0,0 +1,101 @@ +package tui +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" +) + +type model struct { + choices []string // items on list + cursor int // which item our cursor is pointing at + selected int // which items are selected +} + +func initialModel(display_list []string) model { + return model{ + choices: display_list, + selected: 0, + } +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + switch msg := msg.(type) { + case tea.KeyMsg: + + switch msg.String() { + + case "ctrl+c", "q": + m.selected = 6 + return m, tea.Quit + + case "up", "k": + if m.cursor > 0 { + m.cursor-- + } + + case "down", "j": + if m.cursor < len(m.choices)-1 { + m.cursor++ + } + + case "enter", " ": + m.selected = m.cursor+1 + return m, tea.Quit + } + } + + // Return the updated model to the Bubble Tea runtime for processing. + // Note that we're not returning a command. + return m, nil +} + +func (m model) View() string { + var s string + + s += getCustomStyle(80).Render("=== SysReplicate - Distro Hopping Tool ===") + s += "\n\n" + for i, choice := range m.choices { + cursor := " " + if m.cursor == i { + cursor = cursorStyle.Render(">") + } + + checked := " " + if ok := m.selected; ok==i+1 { + checked = checkedStyle.Render("x") + } + + line := fmt.Sprintf("%s [%s] ", cursor, checked) + s += itemStyle.Render(line)+ textStyle.Render(choice) + "\n" + } + + s += "\n" + footerStyle.Render("Press q to quit.") + + return s +} + +func PublicPrint(s []string) { // to use outside this package + for _, str := range s { + fmt.Println(textStyle.Render(str)) + } +} + +func PublicOptions(string_list []string) int { + p := tea.NewProgram(initialModel(string_list)) + m, err := p.Run() + if err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } + return m.(model).selected +} + +// currently how this works: +//When user boots, bubble tea starts, when he selects an option, bubble tea quits and returns the selected option to Run function in run.go +//and idk if I need to replace fmt everywhere, but for now, just made changes to run.go \ No newline at end of file diff --git a/internal/tui/styles.go b/internal/tui/styles.go new file mode 100644 index 0000000..6ef5a92 --- /dev/null +++ b/internal/tui/styles.go @@ -0,0 +1,44 @@ +package tui + +import ( + "github.com/charmbracelet/lipgloss" +) + + +func getCustomStyle(width int) lipgloss.Style { + style := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")). + Background(lipgloss.Color("#7D56F4")). + PaddingTop(2). + PaddingBottom(2). + PaddingLeft(4). + Width(width) + return style +} + +var ( + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")) + + cursorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#7D56F4")). + Bold(true) + + textStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#f1fab7")) + + checkedStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#04B575")) + + itemStyle = lipgloss.NewStyle(). + PaddingLeft(1) + + footerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#626262")) +) + +// func main() { +// fmt.Println(style.Render("Hello, kitty")) +// } \ No newline at end of file diff --git a/system/automation/automation.go b/system/automation/automation.go index 1c84299..e610cd0 100644 --- a/system/automation/automation.go +++ b/system/automation/automation.go @@ -8,25 +8,25 @@ import ( "github.com/mdgspace/sysreplicate/internal/domain" ) -type AutomationData struct { - SystemDServices []SystemDUnit `json:"systemd_services"` - SystemDTimers []SystemDUnit `json:"systemd_timers"` - UserCronjobs []Cronjob `json:"user_cronjobs"` - SystemCronjobs []Cronjob `json:"system_cronjobs"` -} -type SystemDUnit struct { - Name string `json:"name"` - Path string `json:"path"` - Content string `json:"content"` - UnitType string `json:"unit_type"` ////saare service, timer and target available ere - IsEnabled bool `json:"is_enabled"` - IsActive bool `json:"is_active"` -} -type Cronjob struct { - Path string `json:"path"` - Content string `json:"content"` - Type string `json:"type"` //.//user, system, cron_d -} +// type AutomationData struct { +// SystemDServices []SystemDUnit `json:"systemd_services"` +// SystemDTimers []SystemDUnit `json:"systemd_timers"` +// UserCronjobs []Cronjob `json:"user_cronjobs"` +// SystemCronjobs []Cronjob `json:"system_cronjobs"` +// } +// type SystemDUnit struct { +// Name string `json:"name"` +// Path string `json:"path"` +// Content string `json:"content"` +// UnitType string `json:"unit_type"` ////saare service, timer and target available ere +// IsEnabled bool `json:"is_enabled"` +// IsActive bool `json:"is_active"` +// } +// type Cronjob struct { +// Path string `json:"path"` +// Content string `json:"content"` +// Type string `json:"type"` //.//user, system, cron_d +// } type AutomationManager struct { username string } @@ -40,14 +40,14 @@ func NewAutomationManager() *AutomationManager { username: username, } } -func (am *AutomationManager) DetectAutomation() (*AutomationData, error) { +func (am *AutomationManager) DetectAutomation() (*domain.AutomationData, error) { fmt.Println("Detecting automation files...") - data := &AutomationData{ - SystemDServices: make([]SystemDUnit, 0), - SystemDTimers: make([]SystemDUnit, 0), - UserCronjobs: make([]Cronjob, 0), - SystemCronjobs: make([]Cronjob, 0), + data := &domain.AutomationData{ + SystemDServices: make([]domain.SystemDUnit, 0), + SystemDTimers: make([]domain.SystemDUnit, 0), + UserCronjobs: make([]domain.Cronjob, 0), + SystemCronjobs: make([]domain.Cronjob, 0), } systemdServices, systemdTimers, err := am.detectSystemDUnits() diff --git a/system/automation/backup.go b/system/automation/backup.go index 742b088..19c9deb 100644 --- a/system/automation/backup.go +++ b/system/automation/backup.go @@ -5,9 +5,10 @@ import ( "fmt" "path/filepath" "strings" + "github.com/mdgspace/sysreplicate/internal/domain" ) -func (am *AutomationManager) BackupAutomation(data *AutomationData, tarWriter *tar.Writer) error { +func (am *AutomationManager) BackupAutomation(data *domain.AutomationData, tarWriter *tar.Writer) error { fmt.Println("Adding automation files to backup...") for _, service := range data.SystemDServices { @@ -52,7 +53,7 @@ func (am *AutomationManager) addFileToTarball(originalPath, content, tarballPref return nil } ///TODO(@jaadu): IMPROVE THE RESTORE LOGIC AND RESTORATION COMMAND -func (am *AutomationManager) GenerateRestorationCommands(data *AutomationData) []string { +func (am *AutomationManager) GenerateRestorationCommands(data *domain.AutomationData) []string { var commands []string if len(data.SystemDServices) > 0 || len(data.SystemDTimers) > 0 { commands = append(commands, "echo 'Restoring SystemD units...'") @@ -104,7 +105,7 @@ func (am *AutomationManager) GenerateRestorationCommands(data *AutomationData) [ return commands } -func (am *AutomationManager) ValidateAutomationData(data *AutomationData) error { +func (am *AutomationManager) ValidateAutomationData(data *domain.AutomationData) error { unitNames := make(map[string]bool) for _, service := range data.SystemDServices { diff --git a/system/automation/detect.go b/system/automation/detect.go index b5cdfac..aecb389 100644 --- a/system/automation/detect.go +++ b/system/automation/detect.go @@ -9,6 +9,10 @@ import ( "github.com/mdgspace/sysreplicate/internal/domain" ) +type SystemDUnit = domain.SystemDUnit +type Cronjob = domain.Cronjob +type AutomationData = domain.AutomationData + func (am *AutomationManager) detectSystemDUnits() ([]SystemDUnit, []SystemDUnit, error) { var services []SystemDUnit var timers []SystemDUnit diff --git a/system/output/json.go b/system/output/json.go deleted file mode 100644 index 0b791d9..0000000 --- a/system/output/json.go +++ /dev/null @@ -1,24 +0,0 @@ -package output - -import ( - "encoding/json" -) - -// BuildSystemJSON creates a well-structured JSON object for the system info and packages. -func BuildSystemJSON(osType, distro, baseDistro string, packages map[string][]string) ([]byte, error) { - - type SystemInfo struct { - OS string `json:"os"` - Distro string `json:"distro"` - BaseDistro string `json:"base_distro"` - Packages map[string][]string `json:"packages"` - } - - info := SystemInfo{ - OS: osType, - Distro: distro, - BaseDistro: baseDistro, - Packages: packages, - } - return json.MarshalIndent(info, "", " ") -} diff --git a/system/run.go b/system/run.go index 477b3e3..5702ef1 100644 --- a/system/run.go +++ b/system/run.go @@ -1,75 +1,67 @@ package system import ( - "bufio" - "fmt" + // "bufio" + // "fmt" "log" "os" "runtime" - "strings" + // "strings" - "github.com/mdgspace/sysreplicate/system/output" - "github.com/mdgspace/sysreplicate/internal/platform" + "github.com/mdgspace/sysreplicate/internal/core/generator" "github.com/mdgspace/sysreplicate/internal/domain" + "github.com/mdgspace/sysreplicate/internal/platform" + "github.com/mdgspace/sysreplicate/internal/tui" ) +var string_list = []string{"1. Create Complete System Backup (Recommended)", +"2. Restore System from Backup", +"3. Generate package replication files only", +"4. Backup SSH/GPG keys only", +"5. Backup dotfiles only", +} + // Run is the entry point for the system orchestrator. func Run() { osType := runtime.GOOS - fmt.Println("Detected OS Type:", osType) + tui.PublicPrint([]string{"Detected OS Type:", osType}) switch osType { case "darwin": - fmt.Println("MacOS is not supported") + tui.PublicPrint([]string{"MacOS is not supported"}) return case "windows": - fmt.Println("Windows is not supported") + tui.PublicPrint([]string{"Windows is not supported"}) return case "linux": + // tui.PublicOptions(string_list) showMenu() ////main menu component default: - fmt.Println("OS not supported") + tui.PublicPrint([]string{"OS not supported"}) } } // showMenu displays the main menu for Linux users func showMenu() { - scanner := bufio.NewScanner(os.Stdin) - - for { - fmt.Println("\n=== SysReplicate - Distro Hopping Tool ===") - fmt.Println("1. Create Complete System Backup (Recommended)") - fmt.Println("2. Restore System from Backup") - fmt.Println("3. Generate package replication files only") - fmt.Println("4. Backup SSH/GPG keys only") - fmt.Println("5. Backup dotfiles only") - fmt.Println("6. Exit") - fmt.Print("Choose an option (1-6): ") - - if !scanner.Scan() { - break - } - - choice := strings.TrimSpace(scanner.Text()) - + choice := tui.PublicOptions(string_list) switch choice { - case "1": + case 1: RunUnifiedBackup() - case "2": + case 2: RunRestore() - case "3": + case 3: runPackageReplication() - case "4": + case 4: RunBackup() - case "5": + case 5: RunDotfileBackup() - case "6": - fmt.Println("Goodbye Captain!") + case 6: + tui.PublicPrint([]string{"Goodbye Captain!"}) return default: - fmt.Println("Invalid choice. Please select 1-6.") + tui.PublicPrint([]string{"Invalid choice. Please select 1-6."}) } - } + } // this handles the original package replication functionality @@ -80,11 +72,11 @@ func runPackageReplication() { return } - fmt.Println("Distribution:", distro) - fmt.Println("Built On:", baseDistro) + tui.PublicPrint([]string{"Distribution:", distro}) + tui.PublicPrint([]string{"Built On:", baseDistro}) packages := platform.FetchPackages(baseDistro) - jsonObj, err := output.BuildSystemJSON("linux", distro, baseDistro, packages) + jsonObj, err := generator.GenerateMetadata("linux", distro, baseDistro, packages) if err != nil { log.Println("Error marshalling JSON:", err) return @@ -105,9 +97,9 @@ func runPackageReplication() { return } - if err := output.GenerateInstallScript(baseDistro, packages, nil, domain.ScriptOutputPath); err != nil { + if err := generator.GenerateInstallScript(baseDistro, packages, nil, domain.ScriptOutputPath); err != nil { log.Println("Error generating install script:", err) } else { - fmt.Println("Script generated successfully at:", domain.ScriptOutputPath) + tui.PublicPrint([]string{"Script generated successfully at:", domain.ScriptOutputPath}) } }