From fe962ed662b414d8bf609c616682a6b79830a799 Mon Sep 17 00:00:00 2001 From: brayan Date: Fri, 27 Jun 2025 10:30:16 -0600 Subject: [PATCH 01/21] add initial implementation of TUI --- cmd/config/tui.go | 1471 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1471 insertions(+) create mode 100644 cmd/config/tui.go diff --git a/cmd/config/tui.go b/cmd/config/tui.go new file mode 100644 index 000000000..2510e041c --- /dev/null +++ b/cmd/config/tui.go @@ -0,0 +1,1471 @@ +/* + Licensed under the MIT License . + + Copyright © 2023-2025 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2025 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + +package main + +import ( + "fmt" + "net/url" + "os" + "strconv" + "strings" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + "gopkg.in/yaml.v3" +) + + +var ( + accountName = "my-account" + storageProtocols = []string {"s3storage", "azstorage"} + storageProtocol = "s3storage" + storageProviders = []string{"LyveCloud", "Microsoft", "AWS", "Other"} + storageProvider = "LyveCloud" + cacheModes = []string {"stream", "file_cache", "block_cache"} + cacheMode = "file_cache" + bucketName = "my-bucket" + cacheLocation = "/var/cache/s3storage" + cacheSize = "80" + cacheRetentionDuration = "30" + cacheRetentionUnit = "Days" + endpointURL = "https://s3.sv15.seagate.com" + region = "us-east-1" + previewPage = "page1" + accessKey = "STX11NXL2MJ9OKXCQVWKHS41" + secretKey = "IC3iz6sL+/fEB1MUaKln2lAsQW2FPU+ySM5/xndGy8m" + menuButtonColor = tcell.GetColor("#6EBE49") + menuButtonTextColor = tcell.ColorBlack + menuButtonAlignment = tview.AlignLeft +) + + +type Config struct { + Logging LoggingConfig `yaml:"logging"` + Components []string `yaml:"components"` + Libfuse LibfuseConfig `yaml:"libfuse"` + Stream StreamConfig `yaml:"stream"` + FileCache FileCacheConfig `yaml:"file_cache"` + BlockCache BlockCacheConfig `yaml:"block_cache"` + AttrCache AttrCacheConfig `yaml:"attr_cache"` + S3Storage S3StorageConfig `yaml:"s3storage"` + AzStorage AzureStorageConfig `yaml:"azstorage"` // Optional field for Azure storage +} + +type LoggingConfig struct { + Type string `yaml:"type"` + Level string `yaml:"level"` +} + +type LibfuseConfig struct { + AttributeExpirationSec int `yaml:"attribute-expiration-sec"` + EntryExpirationSec int `yaml:"entry-expiration-sec"` + NegativeEntryExpirationSec int `yaml:"negative-entry-expiration-sec"` + NetworkShare bool `yaml:"network-share"` +} + +type StreamConfig struct { + BlockSizeMB int `yaml:"block-size-mb"` + BlocksPerFile int `yaml:"blocks-per-file"` + CacheSizeMB int `yaml:"cache-size-mb"` +} + +type FileCacheConfig struct { + Path string `yaml:"path"` + TimeOutSec int `yaml:"timeout-sec"` + CleanUpOnStart bool `yaml:"cleanup-on-start"` + IgnoreSync bool `yaml:"ignore-sync"` +} + +type BlockCacheConfig struct { + BlockSizeMB int `yaml:"block-size-mb"` + MemorySizeMB int `yaml:"mem-size-mb"` + Prefetch int `yaml:"prefetch"` + Parallelism int `yaml:"parallelism"` +} + + +type AttrCacheConfig struct { + TimeoutSec int `yaml:"timeout-sec"` +} + +type S3StorageConfig struct { + BucketName string `yaml:"bucket-name"` + KeyID string `yaml:"key-id"` + SecretKey string `yaml:"secret-key"` + Endpoint string `yaml:"endpoint"` + Region string `yaml:"region"` + EnableDirMarker bool `yaml:"enable-dir-marker"` +} + + +type AzureStorageConfig struct { + Type string `yaml:"type"` + AccountName string `yaml:"account-name"` + AccountKey string `yaml:"account-key"` + Endpoint string `yaml:"endpoint"` + Mode string `yaml:"mode"` + Container string `yaml:"container"` +} + + +func main() { + app := tview.NewApplication() + app.EnableMouse(true) + app.EnablePaste(true) + + buildTUI(app) + + if err := app.Run(); err != nil { + panic(err) + } + + // After the TUI is done, create the YAML config file + createYAMLConfig() +} + + +func buildTUI(app *tview.Application) { + pages := tview.NewPages() + + // --- Home Page --- + homePage := buildHomePage(app, pages) + + // --- Page 1: Storage Type Selection --- + page1 := buildStorageSelectionPage(app, pages) + + // --- Page 2: Endpoint & Region Entry --- + page2 := buildEndpointRegionPage(app, pages) + + // --- Page 3: Credentials Entry --- + page3 := buildCredentialsPage(app, pages) + + // --- Page 4: Bucket Name Entry --- + page4 := buildBucketNamePage(app, pages) + + // --- Page 5: Caching Settings --- + page5 := buildCachingPage(app, pages) + + // --- Add pages to the page stack --- + pages.AddPage("home", homePage, true, true) + pages.AddPage("page1", page1, true, false) + pages.AddPage("page2", page2, true, false) + pages.AddPage("page3", page3, true, false) + pages.AddPage("page4", page4, true, false) + pages.AddPage("page5", page5, true, false) + + app.SetRoot(pages, true) +} + + +func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { + // Banner / welcome message + bannerText := "[#6EBE49::b]░█▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + + "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + + "[white::b]Welcome to the CloudFuse Configuration Tool\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + + "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" + + bannerView := tview.NewTextView(). + SetText(bannerText). + SetTextAlign(tview.AlignCenter). + SetDynamicColors(true). + SetWrap(true) + + // Instructions + instructionsView := tview.NewTextView(). + SetText("[::b]Instructions:[-:-]\n" + + "[#6EBE49]•[-] Use your [::b]mouse[-:-] or [::b]arrow keys[-:-] to navigate.\n" + + "[#6EBE49]•[-] Press [::b]Enter[-:-] to select items.\n" + + "[#6EBE49]•[-] For the best experience, expand terminal window to full size.\n"). + SetDynamicColors(true). + SetTextAlign(tview.AlignLeft). + SetWrap(true) + + // Dropdown hint + jumpToView := tview.NewTextView(). + SetText("[::i]Tip: Use the dropdown below to quickly jump to any step.[::-]"). + SetTextAlign(tview.AlignLeft). + SetDynamicColors(true). + SetWrap(true) + + // Start / Quit buttons + startQuitWidget := tview.NewForm(). + AddButton("🚀 Start", func() { + pages.SwitchToPage("page1") + }). + AddButton("❌ Quit", func() { + app.Stop() + }). + SetButtonBackgroundColor(tcell.GetColor("#6EBE49")). + SetButtonTextColor(tcell.ColorWhite). + SetButtonsAlign(tview.AlignCenter) + + // Dropdown to jump between pages + jumpToWidget := tview.NewForm(). + AddDropDown("Jump to:", []string{ + " Storage Selection ⬇️ ", + " Endpoint & Region ", + " Credentials ", + " Bucket Name ", + " Caching Settings ", + }, 0, func(option string, index int) { + pages.SwitchToPage(fmt.Sprintf("page%d", index+1)) + }). + SetLabelColor(tcell.GetColor("#FFD700")). + SetFieldBackgroundColor(tcell.GetColor("#FFD700")) + + // About section + aboutView := tview.NewTextView(). + SetText("[::b]ABOUT[-]\n" + + "[gray]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "CloudFuse TUI Configuration Tool\n\n" + + "Seagate Technology, LLC\n" + + "cloudfuse@seagate.com\n\n" + + "Version: 1.0.0"). + + SetDynamicColors(true). + SetTextAlign(tview.AlignCenter). + SetWrap(true) + + // Assemble layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(nil, 1, 0, false). // Top padding + AddItem(bannerView, 10, 0, false). // Banner + AddItem(nil, 1, 0, false). // Banner and start/quit padding + AddItem(startQuitWidget, 3, 0, false). // Start/Quit buttons + AddItem(nil, 1, 0, false). // Padding between buttons and instructions + AddItem(instructionsView, 4, 0, false). // Instructions + AddItem(nil, 2, 0, false). // Padding between instructions and dropdown hint + AddItem(jumpToView, 1, 0, false). + AddItem(jumpToWidget, 3, 0, false). + AddItem(nil, 2, 0, false). + AddItem(aboutView, 9, 0, false). // New About section + AddItem(nil, 1, 0, false) // Bottom padding + layout.SetBorder(true).SetBorderColor(tcell.GetColor("#6EBE49")).SetBorderPadding(1, 1, 1, 1) + + return layout +} + + +func buildStorageSelectionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + // Header / section banner + headerText := "[#6EBE49::b]Step 1: Select Your Cloud Storage Provider[-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + + "[white]Choose your cloud storage provider from the dropdown below.\n\n" + + "If your provider is not listed, choose [::b]Other[::-] and you’ll be prompted " + + "to enter the endpoint URL and region manually." + + pageText := tview.NewTextView(). + SetText(headerText). + SetTextAlign(tview.AlignCenter). + SetDynamicColors(true). + SetWrap(true) + + // Dropdown for storage provider + storageProviderDropdown := tview.NewDropDown(). + SetLabel("📦 Storage Provider: "). + SetOptions([]string{" LyveCloud ⬇️", " Microsoft ", " AWS ", " Other "}, func(option string, index int) { + storageProvider = option + switch option { + case " LyveCloud ⬇️": + storageProtocol = "s3storage" + storageProvider = "LyveCloud" + case " Microsoft ": + storageProtocol = "azstorage" + storageProvider = "Microsoft" + case " AWS ": + storageProtocol = "s3storage" + storageProvider = "AWS" + case " Other ": + storageProtocol = "s3storage" + storageProvider = "Other" + default: + storageProtocol = "s3storage" + storageProvider = "LyveCloud" + } + }). + SetCurrentOption(0). + SetLabelColor(tcell.GetColor("#FFD700")). + SetFieldBackgroundColor(tcell.GetColor("#FFD700")). + SetFieldWidth(14) + + + // Navigation buttons + form := tview.NewForm(). + // AddFormItem(storageProviderDropdown). + AddButton("🏠 Home", func() { + pages.SwitchToPage("home") + }). + AddButton("➡ Next", func() { + page2 := buildEndpointRegionPage(app, pages) + pages.AddPage("page2", page2, true, false) + pages.SwitchToPage("page2") + }). + AddButton("📄 Preview", func() { + summaryPage := buildSummaryPage(app, pages) + pages.AddPage("summaryPage", summaryPage, true, false) + pages.SwitchToPage("summaryPage") + }). + AddButton("❌ Quit", func() { + app.Stop() + }). + SetButtonBackgroundColor(menuButtonColor). + SetButtonTextColor(menuButtonTextColor). + SetButtonsAlign(tview.AlignCenter) + + // Layout assembly + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(nil, 1, 0, false). // Top padding + AddItem(pageText, 7, 0, false). // Header and instructions + AddItem(nil, 1, 0, false). // Spacing + AddItem(storageProviderDropdown, 3, 0, false). // Dropdown for storage provider + AddItem(form, 6, 0, false). // Dropdown + nav buttons + AddItem(nil, 1, 0, false) // Bottom padding + + return layout +} + + +// func buildEndpointRegionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + +// var regions []string +// var regionInput tview.FormItem + +// // Switch case to set the default URL and region based on the selected storage provider +// switch storageProvider { +// case "LyveCloud": +// urlRegionHelpText = "For LyveCloud, the endpoint URL general format is: \n\n" + +// "\t[darkmagenta::b]https://s3.<[-][darkcyan::b]region[-][darkmagenta::b]>.lyvecloud.seagate.com[-]\n\n" + +// "For example, if your region is \"us-east-1\", the endpoint URL would be:\n\n" + +// "\t[darkmagenta::b]https://s3.us-east-1.seagate.com[-]\n\n" + +// "You can also use the LyveCloud portal to find your storage account endpoint.\n" + +// "The available regions for Seagate LyveCloud are listed in the dropdown below." +// urlText = "https://s3.sv15.seagate.com" +// regionText = "us-east-1" +// regions = lyvecloudRegions +// case "Microsoft": +// urlRegionHelpText = "For Microsoft Azure, the endpoint URL general format is:\n\n" + +// "\t[darkmagenta::b]https://<[-][darkcyan::b]storage-account-name[-][darkmagenta::b]>.<[-][darkcyan::b]service[-][darkmagenta::b]>.core.windows.net[-]\n\n" + +// "For example, if your storage account name is \"mystorageaccount\" and the\n" + +// "service is \"file\", the endpoint URL would be:\n\n" + +// "\t[darkmagenta::b]https://mystorageaccount.file.core.windows.net[-]\n\n" + +// "You can also use the Azure portal to find your storage account endpoint.\n" + +// "The available regions for Microsoft Azure are listed in the dropdown below." +// urlText = "https://.file.core.windows.net" +// regionText = "us-east" +// regions = azureRegions +// case "AWS": +// urlRegionHelpText = "For AWS S3, the endpoint URL general format is:\n\n" + +// "\t[darkmagenta::b]https://s3.<[-][darkcyan::b]region[-][darkmagenta::b]>.amazonaws.com[-]\n\n" + +// "For example, if your region is \"us-east-1\", the endpoint URL would be:\n\n" + +// "\t[darkmagenta::b]https://s3.us-east-1.amazonaws.com[-]\n\n" + +// "You can also use the AWS Management Console to find your S3 bucket endpoint.\n" + +// "The available regions for AWS S3 are listed in the dropdown below." +// urlText = "https://s3.amazonaws.com" +// regionText = "us-east-1" +// regions = awsRegions +// case "Other": +// urlText = "https://your-storage-endpoint.com" +// regionText = "your-region" // Default for 'Other' +// default: +// urlText = "https://s3.sv15.seagate.com" // Default for LyveCloud +// regionText = "us-east-1" // Default region +// } + +// pageText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(fmt.Sprintf("[green::b]Endpoint URL and Region for %s:[-] \n\n%s", storageProvider, urlRegionHelpText)) + +// urlInput := tview.NewInputField(). +// SetLabel("Endpoint URL:"). +// SetText(urlText). +// SetFieldWidth(60). +// SetChangedFunc(func(text string) { +// urlText = text +// }) + +// // Dropdown for region selection based on storage type +// // var regions []string +// // var regionInput tview.FormItem + +// if storageProvider != "Other" { +// regionInput = tview.NewDropDown(). +// SetLabel("Region:"). +// SetOptions(regions, func(text string, index int) { +// regionText = text +// }). +// SetCurrentOption(0) + +// } else { +// regionInput = tview.NewInputField(). +// SetLabel("Region:"). +// SetText("Enter Region (i.e., us-east-1)"). +// SetFieldWidth(30). +// SetChangedFunc(func(text string) { +// regionText = text +// }) +// } + +// form := tview.NewForm(). +// AddFormItem(urlInput). +// AddFormItem(regionInput). +// AddButton("Home", func() { +// pages.SwitchToPage("home") +// }). +// AddButton("Next", func() { +// // Normalize and validate the URL +// _, err := validateURL(urlText) +// if err != nil { +// showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { +// pages.SwitchToPage("page2") +// }) +// return +// } + +// // Move to the next page +// pages.SwitchToPage("page3") +// }). +// AddButton("Back", func() { +// pages.SwitchToPage("page1") +// }). +// AddButton("Preview", func() { +// // Normalize and validate URL +// _, err := validateURL(urlText) +// if err != nil { +// showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { +// pages.SwitchToPage("page2") +// }) +// return +// } + +// previewPage = "page2" +// summaryPage := buildSummaryPage(app, pages) +// pages.AddPage("summaryPage", summaryPage, true, false) +// pages.SwitchToPage("summaryPage") // Switch to Page 3 +// }). +// AddButton("Quit", func() { +// app.Stop() +// }) + +// // form.SetBorder(true).SetTitle("[ Endpoint and Region ]").SetTitleAlign(tview.AlignLeft) +// form.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) + +// layout := tview.NewFlex(). +// SetDirection(tview.FlexRow). +// AddItem(nil, 1, 1, false). // Top padding +// AddItem(pageText, 13, 1, false). // Main content +// AddItem(form, 10, 1, false). // Form for endpoint +// AddItem(nil, 1, 1, false) // Bottom padding + +// return layout +// } + + + +func buildEndpointRegionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + var regions []string + var regionInput tview.FormItem + urlRegionHelpText := "" + + // Determine URL, region, and help text based on selected provider + switch storageProvider { + case "LyveCloud": + urlRegionHelpText = `[::b]For LyveCloud, the endpoint URL format is:[-] + + [darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.lyvecloud.seagate.com[-] + +Example: + [darkmagenta::b] https://s3.us-east-1.seagate.com[-] + +Find more info in your LyveCloud portal. +Available regions are shown in the dropdown below.` + endpointURL = "https://s3.sv15.seagate.com" + region = "us-east-1" + regions = lyvecloudRegions + + case "Microsoft": + urlRegionHelpText = `[::b]For Microsoft Azure, the endpoint URL format is:[-] + + [darkmagenta::b] https://<[darkcyan::b]account-name[darkmagenta::b]>.<[darkcyan::b]service[darkmagenta::b]>.core.windows.net[-] + +Example: + [darkmagenta::b] https://mystorageaccount.file.core.windows.net[-] + +Find more info in the Azure portal. +Available regions are listed below.` + endpointURL = "https://.file.core.windows.net" + region = "us-east" + regions = azureRegions + + case "AWS": + urlRegionHelpText = `[::b]For AWS S3, the endpoint URL format is:[-] + + [darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-] + +Example: + [darkmagenta::b] https://s3.us-east-1.amazonaws.com[-] + +Use the AWS Console to find your bucket endpoint. +Available regions are listed in the dropdown.` + endpointURL = "https://s3.amazonaws.com" + region = "us-east-1" + regions = awsRegions + + case "Other": + urlRegionHelpText = `[::b]You selected a custom provider.[-] + +Enter the endpoint URL and region manually. +Refer to your provider’s documentation for valid formats.` + endpointURL = "https://your-storage-endpoint.com" + region = "your-region" + default: + endpointURL = "https://s3.sv15.seagate.com" + region = "us-east-1" + } + + // Header and help text + header := fmt.Sprintf(`[#6EBE49::b]Step 2: Enter Endpoint & Region for %s[-] +[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[white] +%s`, storageProvider, urlRegionHelpText) + + pageText := tview.NewTextView(). + SetText(header). + SetTextAlign(tview.AlignCenter). + SetWrap(true). + SetDynamicColors(true) + + // URL input field + urlInput := tview.NewInputField(). + SetLabel("🔗 Endpoint URL: "). + SetText(endpointURL). + SetFieldWidth(60). + SetChangedFunc(func(text string) { + endpointURL = text + }). + SetLabelColor(tcell.ColorYellow). + SetFieldTextColor(tcell.ColorWhite). + SetFieldBackgroundColor(tcell.ColorBlue) + + // Region input (dropdown or manual) + if storageProvider != "Other" { + regionInput = tview.NewDropDown(). + SetLabel("🌐 Region: "). + SetOptions(regions, func(text string, index int) { + region = text + }). + SetCurrentOption(0). + SetLabelColor(tcell.ColorYellow). + SetFieldTextColor(tcell.ColorWhite). + SetFieldBackgroundColor(tcell.ColorBlue) + } else { + regionInput = tview.NewInputField(). + SetLabel("🌐 Region: "). + SetText("Enter Region (e.g., us-east-1)"). + SetFieldWidth(30). + SetLabelColor(tcell.ColorYellow). + SetFieldTextColor(tcell.ColorWhite). + SetFieldBackgroundColor(tcell.ColorBlue). + SetChangedFunc(func(text string) { + region = text + }) + } + + // Navigation form + form := tview.NewForm(). + AddFormItem(urlInput). + AddFormItem(regionInput). + AddButton("🏠 Home", func() { + pages.SwitchToPage("home") + }). + AddButton("➡ Next", func() { + if _, err := validateURL(endpointURL); err != nil { + showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { + pages.SwitchToPage("page2") + }) + return + } + pages.SwitchToPage("page3") + }). + AddButton("⬅ Back", func() { + pages.SwitchToPage("page1") + }). + AddButton("📄 Preview", func() { + if _, err := validateURL(endpointURL); err != nil { + showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { + pages.SwitchToPage("page2") + }) + return + } + previewPage = "page2" + summaryPage := buildSummaryPage(app, pages) + pages.AddPage("summaryPage", summaryPage, true, false) + pages.SwitchToPage("summaryPage") + }). + AddButton("❌ Quit", func() { + app.Stop() + }). + SetButtonBackgroundColor(menuButtonColor). + SetButtonTextColor(menuButtonTextColor). + SetButtonsAlign(tview.AlignCenter) + + // Final layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(nil, 1, 0, false). + AddItem(pageText, 14, 0, false). + AddItem(nil, 1, 0, false). + AddItem(form, 10, 0, true). + AddItem(nil, 1, 0, false) + + return layout +} + + +// func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +// // Placeholder for credentials page +// pageText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`[green::b]Enter your Cloud Storage Credentials:[-] + +// [yellow::b]Access Key:[-] This is your unique identifier for accessing your cloud storage. +// [yellow::b]Secret Key:[-] This is your secret password for accessing your cloud storage. +// [::i](Make sure to keep these credentials secure and do not share them with anyone.)[-]`) + +// form := tview.NewForm() + +// accessKeyField := tview.NewInputField(). +// SetLabel("Access Key:"). +// SetText(accessKey). // needs to be deleted after testing +// SetFieldWidth(24) + +// secretKeyField := tview.NewInputField(). +// SetLabel("Secret Key:"). +// SetText(secretKey). // needs to be deleted after testing +// SetFieldWidth(43). +// SetMaskCharacter('*') + +// form. +// AddFormItem(accessKeyField). +// AddFormItem(secretKeyField). +// AddButton("Home", func() { +// pages.SwitchToPage("home") +// }). +// AddButton("Next", func() { +// // Validate credentials here: make sure that the keys are 20 characters long, only alphanumeric characters, no special characters +// accessKey := accessKeyField.GetText() +// secretKey := secretKeyField.GetText() +// // Convert to uppercase +// accessKey = strings.ToUpper(accessKey) +// // Check prefixes +// // if !strings.HasPrefix(accessKey, "AKIA") { +// // accessKey = "AKIA" + accessKey +// // } + +// if len(accessKey) != 24 || len(secretKey) != 43 { +// showModal(app, pages, "Invalid credentials.\nPlease try again.", func() { +// pages.SwitchToPage("page3") +// }) +// return +// } +// pages.SwitchToPage("page4") +// }). +// AddButton("Back", func() { +// pages.SwitchToPage("page2") +// }). +// AddButton("Preview", func() { +// summaryPage := buildSummaryPage(app, pages) +// pages.AddPage("summaryPage", summaryPage, true, false) +// pages.SwitchToPage("summaryPage") +// }). +// AddButton("Quit", func() { +// app.Stop() +// }) + +// // form.SetBorder(true).SetTitle("[ Credentials ]").SetTitleAlign(tview.AlignLeft) +// form.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) + +// layout := tview.NewFlex(). +// SetDirection(tview.FlexRow). +// AddItem(nil, 1, 1, false). // Top padding +// AddItem(pageText, 5, 1, false). // Main content +// AddItem(form, 8, 1, false). // Form for credentials +// AddItem(nil, 1, 1, false) // Bottom padding + +// return layout +// } + + +func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + // Instructional text with consistent style + pageText := tview.NewTextView(). + SetTextAlign(tview.AlignCenter). + SetWrap(true). + SetDynamicColors(true). + SetText(`[#6EBE49::b]Step 3: Enter Your Cloud Storage Credentials[-] +[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[white] +[#FFD700::b]Access Key:[-] This is your unique identifier for accessing your cloud storage. +[#FFD700::b]Secret Key:[-] This is your secret password for accessing your cloud storage. + +[::i]Please keep these credentials secure and do not share them with anyone.[-]`) + + // Access key input field + accessKeyField := tview.NewInputField(). + SetLabel("🔑 Access Key: "). + SetText(accessKey). // For testing – remove in production + SetFieldWidth(24). + SetLabelColor(tcell.ColorYellow). + SetFieldTextColor(tcell.ColorWhite). + SetFieldBackgroundColor(tcell.ColorBlue) + + // Secret key input field + secretKeyField := tview.NewInputField(). + SetLabel("🔑 Secret Key: "). + SetText(secretKey). // For testing – remove in production + SetFieldWidth(43). + SetMaskCharacter('*'). + SetLabelColor(tcell.ColorYellow). + SetFieldTextColor(tcell.ColorWhite). + SetFieldBackgroundColor(tcell.ColorBlue) + + // Credential form + form := tview.NewForm(). + AddFormItem(accessKeyField). + AddFormItem(secretKeyField). + AddButton("🏠 Home", func() { + pages.SwitchToPage("home") + }). + AddButton("➡ Next", func() { + accessKey := strings.ToUpper(accessKeyField.GetText()) + secretKey := secretKeyField.GetText() + + if len(accessKey) != 24 || len(secretKey) != 43 { + showModal(app, pages, "Invalid credentials.\nPlease try again.", func() { + pages.SwitchToPage("page3") + }) + return + } + pages.SwitchToPage("page4") + }). + AddButton("⬅ Back", func() { + pages.SwitchToPage("page2") + }). + AddButton("📄 Preview", func() { + summaryPage := buildSummaryPage(app, pages) + pages.AddPage("summaryPage", summaryPage, true, false) + pages.SwitchToPage("summaryPage") + }). + AddButton("❌ Quit", func() { + app.Stop() + }). + SetButtonBackgroundColor(menuButtonColor). + SetButtonTextColor(menuButtonTextColor). + SetButtonsAlign(tview.AlignCenter) + + // Final layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(nil, 1, 0, false). // Top padding + AddItem(pageText, 9, 0, false). // Instructional text + AddItem(nil, 1, 0, false). + AddItem(form, 9, 0, true). // Credential input form + AddItem(nil, 1, 0, false) // Bottom padding + + return layout +} + +// func buildBucketNamePage(app *tview.Application, pages *tview.Pages) tview.Primitive { + +// pageText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`[green::b]Select your Bucket/Container Name:[-] + +// The bucket/container names available for your cloud storage provider are +// listed below in the dropdown. The available bucket/container names are based +// on the credentials you entered in the previous step.`) + +// bucketNameField := tview.NewInputField(). +// SetLabel("Bucket/Container Name:"). +// SetText("my-bucket"). +// SetFieldWidth(30) + +// form := tview.NewForm(). +// AddFormItem(bucketNameField). +// AddButton("Home", func() { +// pages.SwitchToPage("home") +// }). +// AddButton("Next", func() { +// bucketName = bucketNameField.GetText() +// if bucketName == "" { +// showModal(app, pages, "Bucket/container name cannot be empty.\nPlease try again.", func() { +// pages.SwitchToPage("page4") +// }) +// return +// } +// // Proceed to the next step or page +// pages.SwitchToPage("page5") +// }). +// AddButton("Back", func() { +// pages.SwitchToPage("page3") +// }). +// AddButton("Preview", func() { +// summaryPage := buildSummaryPage(app, pages) +// pages.AddPage("summaryPage", summaryPage, true, false) +// pages.SwitchToPage("summaryPage") // Switch to Page 3 +// }). +// AddButton("Quit", func() { +// app.Stop() +// }) + +// // form.SetBorder(true).SetTitle("[ Bucket/Container Name ]").SetTitleAlign(tview.AlignLeft) +// form.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) + +// layout := tview.NewFlex(). +// SetDirection(tview.FlexRow). +// AddItem(nil, 1, 1, false). // Top padding +// AddItem(pageText, 5, 1, false). // Main content +// AddItem(form, 8, 1, false). // Form for bucket +// AddItem(nil, 1, 1, false) // Bottom padding + +// return layout +// } + + +func buildBucketNamePage(app *tview.Application, pages *tview.Pages) tview.Primitive { + + pageText := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(`[#6EBE49::b]Step 4: Select Your Bucket or Container Name[-] +[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[white] +Enter the name of your storage bucket or container. These should be accessible +based on the credentials you entered in the previous step.`) + + // Bucket name input + bucketNameField := tview.NewInputField(). + SetLabel("🪣 Bucket/Container Name: "). + SetText("my-bucket"). + SetFieldWidth(30). + SetLabelColor(tcell.ColorYellow). + SetFieldTextColor(tcell.ColorWhite). + SetFieldBackgroundColor(tcell.ColorBlue) + + // Form with navigation + form := tview.NewForm(). + AddFormItem(bucketNameField). + AddButton("🏠 Home", func() { + pages.SwitchToPage("home") + }). + AddButton("➡ Next", func() { + bucketName = bucketNameField.GetText() + if strings.TrimSpace(bucketName) == "" { + showModal(app, pages, "Bucket/container name cannot be empty.\nPlease try again.", func() { + pages.SwitchToPage("page4") + }) + return + } + pages.SwitchToPage("page5") + }). + AddButton("⬅ Back", func() { + pages.SwitchToPage("page3") + }). + AddButton("📄 Preview", func() { + summaryPage := buildSummaryPage(app, pages) + pages.AddPage("summaryPage", summaryPage, true, false) + pages.SwitchToPage("summaryPage") + }). + AddButton("❌ Quit", func() { + app.Stop() + }). + SetButtonBackgroundColor(menuButtonColor). + SetButtonTextColor(menuButtonTextColor). + SetButtonsAlign(tview.AlignCenter) + + // Final layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(nil, 1, 0, false). // Top padding + AddItem(pageText, 7, 0, false). // Instructional text + AddItem(nil, 1, 0, false). + AddItem(form, 9, 0, true). // Input form + AddItem(nil, 1, 0, false) // Bottom padding + + return layout +} + +// func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +// // Placeholder for caching page +// pageText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`[green::b]Configure Caching Settings:[-] + +// To help Cloudfuse work best for you, let's determine how you'd like to use your local storage.`) + + +// localCacheText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`Do you have available local storage that you'd like +// Cloudfuse to use for improved performance and reliability?`) + +// cacheToDisk := tview.NewDropDown(). +// SetLabel("Cache to local disk? "). +// SetOptions([]string{" Yes ", " No "}, func(text string, index int) { +// if text == "Yes" { +// // Enable disk caching settings +// } else { +// // Disable disk caching settings +// } +// }). +// SetCurrentOption(0) + +// cacheLocationText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`If you selected "Yes" above, please enter the location of the cache directory. +// For example, /var/cache/s3storage or /tmp/s3cache.`) + +// cacheLocationField := tview.NewInputField(). +// SetLabel("Cache Location:"). +// SetText("/var/cache/s3storage"). +// SetFieldWidth(30). +// SetChangedFunc(func(text string) { +// // Validate cache location input +// if text == "" { +// showModal(app, pages, "Cache location cannot be empty.\nPlease try again.", func() { +// pages.SwitchToPage("page5") +// }) +// return +// } +// // Update cache location if needed +// cacheLocation = text + +// }) + +// cacheSizeText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`We've detected [X GB] available at this location. +// By default, Cloudfuse will use up to [80% of X GB] for its cache. +// Would you like to specify a different cache size (in GB)?`) + +// // Cache size input field. How much space (at most) do they want the cache to take up (default is 80% of available space on the drive which contains the directory they entered above) +// cacheSizeField := tview.NewInputField(). +// SetLabel("Cache Size:"). +// SetText("80"). // Default to 80% +// SetFieldWidth(10). +// SetChangedFunc(func(text string) { +// // Validate cache size input +// if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { +// showModal(app, pages, "Cache size must be between 1 and 100.\nPlease try again.", func() { +// pages.SwitchToPage("page5") +// }) +// return +// } +// cacheSize = text // Update cache size +// }) + +// cacheRetentionText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`Do you need cached data to be automatically removed from +// local storage after a certain amount of time since its last access?`) + + +// cacheRetention := tview.NewCheckbox(). +// SetLabel("Enable Cache Retention?"). +// SetChecked(false). +// SetChangedFunc(func(checked bool) { +// if checked { +// // Enable cache retention settings +// } else { +// // Disable cache retention settings +// } +// }) + +// cacheRetentionDurationText := tview.NewTextView(). +// SetTextAlign(tview.AlignLeft). +// SetWrap(true). +// SetDynamicColors(true). +// SetText(`If you selected "Yes" above, please enter the duration and +// select the unit for cache retention from the dropdown`) + +// cacheRetentionDurationUnit := tview.NewForm(). +// AddInputField("Cache Retention Duration:", "30", 10, nil, func(text string) { +// // Validate cache retention duration input +// cacheRetentionDuration = text +// }). +// AddDropDown("Unit:", []string{"Seconds", "Minutes", "Hours", "Days"}, 0, func(option string, index int) { +// retentionUnit = option +// }) + + +// menuButtons := tview.NewForm(). +// AddButton("Home", func() { +// pages.SwitchToPage("home") +// }). +// AddButton("Finish", func() { +// app.Stop() +// }). +// AddButton("Back", func() { +// pages.SwitchToPage("page4") +// }). +// AddButton("Preview", func() { +// summaryPage := buildSummaryPage(app, pages) +// pages.AddPage("summaryPage", summaryPage, true, false) +// pages.SwitchToPage("summaryPage") +// }). +// AddButton("Quit", func() { +// app.Stop() +// }) + +// // form.SetBorder(true).SetTitle("[ Caching Settings ]").SetTitleAlign(tview.AlignLeft) +// menuButtons.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) + +// layout := tview.NewFlex(). +// SetDirection(tview.FlexRow). +// AddItem(nil, 1, 1, false). // Top padding +// AddItem(pageText, 5, 1, false). // Main content +// AddItem(localCacheText, 3, 1, false). // Local +// AddItem(cacheToDisk, 3, 1, false). // Cache to disk dropdown +// AddItem(cacheLocationText, 3, 1, false). // Cache location text +// AddItem(cacheLocationField, 3, 1, false). // Cache location +// AddItem(cacheSizeText, 3, 1, false). // Cache +// AddItem(cacheSizeField, 3, 1, false). // Cache size input +// AddItem(cacheRetentionText, 3, 1, false). // Cache retention text +// AddItem(cacheRetention, 3, 1, false). // Cache retention +// AddItem(cacheRetentionDurationText, 3, 1, false). // Cache retention +// AddItem(cacheRetentionDurationUnit, 5, 1, false). // Cache retention +// AddItem(menuButtons, 3, 1, false). // Menu buttons +// AddItem(nil, 1, 1, false) // Bottom padding + +// return layout +// } + + +func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + pageText := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(`[#6EBE49::b]Step 5: Configure Caching Settings[-] +[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[white] +To optimize performance and reliability, you can allow CloudFuse to cache +data locally on your disk. You can customize where, how much, and for how long +this cache is used.`) + + localCacheText := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(`[::b]💾 Do you want to enable local caching?[-:-] +Enable this if you have enough local storage available. Cached data improves +performance and resilience when the cloud is temporarily unavailable.`) + + cacheToDisk := tview.NewDropDown(). + SetLabel("📁 Cache to Local Disk: "). + SetOptions([]string{" Yes ", " No "}, func(text string, index int) { + // optional logic could be added to enable/disable below fields dynamically + }). + SetCurrentOption(0). + SetLabelColor(tcell.ColorYellow). + SetFieldBackgroundColor(tcell.ColorBlue). + SetFieldTextColor(tcell.ColorWhite) + + cacheLocationText := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(`[::b]📂 Cache Directory Location:[-:-] +Enter the absolute path to a directory where CloudFuse can store cached files. +Example: [blue]/var/cache/s3storage[-] or [blue]/tmp/cloudcache[-]`) + + cacheLocationField := tview.NewInputField(). + SetLabel("📍 Cache Location: "). + SetText("/var/cache/s3storage"). + SetFieldWidth(40). + SetLabelColor(tcell.ColorYellow). + SetFieldBackgroundColor(tcell.ColorBlue). + SetFieldTextColor(tcell.ColorWhite). + SetChangedFunc(func(text string) { + if strings.TrimSpace(text) == "" { + showModal(app, pages, "Cache location cannot be empty.\nPlease try again.", func() { + pages.SwitchToPage("page5") + }) + return + } + cacheLocation = text + }) + + cacheSizeText := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(`[::b]🧠 Cache Size (in GB):[-:-] +Specify how much disk space to allow CloudFuse for cache storage. +Recommended default is 80%% of available space on the chosen drive.`) + + cacheSizeField := tview.NewInputField(). + SetLabel("📦 Cache Size (GB): "). + SetText("80"). + SetFieldWidth(10). + SetLabelColor(tcell.ColorYellow). + SetFieldBackgroundColor(tcell.ColorBlue). + SetFieldTextColor(tcell.ColorWhite). + SetChangedFunc(func(text string) { + if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { + showModal(app, pages, "Cache size must be between 1 and 100.\nPlease try again.", func() { + pages.SwitchToPage("page5") + }) + return + } + cacheSize = text + }) + + cacheRetentionText := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(`[::b]🕒 Cache Retention Settings:[-:-] +You can optionally have cached files auto-deleted if they haven’t been +accessed in a while.`) + + cacheRetention := tview.NewCheckbox(). + SetLabel("🧹 Enable Cache Retention: "). + SetChecked(false). + SetLabelColor(tcell.ColorYellow). + SetChangedFunc(func(checked bool) { + // Logic could enable/disable retention duration input dynamically + }) + + cacheRetentionDurationText := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(`[::b]⏳ Retention Duration:[-:-] +If retention is enabled, enter the duration and unit below. +For example: 30 [blue]Days[-] or 12 [blue]Hours[-].`) + + cacheRetentionDurationUnit := tview.NewForm(). + AddInputField("⏱ Duration:", "30", 10, nil, func(text string) { + cacheRetentionDuration = text + }). + AddDropDown("🕰 Unit:", []string{"Seconds", "Minutes", "Hours", "Days"}, 0, func(option string, index int) { + cacheRetentionUnit = option + }). + SetLabelColor(tcell.ColorYellow). + SetFieldBackgroundColor(tcell.ColorBlue). + SetFieldTextColor(tcell.ColorWhite) + + // Navigation buttons + menuButtons := tview.NewForm(). + AddButton("🏠 Home", func() { + pages.SwitchToPage("home") + }). + AddButton("✅ Finish", func() { + app.Stop() + }). + AddButton("⬅ Back", func() { + pages.SwitchToPage("page4") + }). + AddButton("📄 Preview", func() { + summaryPage := buildSummaryPage(app, pages) + pages.AddPage("summaryPage", summaryPage, true, false) + pages.SwitchToPage("summaryPage") + }). + AddButton("❌ Quit", func() { + app.Stop() + }). + SetButtonBackgroundColor(menuButtonColor). + SetButtonTextColor(menuButtonTextColor). + SetButtonsAlign(tview.AlignCenter) + + // Layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(nil, 1, 0, false). // Top padding + AddItem(pageText, 6, 0, false). + AddItem(localCacheText, 3, 0, false). + AddItem(cacheToDisk, 2, 0, false). + AddItem(cacheLocationText, 3, 0, false). + AddItem(cacheLocationField, 2, 0, false). + AddItem(cacheSizeText, 3, 0, false). + AddItem(cacheSizeField, 2, 0, false). + AddItem(cacheRetentionText, 3, 0, false). + AddItem(cacheRetention, 2, 0, false). + AddItem(cacheRetentionDurationText, 2, 0, false). + AddItem(cacheRetentionDurationUnit, 4, 0, false). + AddItem(menuButtons, 3, 0, false). + AddItem(nil, 1, 0, false) // Bottom padding + + return layout +} + + +// func buildSummaryPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +// // Rebuild the modal each time, using the updated values + +// summaryText := fmt.Sprintf( +// "[yellow::b]Summary Configuration for %s:\n\n"+ +// "Storage Provider: %s\n"+ +// "Endpoint URL: %s\n"+ +// "Region: %s\n"+ +// "Bucket/Container Name: %s\n"+ +// "Cache Mode: %s\n"+ +// "Cache Size: %s GB\n"+ +// "Cache Retention: %s %s\n", +// storageProvider, storageProvider, urlText, regionText, bucketName, +// cacheMode, cacheSize, retentionUnit, cacheRetentionDuration, +// ) + +// modal := tview.NewModal(). +// SetText(summaryText). +// AddButtons([]string{"Return"}). +// SetDoneFunc(func(buttonIndex int, buttonLabel string) { +// pages.SwitchToPage(previewPage) +// }) + +// return modal +// } + +func buildSummaryPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + summary := fmt.Sprintf( + "[green::b]\t\tCloudFuse Summary Configuration:[-]\n\n"+ + "Storage Provider: [yellow::b]%s[-]\n"+ + " Endpoint URL: [yellow::b]%s[-]\n"+ + " Region: [yellow::b]%s[-]\n"+ + " Container Name: [yellow::b]%s[-]\n"+ + " Cache Mode: [yellow::b]%s[-]\n"+ + " Cache Size: [yellow::b]%s GB[-]\n"+ + " Cache Retention: [yellow::b]%s %s[-]\n", + storageProvider, endpointURL, region, bucketName, + cacheMode, cacheSize, cacheRetentionDuration, cacheRetentionUnit, + ) + + textView := tview.NewTextView(). + SetTextAlign(tview.AlignLeft). + SetWrap(true). + SetDynamicColors(true). + SetText(summary). + SetScrollable(true). + SetChangedFunc(func() { + app.Draw() + }) + + buttons := tview.NewFlex().SetDirection(tview.FlexColumn) + + returnButton := tview.NewButton("Return"). + SetSelectedFunc(func() { + pages.SwitchToPage(previewPage) + }) + + buttons.AddItem(nil, 0, 1, false) // Spacer + buttons.AddItem(returnButton, 12, 1, true) + buttons.AddItem(nil, 0, 1, false) // Spacer + + frame := tview.NewFrame(textView). + SetBorders(1, 1, 1, 1, 2, 2) + // AddText("Summary", true, tview.AlignCenter, tcell.ColorYellow) + + modal := tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(frame, 16, 1, false). + AddItem(buttons, 3, 0, true) + + leftAlignedModal := tview.NewFlex(). + AddItem(modal, 60, 0, true). // fixed width modal on the left + AddItem(nil, 0, 1, false) // spacer on the right + + return leftAlignedModal +} + + +// Helper to show modals (e.g., for errors) +func showModal(app *tview.Application, pages *tview.Pages, message string, onClose func()) { + modal := tview.NewModal(). + SetText(message). + AddButtons([]string{"OK"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + pages.RemovePage("modal") + onClose() + }) + pages.AddPage("modal", modal, false, true) +} + + +// Helper function to normalize and validate the URL +func validateURL(rawURL string) (string, error) { + rawURL = strings.TrimSpace(rawURL) + + if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { + rawURL = "https://" + rawURL + } + + if _, err := url.ParseRequestURI(rawURL); err != nil { + return "", fmt.Errorf("invalid URL format") + } + + return rawURL, nil +} + + +// Function to create YAML configuration file from all data collected from the TUI +func createYAMLConfig() { + + config := Config{ + Logging: LoggingConfig{ + Type: "syslog", + Level: "log_warning", + }, + + Components: []string{"libfuse", cacheMode, "attr_cache", storageProtocol}, + + Libfuse: LibfuseConfig{ + AttributeExpirationSec: 120, + EntryExpirationSec: 120, + NegativeEntryExpirationSec: 240, + NetworkShare: true, + }, + + Stream: StreamConfig{ + BlockSizeMB: 8, + BlocksPerFile: 3, + CacheSizeMB: 1024, + }, + + AttrCache: AttrCacheConfig{ + TimeoutSec: 7200, + }, + } + + switch cacheMode { + case "fileCache": + config.FileCache = FileCacheConfig{ + Path: "Path/to/cache/dir", + TimeOutSec: 64000000, + CleanUpOnStart: true, + IgnoreSync: true, + } + case "blockCache": + config.BlockCache = BlockCacheConfig{ + BlockSizeMB: 8, + MemorySizeMB: 1024, + Prefetch: 2, + Parallelism: 4, + } + default: // "stream" or any unrecognized mode defaults to stream + config.Stream = StreamConfig{ + BlockSizeMB: 8, + BlocksPerFile: 3, + CacheSizeMB: 1024, + } + } + + + if storageProtocol == "s3storage" { + config.S3Storage = S3StorageConfig{ + BucketName: bucketName, // This should be set from the bucket + KeyID: accessKey, // This should be set from the access key input + SecretKey: secretKey, // This should be set from the secret key input + Endpoint: endpointURL, // This should be set from the URL input + Region: region, // This should be set from the region input + EnableDirMarker: true, // Default to true, can be changed in the TUI + } + } else { + config.AzStorage = AzureStorageConfig{ + Type: "block", + AccountName: accountName, // This should be set from the account name input + AccountKey: secretKey, // This should be set from the account key input + Endpoint: endpointURL, // This should be set from the URL input + Mode: "key", // Default mode, can be changed in the TUI + Container: bucketName, // This should be set from the container name input + } + } + + // Marshal the struct to YAML (returns []byte and error) + yamlData, err := yaml.Marshal(&config) + if err != nil { + fmt.Printf("Failed to marshal YAML: %v", err) + } + + // Write the YAML to a file + if err := os.WriteFile("config.yaml", yamlData, 0644); err != nil { + fmt.Printf("Failed to write YAML to file: %v", err) + } + + fmt.Printf("YAML config written to config.yaml\n") + +} + +var ( + azureRegions = []string{ + "us-east", "us-west", "us-central", "us-south", + "eu-west", "eu-central", "eu-south", "eu-north", + "asia-east", "asia-west", "asia-south", "asia-central", + "au-east", "au-west", "au-central", "au-south", + "sa-east", "sa-west", "sa-central", "sa-south", + "africa-north", "africa-south", "africa-west", "africa-east", + "canada-east", "canada-west", "canada-central", "canada-south", + "middle-east-north", "middle-east-south", "middle-east-central", + "japan-east", "japan-west", "japan-central", "japan-south" } + + awsRegions = []string{ + "us-east-1", "us-east-2", "us-west-1", "us-west-2", + "af-south-1", "ap-east-1", "ap-south-1", "ap-south-2", + "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-4", "ap-southeast-5", "ap-southeast-7", + "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", + "ca-central-1", "ca-west-1", "eu-central-1", + "eu-west-1", "eu-west-2", "eu-west-3", + "eu-south-1", "eu-south-2", "eu-north-1", + "eu-central-2", "il-central-1", "mx-central-1", + "me-south-1", "me-central-1", "sa-east-1", + } + + lyvecloudRegions = []string{ + "us-east-1", "us-west-1", "us-central-1", "eu-west-1", + } + +) \ No newline at end of file From 4a4308493be9d197cf2a5391f0be6e782b1ba229 Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 7 Jul 2025 07:52:21 -0600 Subject: [PATCH 02/21] add s3 listBuckets functionality --- cmd/tui.go | 660 +++++++---------------------------------------------- 1 file changed, 81 insertions(+), 579 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index d4e8c0944..5eb6db9ab 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -54,7 +54,7 @@ var ( cacheSize = "80" cacheRetentionDuration = "30" cacheRetentionUnit = "Days" - endpointURL = "https://s3.sv15.seagate.com" + endpointURL = "https://s3.us-east-1.sv15.lyve.seagate.com" region = "us-east-1" previewPage = "page1" accessKey = "" @@ -66,14 +66,14 @@ var ( type Config struct { - Logging LoggingConfig `yaml:"logging"` - Components []string `yaml:"components"` - Libfuse LibfuseConfig `yaml:"libfuse"` + Logging LoggingConfig `yaml:"logging,omitempty"` + Components []string `yaml:"components,omitempty"` + Libfuse LibfuseConfig `yaml:"libfuse,omitempty"` Stream StreamConfig `yaml:"stream,omitempty"` FileCache FileCacheConfig `yaml:"file_cache,omitempty"` BlockCache BlockCacheConfig `yaml:"block_cache,omitempty"` - AttrCache AttrCacheConfig `yaml:"attr_cache"` - S3Storage S3StorageConfig `yaml:"s3storage"` + AttrCache AttrCacheConfig `yaml:"attr_cache,omitempty"` + S3Storage S3StorageConfig `yaml:"s3storage,omitempty"` AzStorage *AzureStorageConfig `yaml:"azstorage,omitempty"` } @@ -115,7 +115,7 @@ type AttrCacheConfig struct { } type S3StorageConfig struct { - BucketName string `yaml:"bucket-name"` + BucketName string `yaml:"bucket-name,omitempty"` KeyID string `yaml:"key-id"` SecretKey string `yaml:"secret-key"` Endpoint string `yaml:"endpoint"` @@ -280,10 +280,10 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { func buildStorageSelectionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { // Header / section banner - headerText := "[#6EBE49::b]Step 1: Select Your Cloud Storage Provider[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + - "[white]Choose your cloud storage provider from the dropdown below.\n\n" + - "If your provider is not listed, choose [::b]Other[::-] and you’ll be prompted " + + headerText := "[#6EBE49::b]Step 1: Select Your Cloud Storage Provider[-::-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[white::b]Choose your cloud storage provider from the dropdown below.[-::-]\n\n" + + "If your provider is not listed, choose [darkmagenta::b]Other[-::-] and you’ll be prompted " + "to enter the endpoint URL and region manually." pageText := tview.NewTextView(). @@ -358,144 +358,6 @@ func buildStorageSelectionPage(app *tview.Application, pages *tview.Pages) tview } -// func buildEndpointRegionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { - -// var regions []string -// var regionInput tview.FormItem - -// // Switch case to set the default URL and region based on the selected storage provider -// switch storageProvider { -// case "LyveCloud": -// urlRegionHelpText = "For LyveCloud, the endpoint URL general format is: \n\n" + -// "\t[darkmagenta::b]https://s3.<[-][darkcyan::b]region[-][darkmagenta::b]>.lyvecloud.seagate.com[-]\n\n" + -// "For example, if your region is \"us-east-1\", the endpoint URL would be:\n\n" + -// "\t[darkmagenta::b]https://s3.us-east-1.seagate.com[-]\n\n" + -// "You can also use the LyveCloud portal to find your storage account endpoint.\n" + -// "The available regions for Seagate LyveCloud are listed in the dropdown below." -// urlText = "https://s3.sv15.seagate.com" -// regionText = "us-east-1" -// regions = lyvecloudRegions -// case "Microsoft": -// urlRegionHelpText = "For Microsoft Azure, the endpoint URL general format is:\n\n" + -// "\t[darkmagenta::b]https://<[-][darkcyan::b]storage-account-name[-][darkmagenta::b]>.<[-][darkcyan::b]service[-][darkmagenta::b]>.core.windows.net[-]\n\n" + -// "For example, if your storage account name is \"mystorageaccount\" and the\n" + -// "service is \"file\", the endpoint URL would be:\n\n" + -// "\t[darkmagenta::b]https://mystorageaccount.file.core.windows.net[-]\n\n" + -// "You can also use the Azure portal to find your storage account endpoint.\n" + -// "The available regions for Microsoft Azure are listed in the dropdown below." -// urlText = "https://.file.core.windows.net" -// regionText = "us-east" -// regions = azureRegions -// case "AWS": -// urlRegionHelpText = "For AWS S3, the endpoint URL general format is:\n\n" + -// "\t[darkmagenta::b]https://s3.<[-][darkcyan::b]region[-][darkmagenta::b]>.amazonaws.com[-]\n\n" + -// "For example, if your region is \"us-east-1\", the endpoint URL would be:\n\n" + -// "\t[darkmagenta::b]https://s3.us-east-1.amazonaws.com[-]\n\n" + -// "You can also use the AWS Management Console to find your S3 bucket endpoint.\n" + -// "The available regions for AWS S3 are listed in the dropdown below." -// urlText = "https://s3.amazonaws.com" -// regionText = "us-east-1" -// regions = awsRegions -// case "Other": -// urlText = "https://your-storage-endpoint.com" -// regionText = "your-region" // Default for 'Other' -// default: -// urlText = "https://s3.sv15.seagate.com" // Default for LyveCloud -// regionText = "us-east-1" // Default region -// } - -// pageText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(fmt.Sprintf("[green::b]Endpoint URL and Region for %s:[-] \n\n%s", storageProvider, urlRegionHelpText)) - -// urlInput := tview.NewInputField(). -// SetLabel("Endpoint URL:"). -// SetText(urlText). -// SetFieldWidth(60). -// SetChangedFunc(func(text string) { -// urlText = text -// }) - -// // Dropdown for region selection based on storage type -// // var regions []string -// // var regionInput tview.FormItem - -// if storageProvider != "Other" { -// regionInput = tview.NewDropDown(). -// SetLabel("Region:"). -// SetOptions(regions, func(text string, index int) { -// regionText = text -// }). -// SetCurrentOption(0) - -// } else { -// regionInput = tview.NewInputField(). -// SetLabel("Region:"). -// SetText("Enter Region (i.e., us-east-1)"). -// SetFieldWidth(30). -// SetChangedFunc(func(text string) { -// regionText = text -// }) -// } - -// form := tview.NewForm(). -// AddFormItem(urlInput). -// AddFormItem(regionInput). -// AddButton("Home", func() { -// pages.SwitchToPage("home") -// }). -// AddButton("Next", func() { -// // Normalize and validate the URL -// _, err := validateURL(urlText) -// if err != nil { -// showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { -// pages.SwitchToPage("page2") -// }) -// return -// } - -// // Move to the next page -// pages.SwitchToPage("page3") -// }). -// AddButton("Back", func() { -// pages.SwitchToPage("page1") -// }). -// AddButton("Preview", func() { -// // Normalize and validate URL -// _, err := validateURL(urlText) -// if err != nil { -// showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { -// pages.SwitchToPage("page2") -// }) -// return -// } - -// previewPage = "page2" -// summaryPage := buildSummaryPage(app, pages) -// pages.AddPage("summaryPage", summaryPage, true, false) -// pages.SwitchToPage("summaryPage") // Switch to Page 3 -// }). -// AddButton("Quit", func() { -// app.Stop() -// }) - -// // form.SetBorder(true).SetTitle("[ Endpoint and Region ]").SetTitleAlign(tview.AlignLeft) -// form.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) - -// layout := tview.NewFlex(). -// SetDirection(tview.FlexRow). -// AddItem(nil, 1, 1, false). // Top padding -// AddItem(pageText, 13, 1, false). // Main content -// AddItem(form, 10, 1, false). // Form for endpoint -// AddItem(nil, 1, 1, false) // Bottom padding - -// return layout -// } - - - func buildEndpointRegionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { var regions []string var regionInput tview.FormItem @@ -504,52 +366,36 @@ func buildEndpointRegionPage(app *tview.Application, pages *tview.Pages) tview.P // Determine URL, region, and help text based on selected provider switch storageProvider { case "LyveCloud": - urlRegionHelpText = `[::b]For LyveCloud, the endpoint URL format is:[-] - - [darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.lyvecloud.seagate.com[-] - -Example: - [darkmagenta::b] https://s3.us-east-1.seagate.com[-] - -Find more info in your LyveCloud portal. -Available regions are shown in the dropdown below.` - endpointURL = "https://s3.sv15.seagate.com" + urlRegionHelpText = "[::b]For LyveCloud, the endpoint URL format is:[-]\n" + + "[darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.sv15.lyve.seagate.com[-]\n\n" + + "Example:\n[darkmagenta::b] https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + + "Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown." + endpointURL = "https://s3.us-east-1.sv15.lyve.seagate.com" region = "us-east-1" regions = lyvecloudRegions case "Microsoft": - urlRegionHelpText = `[::b]For Microsoft Azure, the endpoint URL format is:[-] - - [darkmagenta::b] https://<[darkcyan::b]account-name[darkmagenta::b]>.<[darkcyan::b]service[darkmagenta::b]>.core.windows.net[-] - -Example: - [darkmagenta::b] https://mystorageaccount.file.core.windows.net[-] - -Find more info in the Azure portal. -Available regions are listed below.` + urlRegionHelpText = "[::b]For Microsoft Azure, the endpoint URL format is:[-]\n" + + "[darkmagenta::b] https://<[darkcyan::b]account-name[darkmagenta::b]>.<[darkcyan::b]service[darkmagenta::b]>.core.windows.net[-]\n\n" + + "Example:\n[darkmagenta::b] https://mystorageaccount.file.core.windows.net[-]\n\n" + + "Find more info in the Azure portal. Available regions are listed below in the dropdown." endpointURL = "https://.file.core.windows.net" region = "us-east" regions = azureRegions case "AWS": - urlRegionHelpText = `[::b]For AWS S3, the endpoint URL format is:[-] - - [darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-] - -Example: - [darkmagenta::b] https://s3.us-east-1.amazonaws.com[-] - -Use the AWS Console to find your bucket endpoint. -Available regions are listed in the dropdown.` + urlRegionHelpText = "[::b]For AWS S3, the endpoint URL format is:[-]\n" + + "[darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-]\n\n" + + "Example:\n[darkmagenta::b] https://s3.us-east-1.amazonaws.com[-]\n\n" + + "Use the AWS Console to find your bucket endpoint. Available regions are listed below in the dropdown." endpointURL = "https://s3.amazonaws.com" region = "us-east-1" regions = awsRegions case "Other": - urlRegionHelpText = `[::b]You selected a custom provider.[-] - -Enter the endpoint URL and region manually. -Refer to your provider’s documentation for valid formats.` + urlRegionHelpText = "[::b]You selected a custom provider.[-]\n" + + "Enter the endpoint URL and region manually.\n" + + "Refer to your provider’s documentation for valid formats." endpointURL = "https://your-storage-endpoint.com" region = "your-region" default: @@ -558,10 +404,9 @@ Refer to your provider’s documentation for valid formats.` } // Header and help text - header := fmt.Sprintf(`[#6EBE49::b]Step 2: Enter Endpoint & Region for %s[-] -[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -[white] -%s`, storageProvider, urlRegionHelpText) + header := fmt.Sprintf("[#6EBE49::b]Step 2: Enter Endpoint & Region for %s[-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" + + "[white]\n%s", storageProvider, urlRegionHelpText) pageText := tview.NewTextView(). SetText(header). @@ -656,176 +501,17 @@ Refer to your provider’s documentation for valid formats.` } -// func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { -// // Placeholder for credentials page -// pageText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`[green::b]Enter your Cloud Storage Credentials:[-] - -// [yellow::b]Access Key:[-] This is your unique identifier for accessing your cloud storage. -// [yellow::b]Secret Key:[-] This is your secret password for accessing your cloud storage. -// [::i](Make sure to keep these credentials secure and do not share them with anyone.)[-]`) - -// form := tview.NewForm() - -// accessKeyField := tview.NewInputField(). -// SetLabel("Access Key:"). -// SetText(accessKey). // needs to be deleted after testing -// SetFieldWidth(24) - -// secretKeyField := tview.NewInputField(). -// SetLabel("Secret Key:"). -// SetText(secretKey). // needs to be deleted after testing -// SetFieldWidth(43). -// SetMaskCharacter('*') - -// form. -// AddFormItem(accessKeyField). -// AddFormItem(secretKeyField). -// AddButton("Home", func() { -// pages.SwitchToPage("home") -// }). -// AddButton("Next", func() { -// // Validate credentials here: make sure that the keys are 20 characters long, only alphanumeric characters, no special characters -// accessKey := accessKeyField.GetText() -// secretKey := secretKeyField.GetText() -// // Convert to uppercase -// accessKey = strings.ToUpper(accessKey) -// // Check prefixes -// // if !strings.HasPrefix(accessKey, "AKIA") { -// // accessKey = "AKIA" + accessKey -// // } - -// if len(accessKey) != 24 || len(secretKey) != 43 { -// showModal(app, pages, "Invalid credentials.\nPlease try again.", func() { -// pages.SwitchToPage("page3") -// }) -// return -// } -// pages.SwitchToPage("page4") -// }). -// AddButton("Back", func() { -// pages.SwitchToPage("page2") -// }). -// AddButton("Preview", func() { -// summaryPage := buildSummaryPage(app, pages) -// pages.AddPage("summaryPage", summaryPage, true, false) -// pages.SwitchToPage("summaryPage") -// }). -// AddButton("Quit", func() { -// app.Stop() -// }) - -// // form.SetBorder(true).SetTitle("[ Credentials ]").SetTitleAlign(tview.AlignLeft) -// form.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) - -// layout := tview.NewFlex(). -// SetDirection(tview.FlexRow). -// AddItem(nil, 1, 1, false). // Top padding -// AddItem(pageText, 5, 1, false). // Main content -// AddItem(form, 8, 1, false). // Form for credentials -// AddItem(nil, 1, 1, false) // Bottom padding - -// return layout -// } - - -// func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { -// // Instructional text with consistent style -// pageText := tview.NewTextView(). -// SetTextAlign(tview.AlignCenter). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`[#6EBE49::b]Step 3: Enter Your Cloud Storage Credentials[-] -// [#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -// [white] -// [#FFD700::b]Access Key:[-] This is your unique identifier for accessing your cloud storage. -// [#FFD700::b]Secret Key:[-] This is your secret password for accessing your cloud storage. - -// [::i]Please keep these credentials secure and do not share them with anyone.[-]`) - -// // Access key input field -// accessKeyField := tview.NewInputField(). -// SetLabel("🔑 Access Key: "). -// SetText(accessKey). // For testing – remove in production -// SetFieldWidth(24). -// SetLabelColor(tcell.ColorYellow). -// SetFieldTextColor(tcell.ColorWhite). -// SetFieldBackgroundColor(tcell.ColorBlue) - -// // Secret key input field -// secretKeyField := tview.NewInputField(). -// SetLabel("🔑 Secret Key: "). -// SetText(secretKey). // For testing – remove in production -// SetFieldWidth(43). -// SetMaskCharacter('*'). -// SetLabelColor(tcell.ColorYellow). -// SetFieldTextColor(tcell.ColorWhite). -// SetFieldBackgroundColor(tcell.ColorBlue) - -// // Credential form -// form := tview.NewForm(). -// AddFormItem(accessKeyField). -// AddFormItem(secretKeyField). -// AddButton("🏠 Home", func() { -// pages.SwitchToPage("home") -// }). -// AddButton("➡ Next", func() { -// accessKey := strings.ToUpper(accessKeyField.GetText()) -// secretKey := secretKeyField.GetText() - -// if len(accessKey) != 24 || len(secretKey) != 43 { -// showModal(app, pages, "Invalid credentials.\nPlease try again.", func() { -// pages.SwitchToPage("page3") -// }) -// return -// } -// // Dry run -// pages.SwitchToPage("page4") -// }). -// AddButton("⬅ Back", func() { -// pages.SwitchToPage("page2") -// }). -// AddButton("📄 Preview", func() { -// summaryPage := buildSummaryPage(app, pages) -// pages.AddPage("summaryPage", summaryPage, true, false) -// pages.SwitchToPage("summaryPage") -// }). -// AddButton("❌ Quit", func() { -// app.Stop() -// }). -// SetButtonBackgroundColor(menuButtonColor). -// SetButtonTextColor(menuButtonTextColor). -// SetButtonsAlign(tview.AlignCenter) - -// // Final layout -// layout := tview.NewFlex(). -// SetDirection(tview.FlexRow). -// AddItem(nil, 1, 0, false). // Top padding -// AddItem(pageText, 9, 0, false). // Instructional text -// AddItem(nil, 1, 0, false). -// AddItem(form, 9, 0, true). // Credential input form -// AddItem(nil, 1, 0, false) // Bottom padding - -// return layout -// } - - func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { // Instructional text with consistent style pageText := tview.NewTextView(). SetTextAlign(tview.AlignCenter). SetWrap(true). SetDynamicColors(true). - SetText(`[#6EBE49::b]Step 3: Enter Your Cloud Storage Credentials[-] -[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -[white] -[#FFD700::b]Access Key:[-] This is your unique identifier for accessing your cloud storage. -[#FFD700::b]Secret Key:[-] This is your secret password for accessing your cloud storage. - -[::i]Please keep these credentials secure and do not share them with anyone.[-]`) + SetText("[#6EBE49::b]Step 3: Enter Your Cloud Storage Credentials[-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + + "[#FFD700::b]Access Key:[-] This is your unique identifier for accessing your cloud storage.\n" + + "[#FFD700::b]Secret Key:[-] This is your secret password for accessing your cloud storage.\n\n" + + "[::i]Please keep these credentials secure and do not share them with anyone.[-]") // Access key input field accessKeyField := tview.NewInputField(). @@ -864,23 +550,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim return } - // Step 1: Write a temp config.yaml with provided credentials - // tempConfig := ` - // components: ["s3storage"] - // s3storage: - // access_key: "` + accessKey + `" - // secret_key: "` + secretKey + `" - // region: "us-east-1" - // ` - // tmpFile := "config-tui-temp.yaml" - // err := os.WriteFile(tmpFile, []byte(tempConfig), 0600) - // if err != nil { - // showModal(app, pages, "Failed to write config file:\n"+err.Error(), nil) - // return - // } - - tmpFile := "config-tui-temp.yaml" - options.ConfigFile = tmpFile + createTmpConfigFile() // Step 2: Parse the config err := parseConfig() @@ -942,64 +612,6 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim return layout } -// func buildBucketNamePage(app *tview.Application, pages *tview.Pages) tview.Primitive { - -// pageText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`[green::b]Select your Bucket/Container Name:[-] - -// The bucket/container names available for your cloud storage provider are -// listed below in the dropdown. The available bucket/container names are based -// on the credentials you entered in the previous step.`) - -// bucketNameField := tview.NewInputField(). -// SetLabel("Bucket/Container Name:"). -// SetText("my-bucket"). -// SetFieldWidth(30) - -// form := tview.NewForm(). -// AddFormItem(bucketNameField). -// AddButton("Home", func() { -// pages.SwitchToPage("home") -// }). -// AddButton("Next", func() { -// bucketName = bucketNameField.GetText() -// if bucketName == "" { -// showModal(app, pages, "Bucket/container name cannot be empty.\nPlease try again.", func() { -// pages.SwitchToPage("page4") -// }) -// return -// } -// // Proceed to the next step or page -// pages.SwitchToPage("page5") -// }). -// AddButton("Back", func() { -// pages.SwitchToPage("page3") -// }). -// AddButton("Preview", func() { -// summaryPage := buildSummaryPage(app, pages) -// pages.AddPage("summaryPage", summaryPage, true, false) -// pages.SwitchToPage("summaryPage") // Switch to Page 3 -// }). -// AddButton("Quit", func() { -// app.Stop() -// }) - -// // form.SetBorder(true).SetTitle("[ Bucket/Container Name ]").SetTitleAlign(tview.AlignLeft) -// form.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) - -// layout := tview.NewFlex(). -// SetDirection(tview.FlexRow). -// AddItem(nil, 1, 1, false). // Top padding -// AddItem(pageText, 5, 1, false). // Main content -// AddItem(form, 8, 1, false). // Form for bucket -// AddItem(nil, 1, 1, false) // Bottom padding - -// return layout -// } - func buildContainerSelectPage(app *tview.Application, pages *tview.Pages) tview.Primitive { @@ -1076,161 +688,6 @@ based on the credentials you entered in the previous step.`) return layout } -// func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { -// // Placeholder for caching page -// pageText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`[green::b]Configure Caching Settings:[-] - -// To help Cloudfuse work best for you, let's determine how you'd like to use your local storage.`) - - -// localCacheText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`Do you have available local storage that you'd like -// Cloudfuse to use for improved performance and reliability?`) - -// cacheToDisk := tview.NewDropDown(). -// SetLabel("Cache to local disk? "). -// SetOptions([]string{" Yes ", " No "}, func(text string, index int) { -// if text == "Yes" { -// // Enable disk caching settings -// } else { -// // Disable disk caching settings -// } -// }). -// SetCurrentOption(0) - -// cacheLocationText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`If you selected "Yes" above, please enter the location of the cache directory. -// For example, /var/cache/s3storage or /tmp/s3cache.`) - -// cacheLocationField := tview.NewInputField(). -// SetLabel("Cache Location:"). -// SetText("/var/cache/s3storage"). -// SetFieldWidth(30). -// SetChangedFunc(func(text string) { -// // Validate cache location input -// if text == "" { -// showModal(app, pages, "Cache location cannot be empty.\nPlease try again.", func() { -// pages.SwitchToPage("page5") -// }) -// return -// } -// // Update cache location if needed -// cacheLocation = text - -// }) - -// cacheSizeText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`We've detected [X GB] available at this location. -// By default, Cloudfuse will use up to [80% of X GB] for its cache. -// Would you like to specify a different cache size (in GB)?`) - -// // Cache size input field. How much space (at most) do they want the cache to take up (default is 80% of available space on the drive which contains the directory they entered above) -// cacheSizeField := tview.NewInputField(). -// SetLabel("Cache Size:"). -// SetText("80"). // Default to 80% -// SetFieldWidth(10). -// SetChangedFunc(func(text string) { -// // Validate cache size input -// if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { -// showModal(app, pages, "Cache size must be between 1 and 100.\nPlease try again.", func() { -// pages.SwitchToPage("page5") -// }) -// return -// } -// cacheSize = text // Update cache size -// }) - -// cacheRetentionText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`Do you need cached data to be automatically removed from -// local storage after a certain amount of time since its last access?`) - - -// cacheRetention := tview.NewCheckbox(). -// SetLabel("Enable Cache Retention?"). -// SetChecked(false). -// SetChangedFunc(func(checked bool) { -// if checked { -// // Enable cache retention settings -// } else { -// // Disable cache retention settings -// } -// }) - -// cacheRetentionDurationText := tview.NewTextView(). -// SetTextAlign(tview.AlignLeft). -// SetWrap(true). -// SetDynamicColors(true). -// SetText(`If you selected "Yes" above, please enter the duration and -// select the unit for cache retention from the dropdown`) - -// cacheRetentionDurationUnit := tview.NewForm(). -// AddInputField("Cache Retention Duration:", "30", 10, nil, func(text string) { -// // Validate cache retention duration input -// cacheRetentionDuration = text -// }). -// AddDropDown("Unit:", []string{"Seconds", "Minutes", "Hours", "Days"}, 0, func(option string, index int) { -// retentionUnit = option -// }) - - -// menuButtons := tview.NewForm(). -// AddButton("Home", func() { -// pages.SwitchToPage("home") -// }). -// AddButton("Finish", func() { -// app.Stop() -// }). -// AddButton("Back", func() { -// pages.SwitchToPage("page4") -// }). -// AddButton("Preview", func() { -// summaryPage := buildSummaryPage(app, pages) -// pages.AddPage("summaryPage", summaryPage, true, false) -// pages.SwitchToPage("summaryPage") -// }). -// AddButton("Quit", func() { -// app.Stop() -// }) - -// // form.SetBorder(true).SetTitle("[ Caching Settings ]").SetTitleAlign(tview.AlignLeft) -// menuButtons.SetButtonBackgroundColor(menuButtonColor).SetButtonTextColor(menuButtonTextColor) - -// layout := tview.NewFlex(). -// SetDirection(tview.FlexRow). -// AddItem(nil, 1, 1, false). // Top padding -// AddItem(pageText, 5, 1, false). // Main content -// AddItem(localCacheText, 3, 1, false). // Local -// AddItem(cacheToDisk, 3, 1, false). // Cache to disk dropdown -// AddItem(cacheLocationText, 3, 1, false). // Cache location text -// AddItem(cacheLocationField, 3, 1, false). // Cache location -// AddItem(cacheSizeText, 3, 1, false). // Cache -// AddItem(cacheSizeField, 3, 1, false). // Cache size input -// AddItem(cacheRetentionText, 3, 1, false). // Cache retention text -// AddItem(cacheRetention, 3, 1, false). // Cache retention -// AddItem(cacheRetentionDurationText, 3, 1, false). // Cache retention -// AddItem(cacheRetentionDurationUnit, 5, 1, false). // Cache retention -// AddItem(menuButtons, 3, 1, false). // Menu buttons -// AddItem(nil, 1, 1, false) // Bottom padding - -// return layout -// } - func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { pageText := tview.NewTextView(). @@ -1498,6 +955,51 @@ func validateURL(rawURL string) (string, error) { } +// Create a temporary YAML configuration file with the provided information +// up to credentials page +func createTmpConfigFile() error { + config := Config{ + + Components: []string{storageProtocol}, + } + + + if storageProtocol == "azstorage" { + config.AzStorage = &AzureStorageConfig{ + Type: "block", + AccountName: accountName, + AccountKey: secretKey, + Endpoint: endpointURL, + Mode: "key", + Container: bucketName, + } + } else if storageProtocol == "s3storage" { + config.S3Storage = S3StorageConfig{ + KeyID: accessKey, + SecretKey: secretKey, + Endpoint: endpointURL, + Region: region, + EnableDirMarker: true, + } + } + + yamlData, err := yaml.Marshal(&config) + if err != nil { + return fmt.Errorf("failed to marshal YAML: %v", err) + } + + tmpFile := "config-temp.yaml" + if err := os.WriteFile(tmpFile, yamlData, 0644); err != nil { + return fmt.Errorf("failed to write YAML to file: %v", err) + } + + // Update options.ConfigFile to point to the temporary file + options.ConfigFile = "config-temp.yaml" + fmt.Printf("Temporary YAML config written to %s\n", tmpFile) + return nil +} + + // Function to create YAML configuration file from all data collected from the TUI func createYAMLConfig() { From 33ffcf207ea7398d6a6ec9b64fc8cea8ed805a85 Mon Sep 17 00:00:00 2001 From: brayan Date: Thu, 14 Aug 2025 15:19:16 -0600 Subject: [PATCH 03/21] Fixed major bugs. Stable working version for 2.0 release --- cmd/tui.go | 1798 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 1055 insertions(+), 743 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index 5eb6db9ab..cd3794aa5 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -29,89 +29,91 @@ import ( "fmt" "net/url" "os" + "path/filepath" "slices" "strconv" "strings" + "syscall" + "time" + "github.com/Seagate/cloudfuse/common" "github.com/Seagate/cloudfuse/common/config" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "gopkg.in/yaml.v3" ) - +/* + Constants and global variables used throughout the TUI application. + These include default values, colors, widget configurations, and storage settings. +*/ var ( - accountName = "my-account" - storageProtocols = []string {"s3storage", "azstorage"} - storageProtocol = "s3storage" - storageProviders = []string{"LyveCloud", "Microsoft", "AWS", "Other"} - storageProvider = "LyveCloud" - cacheModes = []string {"stream", "file_cache", "block_cache"} - cacheMode = "file_cache" - bucketName = "my-bucket" - containerList = []string {} - cacheLocation = "/var/cache/s3storage" - cacheSize = "80" - cacheRetentionDuration = "30" - cacheRetentionUnit = "Days" - endpointURL = "https://s3.us-east-1.sv15.lyve.seagate.com" - region = "us-east-1" - previewPage = "page1" - accessKey = "" - secretKey = "" - menuButtonColor = tcell.GetColor("#6EBE49") - menuButtonTextColor = tcell.ColorBlack - menuButtonAlignment = tview.AlignLeft + tuiVersion string = common.CloudfuseVersion + configFilePath string // Sets file_cache.path + accountName string // Sets azstorage.account-name + accountKey string // Sets azstorage.account-key + accessKey string // Sets s3storage.key-id + secretKey string // Sets s3storage.secret-key + containerName string // Sets azstorage.container-name + bucketName string // Sets s3storage.bucket-name + endpointURL string // Sets s3storage.endpoint + containerList = []string {} // Holds list of available buckets retrieved from cloud provider + storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider + storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. + cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' + enableCaching bool = true // If true, sets cacheMode to file_cache. If false, block_cache + cacheLocation string = getDefaultCachePath() // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache + cacheSize string = "80" // User-defined cache size as % + availableCacheSizeGB int // Total available cache size in GB @ the cache location + currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage + clearCacheOnStart bool = false // If false, sets 'allow-non-empty-temp' to true + cacheRetentionDuration int = 2 // User-defined cache retention duration. Default is '2' + cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' + cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' + + // Global variables for UI elements + tuiAlignment = tview.AlignLeft + previewPage string = "page1" + yellowColor tcell.Color = tcell.GetColor("#FFD700") + greenColor tcell.Color = tcell.GetColor("#6EBE49") + widgetLabelColor = yellowColor + widgetFieldBackgroundColor = yellowColor + navigationButtonColor = greenColor + navigationButtonTextColor = tcell.ColorBlack + navigationButtonAlignment = tview.AlignLeft + navigationStartLabel string = "[black]🚀 Start[-]" + navigationHomeLabel string = "[black]🏠 Home[-]" + navigationNextLabel string = "[black]🡲 Next[-]" + navigationBackLabel string = "[black]🡰 Back[-]" + navigationPreviewLabel string = "[black]📄 Preview[-]" + navigationQuitLabel string = "[black]❌ Quit[-]" + navigationFinishLabel string = "[black]✅ Finish[-]" + navigationWidgetHeight int = 3 ) - type Config struct { - Logging LoggingConfig `yaml:"logging,omitempty"` Components []string `yaml:"components,omitempty"` Libfuse LibfuseConfig `yaml:"libfuse,omitempty"` - Stream StreamConfig `yaml:"stream,omitempty"` FileCache FileCacheConfig `yaml:"file_cache,omitempty"` - BlockCache BlockCacheConfig `yaml:"block_cache,omitempty"` AttrCache AttrCacheConfig `yaml:"attr_cache,omitempty"` S3Storage S3StorageConfig `yaml:"s3storage,omitempty"` - AzStorage *AzureStorageConfig `yaml:"azstorage,omitempty"` -} - -type LoggingConfig struct { - Type string `yaml:"type"` - Level string `yaml:"level"` + AzStorage AzureStorageConfig `yaml:"azstorage,omitempty"` } type LibfuseConfig struct { - AttributeExpirationSec int `yaml:"attribute-expiration-sec"` - EntryExpirationSec int `yaml:"entry-expiration-sec"` - NegativeEntryExpirationSec int `yaml:"negative-entry-expiration-sec"` - NetworkShare bool `yaml:"network-share"` + NetworkShare bool `yaml:"network-share"` } -type StreamConfig struct { - BlockSizeMB int `yaml:"block-size-mb"` - BlocksPerFile int `yaml:"blocks-per-file"` - CacheSizeMB int `yaml:"cache-size-mb"` +type AttrCacheConfig struct { + TimeoutSec int `yaml:"timeout-sec"` } type FileCacheConfig struct { - Path string `yaml:"path"` - TimeOutSec int `yaml:"timeout-sec"` - CleanUpOnStart bool `yaml:"cleanup-on-start"` - IgnoreSync bool `yaml:"ignore-sync"` -} - -type BlockCacheConfig struct { - BlockSizeMB int `yaml:"block-size-mb"` - MemorySizeMB int `yaml:"mem-size-mb"` - Prefetch int `yaml:"prefetch"` - Parallelism int `yaml:"parallelism"` -} - - -type AttrCacheConfig struct { - TimeoutSec int `yaml:"timeout-sec"` + Path string `yaml:"path"` + TimeOutSec int `yaml:"timeout-sec"` + AllowNonEmptyTemp bool `yaml:"allow-non-empty-temp"` + IgnoreSync bool `yaml:"ignore-sync"` + MaxSizeMB int `yaml:"max-size-mb,omitempty"` } type S3StorageConfig struct { @@ -119,21 +121,23 @@ type S3StorageConfig struct { KeyID string `yaml:"key-id"` SecretKey string `yaml:"secret-key"` Endpoint string `yaml:"endpoint"` - Region string `yaml:"region"` EnableDirMarker bool `yaml:"enable-dir-marker"` } - type AzureStorageConfig struct { Type string `yaml:"type"` AccountName string `yaml:"account-name"` AccountKey string `yaml:"account-key"` - Endpoint string `yaml:"endpoint"` - Mode string `yaml:"mode"` + Endpoint string `yaml:"endpoint,omitempty"` + Mode string `yaml:"mode,omitempty"` Container string `yaml:"container"` } +/* + Main function to run the TUI application. + It initializes the tview application, builds the TUI application, and runs it. +*/ func runTUI() error{ app := tview.NewApplication() app.EnableMouse(true) @@ -141,39 +145,31 @@ func runTUI() error{ buildTUI(app) + // Run the application if err := app.Run(); err != nil { panic(err) } - // After the TUI is done, create the YAML config file - createYAMLConfig() - return nil } +/* + Function to build the TUI application. + It initializes the main pages and adds them to the page stack. +*/ func buildTUI(app *tview.Application) { pages := tview.NewPages() - // --- Home Page --- - homePage := buildHomePage(app, pages) - - // --- Page 1: Storage Type Selection --- - page1 := buildStorageSelectionPage(app, pages) - - // --- Page 2: Endpoint & Region Entry --- - page2 := buildEndpointRegionPage(app, pages) - - // --- Page 3: Credentials Entry --- - page3 := buildCredentialsPage(app, pages) - - // --- Page 4: Bucket Name Entry --- - page4 := buildContainerSelectPage(app, pages) - - // --- Page 5: Caching Settings --- - page5 := buildCachingPage(app, pages) + // Initialize the pages + homePage := buildHomePage(app, pages) // --- Home Page --- + page1 := buildStorageProviderPage(app, pages) // --- Page 1: Storage Provider Selection --- + page2 := buildEndpointURLPage(app, pages) // --- Page 2: Endpoint URL Entry --- + page3 := buildCredentialsPage(app, pages) // --- Page 3: Credentials Entry --- + page4 := buildContainerSelectPage(app, pages) // --- Page 4: Bucket Selection --- + page5 := buildCachingPage(app, pages) // --- Page 5: Caching Settings --- - // --- Add pages to the page stack --- + // Add pages to the page stack pages.AddPage("home", homePage, true, true) pages.AddPage("page1", page1, true, false) pages.AddPage("page2", page2, true, false) @@ -185,117 +181,106 @@ func buildTUI(app *tview.Application) { } +/* + --- Page 0: Home Page --- + Displays a welcome banner, instructions, and buttons to start or quit the application. + It also includes an "About" section with information about the tool. +*/ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { - // Banner / welcome message bannerText := "[#6EBE49::b]░█▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + - "[white::b]Welcome to the CloudFuse Configuration Tool\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + - "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" - - bannerView := tview.NewTextView(). - SetText(bannerText). - SetTextAlign(tview.AlignCenter). - SetDynamicColors(true). - SetWrap(true) - - // Instructions - instructionsView := tview.NewTextView(). - SetText("[::b]Instructions:[-:-]\n" + - "[#6EBE49]•[-] Use your [::b]mouse[-:-] or [::b]arrow keys[-:-] to navigate.\n" + - "[#6EBE49]•[-] Press [::b]Enter[-:-] to select items.\n" + - "[#6EBE49]•[-] For the best experience, expand terminal window to full size.\n"). + "[white::b]Welcome to the CloudFuse Configuration Tool\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + + "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" + + // Banner text widget + bannerTextWidget := tview.NewTextView(). + SetText(centerText(bannerText, 75)). + SetTextAlign(tuiAlignment). SetDynamicColors(true). - SetTextAlign(tview.AlignLeft). SetWrap(true) - - // Dropdown hint - jumpToView := tview.NewTextView(). - SetText("[::i]Tip: Use the dropdown below to quickly jump to any step.[::-]"). - SetTextAlign(tview.AlignLeft). + + instructionsText := "[#FFD700::b]Instructions:[::-]\n" + + "[#6EBE49::b]•[-::-] [::]Use your mouse or arrow keys to navigate.[-::-]\n" + + "[#6EBE49::b]•[-::-] [::]Press Enter or left-click to select items.[-::-]\n" + + "[#6EBE49::b]•[-::-] [::]For the best experience, expand terminal window to full size.[-::-]\n" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetText(instructionsText). SetDynamicColors(true). + SetTextAlign(tuiAlignment). SetWrap(true) - // Start / Quit buttons - startQuitWidget := tview.NewForm(). - AddButton("🚀 Start", func() { + // Start/Quit buttons widget + startQuitButtonsWidget := tview.NewForm(). + AddButton(navigationStartLabel, func() { pages.SwitchToPage("page1") }). - AddButton("❌ Quit", func() { + AddButton(navigationQuitLabel, func() { app.Stop() }). - SetButtonBackgroundColor(tcell.GetColor("#6EBE49")). - SetButtonTextColor(tcell.ColorWhite). - SetButtonsAlign(tview.AlignCenter) - - // Dropdown to jump between pages - jumpToWidget := tview.NewForm(). - AddDropDown("Jump to:", []string{ - " Storage Selection ⬇️ ", - " Endpoint & Region ", - " Credentials ", - " Bucket Name ", - " Caching Settings ", - }, 0, func(option string, index int) { - pages.SwitchToPage(fmt.Sprintf("page%d", index+1)) - }). - SetLabelColor(tcell.GetColor("#FFD700")). - SetFieldBackgroundColor(tcell.GetColor("#FFD700")) - - // About section - aboutView := tview.NewTextView(). - SetText("[::b]ABOUT[-]\n" + - "[gray]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "CloudFuse TUI Configuration Tool\n\n" + + SetButtonBackgroundColor(navigationButtonColor). + SetButtonTextColor(navigationButtonTextColor). + SetButtonsAlign(navigationButtonAlignment) + + aboutText := "[#FFD700::b]ABOUT[-::-]\n" + + "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + + "[grey::i]CloudFuse TUI Configuration Tool\n" + "Seagate Technology, LLC\n" + - "cloudfuse@seagate.com\n\n" + - "Version: 1.0.0"). + "cloudfuse@seagate.com\n" + + fmt.Sprintf("Version: %s", tuiVersion) + // About text widget + aboutTextWidget := tview.NewTextView(). + SetText(centerText(aboutText, 75)). SetDynamicColors(true). - SetTextAlign(tview.AlignCenter). + SetTextAlign(tuiAlignment). SetWrap(true) - - // Assemble layout + + // Assemble page layout layout := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(nil, 1, 0, false). // Top padding - AddItem(bannerView, 10, 0, false). // Banner - AddItem(nil, 1, 0, false). // Banner and start/quit padding - AddItem(startQuitWidget, 3, 0, false). // Start/Quit buttons - AddItem(nil, 1, 0, false). // Padding between buttons and instructions - AddItem(instructionsView, 4, 0, false). // Instructions - AddItem(nil, 2, 0, false). // Padding between instructions and dropdown hint - AddItem(jumpToView, 1, 0, false). - AddItem(jumpToWidget, 3, 0, false). - AddItem(nil, 2, 0, false). - AddItem(aboutView, 9, 0, false). // New About section - AddItem(nil, 1, 0, false) // Bottom padding - layout.SetBorder(true).SetBorderColor(tcell.GetColor("#6EBE49")).SetBorderPadding(1, 1, 1, 1) + AddItem(bannerTextWidget, getTextHeight(bannerText), 0, false). // Banner Widget + AddItem(nil, 1, 0, false). // Padding + AddItem(startQuitButtonsWidget, 3, 0, false). // Start/Quit buttons widget + AddItem(nil, 1, 0, false). // Padding + AddItem(instructionsTextWidget, 4, 0, false). // Instructions widget + AddItem(nil, 2, 0, false). // Padding + AddItem(aboutTextWidget, 9, 0, false). // About widget + AddItem(nil, 1, 0, false) // Bottom padding + + layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) return layout } -func buildStorageSelectionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { - // Header / section banner - headerText := "[#6EBE49::b]Step 1: Select Your Cloud Storage Provider[-::-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[white::b]Choose your cloud storage provider from the dropdown below.[-::-]\n\n" + - "If your provider is not listed, choose [darkmagenta::b]Other[-::-] and you’ll be prompted " + - "to enter the endpoint URL and region manually." - - pageText := tview.NewTextView(). - SetText(headerText). - SetTextAlign(tview.AlignCenter). +/* + --- Page 1: Storage Provider Selection --- + This page allows users to select their cloud storage provider from a dropdown list. + It provides options for LyveCloud, Microsoft, AWS, and Other S3. +*/ +func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + + "[grey::i] If your provider is not listed, choose [darkmagenta::b]Other (s3)[-::-][grey::i]. You’ll be\n" + + " prompted to enter the endpoint URL and region manually.[-::-]\n" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetText(instructionsText). + SetTextAlign(tuiAlignment). SetDynamicColors(true). SetWrap(true) - // Dropdown for storage provider - storageProviderDropdown := tview.NewDropDown(). + // Dropdown widget for selecting storage provider + storageProviderDropdownWidget := tview.NewDropDown(). SetLabel("📦 Storage Provider: "). - SetOptions([]string{" LyveCloud ⬇️", " Microsoft ", " AWS ", " Other "}, func(option string, index int) { + SetOptions([]string{" LyveCloud ⬇️", " Microsoft ", " AWS ", " Other (s3) "}, func(option string, index int) { storageProvider = option switch option { case " LyveCloud ⬇️": @@ -307,678 +292,1007 @@ func buildStorageSelectionPage(app *tview.Application, pages *tview.Pages) tview case " AWS ": storageProtocol = "s3storage" storageProvider = "AWS" - case " Other ": + case " Other (s3) ": storageProtocol = "s3storage" storageProvider = "Other" + endpointURL = "" default: storageProtocol = "s3storage" storageProvider = "LyveCloud" } }). SetCurrentOption(0). - SetLabelColor(tcell.GetColor("#FFD700")). - SetFieldBackgroundColor(tcell.GetColor("#FFD700")). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack). SetFieldWidth(14) - - // Navigation buttons - form := tview.NewForm(). - // AddFormItem(storageProviderDropdown). - AddButton("🏠 Home", func() { + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(navigationHomeLabel, func() { pages.SwitchToPage("home") }). - AddButton("➡ Next", func() { - page2 := buildEndpointRegionPage(app, pages) - pages.AddPage("page2", page2, true, false) - pages.SwitchToPage("page2") + AddButton(navigationNextLabel, func() { + // If Microsoft or AWS is selected, switch to page 3 and skip endpoint/region entry. + // These providers handle endpoint and region internally. + if storageProvider == "Microsoft" || storageProvider == "AWS" { + page3 := buildCredentialsPage(app, pages) + pages.AddPage("page3", page3, true, false) + pages.SwitchToPage("page3") + } else { + page2 := buildEndpointURLPage(app, pages) + pages.AddPage("page2", page2, true, false) + pages.SwitchToPage("page2") + } }). - AddButton("📄 Preview", func() { - summaryPage := buildSummaryPage(app, pages) - pages.AddPage("summaryPage", summaryPage, true, false) - pages.SwitchToPage("summaryPage") + AddButton(navigationPreviewLabel, func() { + previewPage := buildPreviewPage(app, pages, "page1") + pages.AddPage("previewPage", previewPage, true, false) + pages.SwitchToPage("previewPage") }). - AddButton("❌ Quit", func() { + AddButton(navigationQuitLabel, func() { app.Stop() }). - SetButtonBackgroundColor(menuButtonColor). - SetButtonTextColor(menuButtonTextColor). - SetButtonsAlign(tview.AlignCenter) + SetButtonBackgroundColor(navigationButtonColor). + SetButtonTextColor(navigationButtonTextColor). + SetButtonsAlign(navigationButtonAlignment) - // Layout assembly + // Assemble page layout layout := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(nil, 1, 0, false). // Top padding - AddItem(pageText, 7, 0, false). // Header and instructions - AddItem(nil, 1, 0, false). // Spacing - AddItem(storageProviderDropdown, 3, 0, false). // Dropdown for storage provider - AddItem(form, 6, 0, false). // Dropdown + nav buttons - AddItem(nil, 1, 0, false) // Bottom padding + AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). + AddItem(nil, 1, 0, false). + AddItem(storageProviderDropdownWidget, 2, 0, false). + AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false). + AddItem(nil, 1, 0, false) + + layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) return layout } -func buildEndpointRegionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { - var regions []string - var regionInput tview.FormItem - urlRegionHelpText := "" +/* + --- Page 2: Endpoint and Region Page --- + This page allows users to enter the endpoint URL for their cloud storage provider. + It validates the endpoint URL format and provides help text based on the selected provider. +*/ +func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Primitive { + var urlRegionHelpText string - // Determine URL, region, and help text based on selected provider + // Determine URL help text based on selected provider switch storageProvider { case "LyveCloud": - urlRegionHelpText = "[::b]For LyveCloud, the endpoint URL format is:[-]\n" + - "[darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.sv15.lyve.seagate.com[-]\n\n" + - "Example:\n[darkmagenta::b] https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + - "Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown." - endpointURL = "https://s3.us-east-1.sv15.lyve.seagate.com" - region = "us-east-1" - regions = lyvecloudRegions - - case "Microsoft": - urlRegionHelpText = "[::b]For Microsoft Azure, the endpoint URL format is:[-]\n" + - "[darkmagenta::b] https://<[darkcyan::b]account-name[darkmagenta::b]>.<[darkcyan::b]service[darkmagenta::b]>.core.windows.net[-]\n\n" + - "Example:\n[darkmagenta::b] https://mystorageaccount.file.core.windows.net[-]\n\n" + - "Find more info in the Azure portal. Available regions are listed below in the dropdown." - endpointURL = "https://.file.core.windows.net" - region = "us-east" - regions = azureRegions - - case "AWS": - urlRegionHelpText = "[::b]For AWS S3, the endpoint URL format is:[-]\n" + - "[darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-]\n\n" + - "Example:\n[darkmagenta::b] https://s3.us-east-1.amazonaws.com[-]\n\n" + - "Use the AWS Console to find your bucket endpoint. Available regions are listed below in the dropdown." - endpointURL = "https://s3.amazonaws.com" - region = "us-east-1" - regions = awsRegions + urlRegionHelpText = "[::b]For LyveCloud, the endpoint URL format is generally:[-]\n" + + "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + + "Example:\n[darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + + "[grey::i]Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown.[-::-]" + urlRegionHelpText = centerText(urlRegionHelpText, 65) case "Other": - urlRegionHelpText = "[::b]You selected a custom provider.[-]\n" + - "Enter the endpoint URL and region manually.\n" + - "Refer to your provider’s documentation for valid formats." - endpointURL = "https://your-storage-endpoint.com" - region = "your-region" - default: - endpointURL = "https://s3.sv15.seagate.com" - region = "us-east-1" + urlRegionHelpText = "[::b]You selected a custom s3 provider.[::-]\n\n" + + "Enter the endpoint URL.\n" + + "[grey::i]Refer to your provider’s documentation for valid formats.[-::-]" + urlRegionHelpText = centerText(urlRegionHelpText, 65) } - // Header and help text - header := fmt.Sprintf("[#6EBE49::b]Step 2: Enter Endpoint & Region for %s[-]\n" + + instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" + - "[white]\n%s", storageProvider, urlRegionHelpText) + "[white]\n %s", storageProvider, urlRegionHelpText) - pageText := tview.NewTextView(). - SetText(header). - SetTextAlign(tview.AlignCenter). + instructionsTextWidget := tview.NewTextView(). + SetText(instructionsText). + SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true) - // URL input field - urlInput := tview.NewInputField(). + endpointURLFieldWidget := tview.NewInputField(). SetLabel("🔗 Endpoint URL: "). SetText(endpointURL). - SetFieldWidth(60). + SetFieldWidth(50). SetChangedFunc(func(text string) { endpointURL = text }). - SetLabelColor(tcell.ColorYellow). - SetFieldTextColor(tcell.ColorWhite). - SetFieldBackgroundColor(tcell.ColorBlue) - - // Region input (dropdown or manual) - if storageProvider != "Other" { - regionInput = tview.NewDropDown(). - SetLabel("🌐 Region: "). - SetOptions(regions, func(text string, index int) { - region = text - }). - SetCurrentOption(0). - SetLabelColor(tcell.ColorYellow). - SetFieldTextColor(tcell.ColorWhite). - SetFieldBackgroundColor(tcell.ColorBlue) - } else { - regionInput = tview.NewInputField(). - SetLabel("🌐 Region: "). - SetText("Enter Region (e.g., us-east-1)"). - SetFieldWidth(30). - SetLabelColor(tcell.ColorYellow). - SetFieldTextColor(tcell.ColorWhite). - SetFieldBackgroundColor(tcell.ColorBlue). - SetChangedFunc(func(text string) { - region = text - }) - } - - // Navigation form - form := tview.NewForm(). - AddFormItem(urlInput). - AddFormItem(regionInput). - AddButton("🏠 Home", func() { + SetPlaceholder("\t\t\t\t"). + SetPlaceholderTextColor(tcell.ColorGray). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(navigationHomeLabel, func() { pages.SwitchToPage("home") }). - AddButton("➡ Next", func() { - if _, err := validateURL(endpointURL); err != nil { - showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { + AddButton(navigationNextLabel, func() { + if err := validateEndpointURL(endpointURL); err != nil { + showErrorModal(app, pages, fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), func() { + pages.RemovePage("page2") + page2 := buildEndpointURLPage(app, pages) + pages.AddPage("page2", page2, true, false) pages.SwitchToPage("page2") }) return } + pages.RemovePage("page3") + page3 := buildCredentialsPage(app, pages) + pages.AddPage("page3", page3, true, false) pages.SwitchToPage("page3") }). - AddButton("⬅ Back", func() { + AddButton(navigationBackLabel, func() { pages.SwitchToPage("page1") }). - AddButton("📄 Preview", func() { - if _, err := validateURL(endpointURL); err != nil { - showModal(app, pages, "Invalid URL format.\nPlease try again.", func() { - pages.SwitchToPage("page2") - }) - return - } - previewPage = "page2" - summaryPage := buildSummaryPage(app, pages) - pages.AddPage("summaryPage", summaryPage, true, false) - pages.SwitchToPage("summaryPage") + AddButton(navigationPreviewLabel, func() { + previewPage := buildPreviewPage(app, pages, "page2") + pages.AddPage("previewPage", previewPage, true, false) + pages.SwitchToPage("previewPage") }). - AddButton("❌ Quit", func() { + AddButton(navigationQuitLabel, func() { app.Stop() }). - SetButtonBackgroundColor(menuButtonColor). - SetButtonTextColor(menuButtonTextColor). - SetButtonsAlign(tview.AlignCenter) + SetButtonBackgroundColor(navigationButtonColor). + SetLabelColor(widgetLabelColor). + SetButtonTextColor(navigationButtonTextColor). + SetButtonsAlign(navigationButtonAlignment) - // Final layout + // Assemble page layout layout := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(nil, 1, 0, false). - AddItem(pageText, 14, 0, false). - AddItem(nil, 1, 0, false). - AddItem(form, 10, 0, true). + AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). + AddItem(nil, 2, 0, false). + AddItem(endpointURLFieldWidget, 2, 0, false). + AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) + layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + return layout } +/* + --- Page 3: Credentials Page --- + This page allows users to enter their cloud storage credentials. + If the storage protocol is "s3", it provides input fields for access key, secret key. + If the storage protocol is "azure", it provides input fields for account name, account key, and container name. +*/ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { - // Instructional text with consistent style - pageText := tview.NewTextView(). - SetTextAlign(tview.AlignCenter). + layout := tview.NewFlex() + layout.Clear() + + // Determine labels for input fields based on storage protocol. + accessLabel := "" + secretLabel := "" + if storageProtocol == "azstorage" { + accessLabel = "🔑 Account Name: " + secretLabel = "🔑 Account Key: " + } else { + accessLabel = "🔑 Access Key: " + secretLabel = "🔑 Secret Key: " + } + + instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Your Cloud Storage Credentials[-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n" + + "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your unique identifier for accessing your cloud storage.\n" + + "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your secret password for accessing your cloud storage.\n", + strings.Trim(accessLabel, "🔑 "), strings.Trim(secretLabel, "🔑 ")) + + if storageProtocol == "azstorage" { + instructionsText += "[#FFD700::b] -[-::-] [#FFD700::b]Container Name:[-::-] This is the name of your Azure Blob Storage container.\n" + } + + instructionsText += "\n[darkmagenta::i]\t\t\t*Keep these credentials secure. Do not share.[-]" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true). - SetText("[#6EBE49::b]Step 3: Enter Your Cloud Storage Credentials[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + - "[#FFD700::b]Access Key:[-] This is your unique identifier for accessing your cloud storage.\n" + - "[#FFD700::b]Secret Key:[-] This is your secret password for accessing your cloud storage.\n\n" + - "[::i]Please keep these credentials secure and do not share them with anyone.[-]") - - // Access key input field - accessKeyField := tview.NewInputField(). - SetLabel("🔑 Access Key: "). - SetText(accessKey). // For testing – remove in production - SetFieldWidth(24). - SetLabelColor(tcell.ColorYellow). - SetFieldTextColor(tcell.ColorWhite). - SetFieldBackgroundColor(tcell.ColorBlue) - - // Secret key input field - secretKeyField := tview.NewInputField(). - SetLabel("🔑 Secret Key: "). - SetText(secretKey). // For testing – remove in production - SetFieldWidth(43). + SetText(instructionsText) + + // Access key field widget + accessKeyFieldWidget := tview.NewInputField(). + SetLabel(accessLabel). + SetText(accessKey). + SetFieldWidth(50). + SetChangedFunc(func(text string) { + accessKey = text + accountName = text + }). + SetPlaceholder("\t\t\t\t"). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Secret key field widget with masked input + secretKeyFieldWidget := tview.NewInputField(). + SetLabel(secretLabel). + SetText(secretKey). + SetFieldWidth(50). + SetChangedFunc(func(text string) { + secretKey = text + accountKey = text + }). + SetPlaceholder("\t\t\t\t"). SetMaskCharacter('*'). - SetLabelColor(tcell.ColorYellow). - SetFieldTextColor(tcell.ColorWhite). - SetFieldBackgroundColor(tcell.ColorBlue) - - // Credential form - form := tview.NewForm(). - AddFormItem(accessKeyField). - AddFormItem(secretKeyField). - AddButton("🏠 Home", func() { + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Container name field widget for Azure storage + containerNameFieldWidget := tview.NewInputField(). + SetLabel("🪣 Container Name: "). + SetText(containerName). + SetPlaceholder("\t\t\t\t"). + SetChangedFunc(func(text string) { + containerName = text + }). + SetFieldWidth(50). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(navigationHomeLabel, func() { pages.SwitchToPage("home") }). - AddButton("➡ Next", func() { - accessKey := strings.ToUpper(accessKeyField.GetText()) - secretKey := secretKeyField.GetText() - - if len(accessKey) != 24 || len(secretKey) != 43 { - showModal(app, pages, "Invalid credentials.\nPlease try again.", func() { + AddButton(navigationNextLabel, func() { + // TODO: Add validation for access key and secret key HERE + // For now, just check that they are not empty + // TODO: Add check to make sure containerName is not empty if storageProvider is Microsoft + if (storageProtocol == "s3storage" && (accessKey == "" || secretKey == "")) || (storageProtocol == "azstorage" && (accountName == "" || accountKey == "" || containerName == "")) { + // if accessKey == "" || accountName == "" || secretKey == "" || accountKey == "" { + showErrorModal(app, pages, "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", func() { pages.SwitchToPage("page3") }) return } - - createTmpConfigFile() - - // Step 2: Parse the config - err := parseConfig() - if err != nil { - showModal(app, pages, "Failed to parse config:\n"+err.Error(), nil) - return - } - - err = config.Unmarshal(&options) - if err != nil { - showModal(app, pages, "Failed to unmarshal config:\n"+err.Error(), nil) + // TODO: Fix bug here where calling listBuckets() in the checkCredentials() function + // causes the layout to shift upwards and the widgets to be misaligned if the user incorrectly + // enters credentials. + if err := checkCredentials(app, pages); err != nil { + showErrorModal(app, pages, fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func (){ + pages.RemovePage("page3") // Remove the current page + page3 := buildCredentialsPage(app, pages) // Rebuild the page + pages.AddPage("page3", page3, true, false) // Add the new page + pages.SwitchToPage("page3") + }) return } - // Step 3: Try to fetch container/bucket list - // var containerList []string - if slices.Contains(options.Components, "azstorage") { - containerList, err = getContainerListAzure() - } else if slices.Contains(options.Components, "s3storage") { - containerList, err = getBucketListS3() + if storageProtocol == "azstorage" { + pages.RemovePage("page4") // Remove previous page if it exists + pages.SwitchToPage("page5") } else { - err = fmt.Errorf("unsupported storage backend") - } - - if err != nil { - showModal(app, pages, "Failed to connect:\n"+err.Error(), nil) - return + page4 := buildContainerSelectPage(app, pages) + pages.AddPage("page4", page4, true, false) + pages.SwitchToPage("page4") } - - // Step 4: Pass containerList to page4 (next page) - page4 := buildContainerSelectPage(app, pages) - pages.AddPage("page4", page4, true, false) - pages.SwitchToPage("page4") }). - AddButton("⬅ Back", func() { + AddButton(navigationBackLabel, func() { + if storageProvider == "Microsoft" || storageProvider == "AWS" { + pages.RemovePage("page2") + pages.SwitchToPage("page1") + } else { + page2 := buildEndpointURLPage(app, pages) + pages.AddPage("page2", page2, true, false) pages.SwitchToPage("page2") + } }). - AddButton("📄 Preview", func() { - summaryPage := buildSummaryPage(app, pages) - pages.AddPage("summaryPage", summaryPage, true, false) - pages.SwitchToPage("summaryPage") + AddButton(navigationPreviewLabel, func() { + previewPage := buildPreviewPage(app, pages, "page3") + pages.AddPage("previewPage", previewPage, true, false) + pages.SwitchToPage("previewPage") }). - AddButton("❌ Quit", func() { + AddButton(navigationQuitLabel, func() { app.Stop() }). - SetButtonBackgroundColor(menuButtonColor). - SetButtonTextColor(menuButtonTextColor). - SetButtonsAlign(tview.AlignCenter) + SetLabelColor(widgetLabelColor). + SetButtonBackgroundColor(navigationButtonColor). + SetButtonTextColor(tcell.ColorBlack). + SetButtonsAlign(navigationButtonAlignment) + + // Combine all credential widgets into a single form + credentialsWidget := tview.NewForm(). + AddFormItem(accessKeyFieldWidget). + AddFormItem(secretKeyFieldWidget). + SetFieldTextColor(tcell.ColorBlack). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor) + + // If Azure is selected, add the container name field + if storageProvider == "Microsoft" { + credentialsWidget.AddFormItem(containerNameFieldWidget) + } - // Final layout - layout := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(nil, 1, 0, false). // Top padding - AddItem(pageText, 9, 0, false). // Instructional text - AddItem(nil, 1, 0, false). - AddItem(form, 9, 0, true). // Credential input form - AddItem(nil, 1, 0, false) // Bottom padding + // Assemble page layout + layout.SetDirection(tview.FlexRow) + layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) + layout.AddItem(nil, 1, 0, false) + layout.AddItem(credentialsWidget, credentialsWidget.GetFormItemCount()*2+1, 0, false) + layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) + layout.AddItem(nil, 1, 0, false) + layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) return layout } +/* + --- Page 4: Container Name Selection --- + This page allows users to select a bucket name from a dropdown list. + It retrieves the list of available buckets from the cloud storage provider based on the + credentials provided in the previous step. +*/ func buildContainerSelectPage(app *tview.Application, pages *tview.Pages) tview.Primitive { - - pageText := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). + instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + + "[grey::i] The list of available buckets is retrieved from your cloud storage provider\n " + + "based on the credentials provided in the previous step.[-::-]" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true). - SetText(`[#6EBE49::b]Step 4: Select Your Bucket or Container Name[-] -[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -[white] -Enter the name of your storage bucket or container. These should be accessible -based on the credentials you entered in the previous step.`) - - // Bucket name input - // bucketNameField := tview.NewInputField(). - // SetLabel("🪣 Bucket/Container Name: "). - // SetText("my-bucket"). - // SetFieldWidth(30). - // SetLabelColor(tcell.ColorYellow). - // SetFieldTextColor(tcell.ColorWhite). - // SetFieldBackgroundColor(tcell.ColorBlue) - containerNameDropdown := tview.NewDropDown(). - SetLabel("🪣 Bucket/Container Name: "). + SetText(instructionsText) + + // Dropdown widget for selecting bucket name + containerSelectionWidget := tview.NewDropDown(). + SetLabel(" 🪣 Bucket Name: "). SetOptions(containerList, func(text string, index int) { bucketName = text }). SetCurrentOption(0). - SetLabelColor(tcell.ColorYellow). - SetFieldTextColor(tcell.ColorWhite). - SetFieldBackgroundColor(tcell.ColorBlue). - SetFieldWidth(30) - - // Form with navigation - form := tview.NewForm(). - // AddFormItem(bucketNameField). - AddFormItem(containerNameDropdown). - AddButton("🏠 Home", func() { + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack). + SetFieldWidth(25) + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(navigationHomeLabel, func() { pages.SwitchToPage("home") }). - AddButton("➡ Next", func() { - // bucketName = containerName.GetText() - // if strings.TrimSpace(bucketName) == "" { - // showModal(app, pages, "Bucket/container name cannot be empty.\nPlease try again.", func() { - // pages.SwitchToPage("page4") - // }) - // return - // } + AddButton(navigationNextLabel, func() { pages.SwitchToPage("page5") }). - AddButton("⬅ Back", func() { + AddButton(navigationBackLabel, func() { pages.SwitchToPage("page3") }). - AddButton("📄 Preview", func() { - summaryPage := buildSummaryPage(app, pages) - pages.AddPage("summaryPage", summaryPage, true, false) - pages.SwitchToPage("summaryPage") + AddButton(navigationPreviewLabel, func() { + previewPage := buildPreviewPage(app, pages, "page4") + pages.AddPage("previewPage", previewPage, true, false) + pages.SwitchToPage("previewPage") }). - AddButton("❌ Quit", func() { + AddButton(navigationQuitLabel, func() { app.Stop() }). - SetButtonBackgroundColor(menuButtonColor). - SetButtonTextColor(menuButtonTextColor). - SetButtonsAlign(tview.AlignCenter) + SetButtonBackgroundColor(navigationButtonColor). + SetButtonTextColor(tcell.ColorBlack). + SetButtonsAlign(navigationButtonAlignment) - // Final layout + // Assemble page layout layout := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(nil, 1, 0, false). // Top padding - AddItem(pageText, 7, 0, false). // Instructional text - AddItem(nil, 1, 0, false). - AddItem(form, 9, 0, true). // Input form - AddItem(nil, 1, 0, false) // Bottom padding + AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). + AddItem(nil, 2, 0, false). + AddItem(containerSelectionWidget, 2, 0, false). + AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false). + AddItem(nil, 1, 0, false) + + layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) return layout } +/* + --- Page 5: Caching Settings --- + Function to build the caching page that allows users to configure caching settings. + This page includes options for cache location, size, and retention settings. +*/ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { - pageText := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). - SetWrap(true). - SetDynamicColors(true). - SetText(`[#6EBE49::b]Step 5: Configure Caching Settings[-] -[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -[white] -To optimize performance and reliability, you can allow CloudFuse to cache -data locally on your disk. You can customize where, how much, and for how long -this cache is used.`) - - localCacheText := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). - SetWrap(true). - SetDynamicColors(true). - SetText(`[::b]💾 Do you want to enable local caching?[-:-] -Enable this if you have enough local storage available. Cached data improves -performance and resilience when the cloud is temporarily unavailable.`) - - cacheToDisk := tview.NewDropDown(). - SetLabel("📁 Cache to Local Disk: "). - SetOptions([]string{" Yes ", " No "}, func(text string, index int) { - // optional logic could be added to enable/disable below fields dynamically - }). - SetCurrentOption(0). - SetLabelColor(tcell.ColorYellow). - SetFieldBackgroundColor(tcell.ColorBlue). - SetFieldTextColor(tcell.ColorWhite) - - cacheLocationText := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). + // Main layout container. Must be instantiated first to allow nested items. + layout := tview.NewFlex().SetDirection(tview.FlexRow) + + instructionsText := "[#6EBE49::b] Configure Caching Settings[-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + + "[white::b] CloudFuse can cache data locally. You control the location, size, and duration.[-::-]\n\n" + + "[#FFD700::b] -[-::-] [#6EBE49::b]Enable[-::-] caching if you frequently re-read data and have ample disk space.\n" + + "[#FFD700::b] -[-::-] [red::b]Disable[-::-] caching if you prefer faster initial access or have limited disk space.\n\n" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true). - SetText(`[::b]📂 Cache Directory Location:[-:-] -Enter the absolute path to a directory where CloudFuse can store cached files. -Example: [blue]/var/cache/s3storage[-] or [blue]/tmp/cloudcache[-]`) + SetText(instructionsText) - cacheLocationField := tview.NewInputField(). - SetLabel("📍 Cache Location: "). - SetText("/var/cache/s3storage"). + // Dropdown widget for enabling/disabling caching + cacheLocationFieldWidget := tview.NewInputField(). + SetLabel("📁 Cache Location: "). + SetText(cacheLocation). SetFieldWidth(40). - SetLabelColor(tcell.ColorYellow). - SetFieldBackgroundColor(tcell.ColorBlue). - SetFieldTextColor(tcell.ColorWhite). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack). SetChangedFunc(func(text string) { - if strings.TrimSpace(text) == "" { - showModal(app, pages, "Cache location cannot be empty.\nPlease try again.", func() { - pages.SwitchToPage("page5") - }) - return - } cacheLocation = text }) - cacheSizeText := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). - SetWrap(true). - SetDynamicColors(true). - SetText(`[::b]🧠 Cache Size (in GB):[-:-] -Specify how much disk space to allow CloudFuse for cache storage. -Recommended default is 80%% of available space on the chosen drive.`) - - cacheSizeField := tview.NewInputField(). - SetLabel("📦 Cache Size (GB): "). - SetText("80"). - SetFieldWidth(10). - SetLabelColor(tcell.ColorYellow). - SetFieldBackgroundColor(tcell.ColorBlue). - SetFieldTextColor(tcell.ColorWhite). + // Input field widget for cache size percentage + cacheSizeFieldWidget := tview.NewInputField(). + SetLabel("📊 Cache Size (%): "). + SetText(cacheSize). // Default to 80% + SetFieldWidth(4). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack). + SetChangedFunc(func(text string) { + if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { + showErrorModal(app, pages, "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", func() { + pages.SwitchToPage("page5") + }) + return + } + cacheSize = text + }) + + // Input field widget for cache retention duration + cacheRetentionDurationFieldWidget := tview.NewInputField(). + SetLabel("⌛ Cache Retention Duration: "). + SetText(fmt.Sprintf("%d", cacheRetentionDuration)). + SetFieldWidth(5). SetChangedFunc(func(text string) { - if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { - showModal(app, pages, "Cache size must be between 1 and 100.\nPlease try again.", func() { - pages.SwitchToPage("page5") - }) - return + if val, err := strconv.Atoi(text); err == nil { + cacheRetentionDuration = val + } else { + // TODO: Handle invalid input + cacheRetentionDuration = 0 } - cacheSize = text - }) - - cacheRetentionText := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). - SetWrap(true). - SetDynamicColors(true). - SetText(`[::b]🕒 Cache Retention Settings:[-:-] -You can optionally have cached files auto-deleted if they haven’t been -accessed in a while.`) - - cacheRetention := tview.NewCheckbox(). - SetLabel("🧹 Enable Cache Retention: "). - SetChecked(false). - SetLabelColor(tcell.ColorYellow). - SetChangedFunc(func(checked bool) { - // Logic could enable/disable retention duration input dynamically - }) - - cacheRetentionDurationText := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). - SetWrap(true). - SetDynamicColors(true). - SetText(`[::b]⏳ Retention Duration:[-:-] -If retention is enabled, enter the duration and unit below. -For example: 30 [blue]Days[-] or 12 [blue]Hours[-].`) - - cacheRetentionDurationUnit := tview.NewForm(). - AddInputField("⏱ Duration:", "30", 10, nil, func(text string) { - cacheRetentionDuration = text - }). - AddDropDown("🕰 Unit:", []string{"Seconds", "Minutes", "Hours", "Days"}, 0, func(option string, index int) { - cacheRetentionUnit = option }). - SetLabelColor(tcell.ColorYellow). - SetFieldBackgroundColor(tcell.ColorBlue). - SetFieldTextColor(tcell.ColorWhite) + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) - // Navigation buttons - menuButtons := tview.NewForm(). - AddButton("🏠 Home", func() { - pages.SwitchToPage("home") - }). - AddButton("✅ Finish", func() { - app.Stop() - }). - AddButton("⬅ Back", func() { - pages.SwitchToPage("page4") - }). - AddButton("📄 Preview", func() { - summaryPage := buildSummaryPage(app, pages) - pages.AddPage("summaryPage", summaryPage, true, false) - pages.SwitchToPage("summaryPage") - }). - AddButton("❌ Quit", func() { - app.Stop() + // Dropdown widget for cache retention unit + cacheRetentionUnitDropdownWidget := tview.NewDropDown(). + SetOptions([]string{"Seconds", "Minutes", "Hours", "Days"}, func(option string, index int) { + cacheRetentionUnit = option + // Convert cache retention duration to seconds + switch cacheRetentionUnit { + case "Seconds": + cacheRetentionDurationSec = cacheRetentionDuration + case "Minutes": + minutes := cacheRetentionDuration + cacheRetentionDurationSec = minutes * 60 + case "Hours": + hours := cacheRetentionDuration + cacheRetentionDurationSec = hours * 3600 + case "Days": + days := cacheRetentionDuration + cacheRetentionDurationSec = days * 86400 + } }). - SetButtonBackgroundColor(menuButtonColor). - SetButtonTextColor(menuButtonTextColor). - SetButtonsAlign(tview.AlignCenter) - - // Layout - layout := tview.NewFlex(). + SetCurrentOption(3). // Default to Days + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Dropdown widget for enabling/disabling cache cleanup on restart + // If enabled --> allow-non-empty-temp: false + // if disabled --> allow-non-empty-temp: true + clearCacheOnStartDropdownWidget := tview.NewDropDown(). + SetLabel("🧹 Clear Cache On Start: "). + SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { + if text == " Enabled " { + clearCacheOnStart = true + } else { + clearCacheOnStart = false + } + }). + SetCurrentOption(0). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Horizontal container to place retention duration and unit side by side + cacheRetentionRow := tview.NewFlex(). + SetDirection(tview.FlexColumn). + AddItem(cacheRetentionDurationFieldWidget, 35, 0, false). + AddItem(cacheRetentionUnitDropdownWidget, 7, 0, false) + + // Group cache field widgets in a container + cacheFields := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(nil, 1, 0, false). // Top padding - AddItem(pageText, 6, 0, false). - AddItem(localCacheText, 3, 0, false). - AddItem(cacheToDisk, 2, 0, false). - AddItem(cacheLocationText, 3, 0, false). - AddItem(cacheLocationField, 2, 0, false). - AddItem(cacheSizeText, 3, 0, false). - AddItem(cacheSizeField, 2, 0, false). - AddItem(cacheRetentionText, 3, 0, false). - AddItem(cacheRetention, 2, 0, false). - AddItem(cacheRetentionDurationText, 2, 0, false). - AddItem(cacheRetentionDurationUnit, 4, 0, false). - AddItem(menuButtons, 3, 0, false). - AddItem(nil, 1, 0, false) // Bottom padding - - return layout -} + AddItem(cacheLocationFieldWidget, 2, 0, false). + AddItem(cacheSizeFieldWidget, 2, 0, false). + AddItem(cacheRetentionRow, 2, 0, false). + AddItem(clearCacheOnStartDropdownWidget, 2, 0, false) + + // Tracks whether or not cache fields are currently shown + showCacheFields := true + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm() + navigationButtonsWidget. + AddButton(navigationHomeLabel, func() { + pages.SwitchToPage("home") + }). + AddButton(navigationFinishLabel, func() { + // Check if caching is enabled and validate cache settings + if enableCaching { + // Validate the cache location + if err := validateCachePath(); err != nil { + showErrorModal(app, pages, "Invalid cache location:\n"+err.Error(), func() { + pages.SwitchToPage("page5") + }) + return + } + + // Check available cache size + if err := getAvailableCacheSize(); err != nil { + showErrorModal(app, pages, "Failed to check available cache size:\n"+err.Error(), func() { + pages.SwitchToPage("page5") + }) + return + } + + cacheSizeText := fmt.Sprintf("Available Disk Space @ Cache Location: [darkred::b]%d GB[-::-]\n", availableCacheSizeGB) + + fmt.Sprintf("Cache Size Currently Set to: [darkred::b]%.0f GB (%s%%)[-::-]\n\n", float64(currentCacheSizeGB), cacheSize) + + "Would you like to proceed with this cache size?\n\n"+ + "If not, hit [darkred::b]Return[-::-] to adjust cache size accordingly. Otherwise, hit [darkred::b]Finish[-::-] to complete the configuration." + + showCacheConfirmationModal(app, pages, cacheSizeText, + // Callback function if the user selects Finish + func() { + if err := createYAMLConfig(); err != nil { + showErrorModal(app, pages, "Failed to create YAML config:\n"+err.Error(), func() { + pages.SwitchToPage("page5") + }) + return + } + showExitModal(app, pages, func() { + app.Stop() + }) + }, + // Callback function if the user selects Return + func() { + pages.SwitchToPage("page5") + }) + } else { + // If caching is disabled, just finish the configuration + if err := createYAMLConfig(); err != nil { + showErrorModal(app, pages, "Failed to create YAML config:\n"+err.Error(), func() { + pages.SwitchToPage("page5") + }) + return + } + showExitModal(app, pages, func() { + app.Stop() + }) + } + }). + AddButton(navigationBackLabel, func() { + if storageProtocol == "azstorage" { + pages.SwitchToPage("page3") + } else { + page4 := buildContainerSelectPage(app, pages) + pages.AddPage("page4", page4, true, false) + pages.SwitchToPage("page4") + } + }). + AddButton(navigationPreviewLabel, func() { + previewPage := buildPreviewPage(app, pages, "page5") + pages.AddPage("previewPage", previewPage, true, false) + pages.SwitchToPage("previewPage") + }). + AddButton(navigationQuitLabel, func() { + app.Stop() + }). + SetButtonBackgroundColor(navigationButtonColor). + SetButtonTextColor(tcell.ColorBlack). + SetButtonsAlign(tuiAlignment) + + // Widget to enable/disable caching + enableCachingDropdownWidget := tview.NewDropDown() + enableCachingDropdownWidget. + SetLabel("💾 Caching: "). + SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { + if text == " Enabled " { + cacheMode = "file_cache" + enableCaching = true + if !showCacheFields { + layout.RemoveItem(navigationButtonsWidget) + layout.RemoveItem(cacheFields) + layout.AddItem(cacheFields, 8, 0, false) + layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) + showCacheFields = true + } + } else { + cacheMode = "block_cache" + enableCaching = false + if showCacheFields { + layout.RemoveItem(cacheFields) + showCacheFields = false + } + } + }). + SetCurrentOption(0). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Assemble page layout + layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) + layout.AddItem(enableCachingDropdownWidget, 2, 0, false) + + if showCacheFields { + layout.AddItem(cacheFields, 8, 0, false) + } -// func buildSummaryPage(app *tview.Application, pages *tview.Pages) tview.Primitive { -// // Rebuild the modal each time, using the updated values - -// summaryText := fmt.Sprintf( -// "[yellow::b]Summary Configuration for %s:\n\n"+ -// "Storage Provider: %s\n"+ -// "Endpoint URL: %s\n"+ -// "Region: %s\n"+ -// "Bucket/Container Name: %s\n"+ -// "Cache Mode: %s\n"+ -// "Cache Size: %s GB\n"+ -// "Cache Retention: %s %s\n", -// storageProvider, storageProvider, urlText, regionText, bucketName, -// cacheMode, cacheSize, retentionUnit, cacheRetentionDuration, -// ) - -// modal := tview.NewModal(). -// SetText(summaryText). -// AddButtons([]string{"Return"}). -// SetDoneFunc(func(buttonIndex int, buttonLabel string) { -// pages.SwitchToPage(previewPage) -// }) - -// return modal -// } - -func buildSummaryPage(app *tview.Application, pages *tview.Pages) tview.Primitive { - summary := fmt.Sprintf( - "[green::b]\t\tCloudFuse Summary Configuration:[-]\n\n"+ - "Storage Provider: [yellow::b]%s[-]\n"+ - " Endpoint URL: [yellow::b]%s[-]\n"+ - " Region: [yellow::b]%s[-]\n"+ - " Container Name: [yellow::b]%s[-]\n"+ - " Cache Mode: [yellow::b]%s[-]\n"+ - " Cache Size: [yellow::b]%s GB[-]\n"+ - " Cache Retention: [yellow::b]%s %s[-]\n", - storageProvider, endpointURL, region, bucketName, - cacheMode, cacheSize, cacheRetentionDuration, cacheRetentionUnit, - ) - - textView := tview.NewTextView(). - SetTextAlign(tview.AlignLeft). - SetWrap(true). - SetDynamicColors(true). - SetText(summary). - SetScrollable(true). - SetChangedFunc(func() { - app.Draw() - }) + layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) + layout.AddItem(nil, 1, 0, false) + layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) - buttons := tview.NewFlex().SetDirection(tview.FlexColumn) + return layout +} - returnButton := tview.NewButton("Return"). - SetSelectedFunc(func() { - pages.SwitchToPage(previewPage) - }) - buttons.AddItem(nil, 0, 1, false) // Spacer - buttons.AddItem(returnButton, 12, 1, true) - buttons.AddItem(nil, 0, 1, false) // Spacer +/* + --- Summary Page --- + Function to build the summary page that displays the configuration summary. + This function creates a text view with the summary information and a return button. + It takes an application instance, pages instance, and the preview page name as parameters. +*/ +func buildPreviewPage(app *tview.Application, pages *tview.Pages, previewPage string) tview.Primitive { + summaryText := + "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n"+ + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]" + + fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", storageProvider)+ + fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", endpointURL)+ + fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", bucketName)+ + fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", cacheMode)+ + fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", cacheLocation)+ + fmt.Sprintf(" Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", cacheSize, currentCacheSizeGB) + + // Display cache retention duration in seconds and specified unit + if cacheRetentionUnit == "Seconds" { + summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", cacheRetentionDurationSec) + } else { + summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", + cacheRetentionDurationSec, cacheRetentionDuration, cacheRetentionUnit) + } - frame := tview.NewFrame(textView). - SetBorders(1, 1, 1, 1, 2, 2) - // AddText("Summary", true, tview.AlignCenter, tcell.ColorYellow) + // Set a dynamic width and height for the summary widget + summaryWidgetHeight := getTextHeight(summaryText) + summaryWidgetWidth := getTextWidth(summaryText) / 3 - modal := tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(frame, 16, 1, false). + summaryWidget := tview.NewTextView(). + SetTextAlign(tuiAlignment). + SetWrap(true). + SetDynamicColors(true). + SetText(summaryText). + SetScrollable(true) + + returnButton := tview.NewButton("[black]Return[-]"). + SetSelectedFunc(func() { + pages.SwitchToPage(previewPage) + }) + returnButton.SetBackgroundColor(greenColor) + returnButton.SetBorder(true) + returnButton.SetBorderColor(yellowColor) + returnButton.SetBackgroundColorActivated(greenColor) + + buttons := tview.NewFlex(). + SetDirection(tview.FlexColumn). + AddItem(nil, 0, 1, false). // Left button spacer + AddItem(returnButton, 20, 0, true). + AddItem(nil, 0, 1, false) // Right button spacer + + modal := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(summaryWidget, summaryWidgetHeight, 0, false). + AddItem(nil, 1, 0, false). AddItem(buttons, 3, 0, true) leftAlignedModal := tview.NewFlex(). - AddItem(modal, 60, 0, true). // fixed width modal on the left - AddItem(nil, 0, 1, false) // spacer on the right + AddItem(modal, summaryWidgetWidth, 0, true) + + leftAlignedModal.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) return leftAlignedModal } -// Helper to show modals (e.g., for errors) -func showModal(app *tview.Application, pages *tview.Pages, message string, onClose func()) { +/* + Function to show a modal dialog with a message and an "OK" button. + This function is used to display error messages or confirmations. + It takes an application instance, pages instance, message string, + and a callback function to execute when the modal is closed. +*/ +func showErrorModal(app *tview.Application, pages *tview.Pages, message string, onClose func()) { modal := tview.NewModal(). SetText(message). AddButtons([]string{"OK"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { pages.RemovePage("modal") onClose() - }) + }). + SetBackgroundColor(greenColor). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(yellowColor) + modal.SetButtonBackgroundColor(yellowColor) + modal.SetButtonTextColor(tcell.ColorBlack) pages.AddPage("modal", modal, false, true) } -// Helper function to normalize and validate the URL -func validateURL(rawURL string) (string, error) { +/* + Function to show a confirmation modal dialog with "Finish" and "Return" buttons. + Used to confirm cache size before proceeding. It takes an application instance, + pages instance, message string, and two callback functions for the "Finish" and "Return" actions. +*/ +func showCacheConfirmationModal(app *tview.Application, pages *tview.Pages, message string, onFinish func(), onReturn func()) { + modal := tview.NewModal(). + SetText(message). + AddButtons([]string{"Finish", "Return"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + pages.RemovePage("modal") + if buttonLabel == "Finish" { + onFinish() + } else { + onReturn() + } + }). + SetBackgroundColor(greenColor). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(yellowColor) + modal.SetButtonBackgroundColor(yellowColor) + modal.SetButtonTextColor(tcell.ColorBlack) + pages.AddPage("modal", modal, true, true) +} + + +/* + Function to show final exit modal when configuration is complete. + This function is called when the user clicks "Finish" on the caching page. + It informs the user that the configuration is complete and they can exit. + It also creates a small processing emoji animation then shows the path to the config file. +*/ +func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) { + + processingEmojis := []string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "✅"} + + modal := tview.NewModal(). + AddButtons([]string{"Exit"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + pages.RemovePage("modal") + if buttonLabel == "Exit" { + onConfirm() + } + }). + SetBackgroundColor(greenColor). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(yellowColor) + modal.SetButtonBackgroundColor(yellowColor) + modal.SetButtonTextColor(tcell.ColorBlack) + + pages.AddPage("modal", modal, true, true) + + // Simulate processing with emoji animation + go func() { + // Show initial message with emoji animation + for i := 0; i < len(processingEmojis); i++ { + currentEmoji := processingEmojis[i] + time.Sleep(100 * time.Millisecond) + app.QueueUpdateDraw(func() { + modal.SetText(fmt.Sprintf("[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", currentEmoji)) + }) + } + + // After animation, show final message + app.QueueUpdateDraw(func() { + modal.SetText(fmt.Sprintf("[#6EBE49::b]Configuration Complete![-::-]\n\n%s\n\n"+ + "Your CloudFuse configuration file has been created at:\n\n[blue:white:b]%s[-:-:-]\n\n"+ + "You can now exit the application.\n\n"+ + "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], configFilePath)) + }) + }() +} + + +/* + Helper function to center lines of text within a specified width. + This function handles color tags and ensures that the text is centered + even if it contains ANSI escape codes or other formatting. + It is used to format text views and other UI elements in the TUI. +*/ +func centerText(text string, width int) string { + var centeredLines []string + lines := strings.Split(text, "\n") + for _, line := range lines { + visibleLen := tview.TaggedStringWidth(line) // handle color tags + if visibleLen >= width { + centeredLines = append(centeredLines, line) + } else { + padding := (width - visibleLen) / 2 + centeredLines = append(centeredLines, strings.Repeat(" ", padding)+line) + } + } + return strings.Join(centeredLines, "\n") +} + + +/* + Helper function to get the length of the longest line in a string. + It splits the string by newline characters and finds the maximum length of the lines. + It returns 0 if the string is empty. + It is used to determine the width of text views and other UI elements. +*/ +func getTextWidth(s string) int { + if s == "" { + return 0 + } + lines := strings.Split(s, "\n") + longest := 0 + for _, line := range lines { + if len(line) > longest { + longest = len(line) + } + } + return longest +} + + +/* + Helper function to count the number of lines in a string. + It splits the string by newline characters and returns the length of the resulting slice. + It returns 0 if the string is empty. + It is used to determine the height of text views and other UI elements. +*/ +func getTextHeight(s string) int { + if s == "" { + return 0 + } + return len(strings.Split(s, "\n")) +} + + +/* + Helper function to get the default cache path. + It retrieves the user's home directory and constructs a default cache path: + `~/.cloudfuse/file_cache` + If the directory does not exist, it attempts to create it. + If it fails to retrieve the home directory or create the path, it returns a fallback path. + It returns the full path to the cache directory. +*/ +func getDefaultCachePath() string { + home, err := os.UserHomeDir() + if err != nil { + // TODO: Handle error if home directory cannot be retrieved. + // TODO: Choose a fallback path for cache directory. + return "/tmp/cloudfuse/cloudfuse.log" + } + filepath := filepath.Join(home, ".cloudfuse", "file_cache") + // If the directory doesn't exist, create it + if _, err := os.Stat(filepath); os.IsNotExist(err) { + if err := os.MkdirAll(filepath, 0755); err != nil { + fmt.Printf("Failed to create cache directory: %v\n", err) + // TODO: Handle error if cache directory cannot be created + return "/tmp/cloudfuse/cloudfuse.log" // fallback path + } + } + // Return the full path to the cache directory + return filepath +} + + +/* + Helper function to validate the entered cache path. It checks if the + path is not empty, does not contain invalid characters, and if it exists. + It returns an error if any validation fails. +*/ +func validateCachePath() error { + // Validate that the path is not empty + if strings.TrimSpace(cacheLocation) == "" { + return fmt.Errorf("[red::b]ERROR: Cache location cannot be empty[-::-]") + } + // Make sure no invalid path characters are used + if strings.ContainsAny(cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { + return fmt.Errorf("[red::b]ERROR: Cache location contains invalid characters[-::-]") + } + // Validate that the cache path exists + if cacheLocation != getDefaultCachePath() && cacheMode == "file_cache" { + if _, err := os.Stat(cacheLocation); os.IsNotExist(err) { + return fmt.Errorf("[red::b]ERROR: '%s': No such file or directory[-::-]", cacheLocation) + } + } + return nil +} + + +/* + Helper function to get the available cache size. + It retrieves the available space in the cache location and calculates + the available cache size in GB based on the user-defined cache size percentage. + It sets the global variable `availableCacheSizeGB` and calculates the current cache size. + If it fails to retrieve the available space, it returns an error. +*/ +func getAvailableCacheSize() (error) { + var stat syscall.Statfs_t + if err := syscall.Statfs(cacheLocation, &stat); err != nil { + return fmt.Errorf("[red::b]ERROR: Failed to get available cache size[-::-]: %v", err) + } + availableCacheSizeBytes := stat.Bavail * uint64(stat.Bsize) // Available space in bytes + availableCacheSizeGB = int(availableCacheSizeBytes / (1024 * 1024 * 1024)) // Convert to GB + cacheSizeInt, _ := strconv.Atoi(cacheSize) + currentCacheSizeGB = int(availableCacheSizeGB) * cacheSizeInt / 100 + return nil +} + + +/* + Helper function to normalize and validate the endpoint URL. + It checks if the URL starts with "http://" or "https://", and if not, it prepends "https://". + It also parses the URL to ensure it is valid. + It returns the normalized URL or an error if the URL is invalid. +*/ +func validateEndpointURL(rawURL string) (error) { rawURL = strings.TrimSpace(rawURL) + // Check if the URL is empty + if strings.TrimSpace(rawURL) == "" { + return fmt.Errorf("[red::b]Endpoint URL cannot be empty[-::-]\nPlease try again.") + } + + // Normalize the URL by adding "https://" if it doesn't start with "http://" or "https://" if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { - rawURL = "https://" + rawURL + endpointURL = "https://" + rawURL + return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n"+ + "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") } if _, err := url.ParseRequestURI(rawURL); err != nil { - return "", fmt.Errorf("invalid URL format") + return fmt.Errorf("[red::b]Invalid URL format[-::-]\n%s\nPlease try again.", err.Error()) } - return rawURL, nil + return nil } -// Create a temporary YAML configuration file with the provided information -// up to credentials page +/* + Function to create a temporary YAML configuration file based on user inputs + This function is called when the user clicks "Next" on the endpoint/region page + It creates a temporary config file that can be used for testing credentials + and then removed after the credentials are verified. +*/ func createTmpConfigFile() error { config := Config{ Components: []string{storageProtocol}, } - if storageProtocol == "azstorage" { - config.AzStorage = &AzureStorageConfig{ + config.AzStorage = AzureStorageConfig{ Type: "block", AccountName: accountName, - AccountKey: secretKey, - Endpoint: endpointURL, + AccountKey: accountKey, Mode: "key", - Container: bucketName, + Container: containerName, } - } else if storageProtocol == "s3storage" { + } else { config.S3Storage = S3StorageConfig{ KeyID: accessKey, SecretKey: secretKey, Endpoint: endpointURL, - Region: region, EnableDirMarker: true, } } @@ -988,133 +1302,131 @@ func createTmpConfigFile() error { return fmt.Errorf("failed to marshal YAML: %v", err) } - tmpFile := "config-temp.yaml" + tmpFile := "config-tmp.yaml" if err := os.WriteFile(tmpFile, yamlData, 0644); err != nil { return fmt.Errorf("failed to write YAML to file: %v", err) } // Update options.ConfigFile to point to the temporary file - options.ConfigFile = "config-temp.yaml" - fmt.Printf("Temporary YAML config written to %s\n", tmpFile) + options.ConfigFile = "config-tmp.yaml" + return nil +} + + +/* + Function to check the credentials entered by the user + This function is called when the user clicks "Next" on the credentials page + It tries to connect to the storage backend and fetch the container/bucket list + If successful, it removes the temporary config file and returns the container/bucket list + If it fails, it returns the error +*/ +func checkCredentials(app *tview.Application, pages *tview.Pages) error { + // Step 1: Create a temporary config file + createTmpConfigFile() + + // Step 2: Parse the config + err := parseConfig() + if err != nil { + return err + } + + err = config.Unmarshal(&options) + if err != nil { + return err + } + + // Step 3: Try to fetch container/bucket list + if slices.Contains(options.Components, "azstorage") { + containerList, err = getContainerListAzure() + + } else if slices.Contains(options.Components, "s3storage") { + containerList, err = getBucketListS3() + + } else { + err = fmt.Errorf("Unsupported storage backend") + } + + if err != nil { + return err + } + + // Step 4: Remove temporary config file + if err := os.Remove("config-tmp.yaml"); err != nil { + return err + } + return nil } -// Function to create YAML configuration file from all data collected from the TUI -func createYAMLConfig() { +/* + Function to create a YAML configuration file based on user inputs + This function is called when the user clicks "Finish" on the caching page + It creates a config.yaml file that can be used by CloudFuse. + Returns an error if the YAML creation fails or if the file cannot be written. +*/ +func createYAMLConfig() error { config := Config{ - Logging: LoggingConfig{ - Type: "syslog", - Level: "log_warning", - }, - Components: []string{"libfuse", cacheMode, "attr_cache", storageProtocol}, Libfuse: LibfuseConfig{ - AttributeExpirationSec: 120, - EntryExpirationSec: 120, - NegativeEntryExpirationSec: 240, - NetworkShare: true, + NetworkShare: true, }, - - // Stream: StreamConfig{ - // BlockSizeMB: 8, - // BlocksPerFile: 3, - // CacheSizeMB: 1024, - // }, AttrCache: AttrCacheConfig{ TimeoutSec: 7200, }, } - switch cacheMode { - case "file_cache": + if cacheMode == "file_cache" { config.FileCache = FileCacheConfig{ - Path: "Path/to/cache/dir", - TimeOutSec: 64000000, - CleanUpOnStart: true, - IgnoreSync: true, - } - case "block_cache": - config.BlockCache = BlockCacheConfig{ - BlockSizeMB: 8, - MemorySizeMB: 1024, - Prefetch: 2, - Parallelism: 4, + Path: cacheLocation, + TimeOutSec: cacheRetentionDurationSec, + AllowNonEmptyTemp: !clearCacheOnStart, + IgnoreSync: true, } - default: // "stream" or any unrecognized mode defaults to stream - config.Stream = StreamConfig{ - BlockSizeMB: 8, - BlocksPerFile: 3, - CacheSizeMB: 1024, + // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it + if cacheSize != "80" { + config.FileCache.MaxSizeMB = currentCacheSizeGB * 1024 // Convert GB to MB } - } - + } if storageProtocol == "s3storage" { config.S3Storage = S3StorageConfig{ - BucketName: bucketName, // This should be set from the bucket - KeyID: accessKey, // This should be set from the access key input - SecretKey: secretKey, // This should be set from the secret key input - Endpoint: endpointURL, // This should be set from the URL input - Region: region, // This should be set from the region input - EnableDirMarker: true, // Default to true, can be changed in the TUI + BucketName: bucketName, + KeyID: accessKey, + SecretKey: secretKey, + Endpoint: endpointURL, + EnableDirMarker: true, } } else { - config.AzStorage = &AzureStorageConfig{ + config.AzStorage = AzureStorageConfig{ Type: "block", - AccountName: accountName, // This should be set from the account name input - AccountKey: secretKey, // This should be set from the account key input - Endpoint: endpointURL, // This should be set from the URL input - Mode: "key", // Default mode, can be changed in the TUI - Container: bucketName, // This should be set from the container name input + AccountName: accountName, + AccountKey: secretKey, + Mode: "key", + Container: containerName, } } // Marshal the struct to YAML (returns []byte and error) yamlData, err := yaml.Marshal(&config) if err != nil { - fmt.Printf("Failed to marshal YAML: %v", err) + return fmt.Errorf("Failed to marshal YAML: %v", err) } // Write the YAML to a file if err := os.WriteFile("config.yaml", yamlData, 0644); err != nil { - fmt.Printf("Failed to write YAML to file: %v", err) + return fmt.Errorf("Failed to write YAML to file: %v", err) } - fmt.Printf("YAML config written to config.yaml\n") - -} - -var ( - azureRegions = []string{ - "us-east", "us-west", "us-central", "us-south", - "eu-west", "eu-central", "eu-south", "eu-north", - "asia-east", "asia-west", "asia-south", "asia-central", - "au-east", "au-west", "au-central", "au-south", - "sa-east", "sa-west", "sa-central", "sa-south", - "africa-north", "africa-south", "africa-west", "africa-east", - "canada-east", "canada-west", "canada-central", "canada-south", - "middle-east-north", "middle-east-south", "middle-east-central", - "japan-east", "japan-west", "japan-central", "japan-south" } - - awsRegions = []string{ - "us-east-1", "us-east-2", "us-west-1", "us-west-2", - "af-south-1", "ap-east-1", "ap-south-1", "ap-south-2", - "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", - "ap-southeast-4", "ap-southeast-5", "ap-southeast-7", - "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", - "ca-central-1", "ca-west-1", "eu-central-1", - "eu-west-1", "eu-west-2", "eu-west-3", - "eu-south-1", "eu-south-2", "eu-north-1", - "eu-central-2", "il-central-1", "mx-central-1", - "me-south-1", "me-central-1", "sa-east-1", - } - - lyvecloudRegions = []string{ - "us-east-1", "us-west-1", "us-central-1", "eu-west-1", - } + // Update global configFilePath variable + currDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("Error: %v", err) + } + configFilePath = filepath.Join(currDir, "config.yaml") -) \ No newline at end of file + return nil +} \ No newline at end of file From 33ab4b5ab2694e24bec5b1075a390879867f9647 Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 18 Aug 2025 15:58:09 -0600 Subject: [PATCH 04/21] Update dependencies, tidy go.mod/go.sum, NOTICE --- NOTICE | 571 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 9 +- go.sum | 67 +++++++ 3 files changed, 646 insertions(+), 1 deletion(-) diff --git a/NOTICE b/NOTICE index a783e7a43..f76d04fa0 100644 --- a/NOTICE +++ b/NOTICE @@ -10389,4 +10389,575 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + + +**************************************************************************** + +============================================================================ +>>> github.com/gdamore/encoding +============================================================================== + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + +**************************************************************************** + +============================================================================ +>>> github.com/gdamore/tcell/v2 +============================================================================== + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + +**************************************************************************** + +============================================================================ +>>> github.com/lucasb-eyer/go-colorful +============================================================================== + +Copyright (c) 2013 Lucas Beyer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + + +**************************************************************************** + +============================================================================ +>>> github.com/mattn/go-runewidth +============================================================================== + +The MIT License (MIT) + +Copyright (c) 2016 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + + +**************************************************************************** + +============================================================================ +>>> github.com/rivo/tview +============================================================================== + +MIT License + +Copyright (c) 2018 Oliver Kuederle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + + +**************************************************************************** + +============================================================================ +>>> github.com/rivo/uniseg +============================================================================== + +MIT License + +Copyright (c) 2019 Oliver Kuederle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + + +**************************************************************************** + +============================================================================ +>>> golang.org/x/telemetry +============================================================================== + +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --------------------- END OF THIRD PARTY NOTICE -------------------------------- diff --git a/go.mod b/go.mod index 45d7125e2..8088b67d5 100644 --- a/go.mod +++ b/go.mod @@ -16,11 +16,13 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.86.0 github.com/aws/smithy-go v1.22.5 github.com/fsnotify/fsnotify v1.9.0 + github.com/gdamore/tcell/v2 v2.8.1 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang/mock v1.6.0 github.com/montanaflynn/stats v0.7.1 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/radovskyb/watcher v1.0.7 + github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb github.com/robfig/cron/v3 v3.0.1 github.com/sevlyar/go-daemon v0.1.7-0.20240723083326-c2a11b2b57fc github.com/shirou/gopsutil/v4 v4.25.7 @@ -35,6 +37,7 @@ require ( golang.org/x/sys v0.35.0 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -57,17 +60,21 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/ebitengine/purego v0.8.4 // indirect + github.com/gdamore/encoding v1.0.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -80,8 +87,8 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.40.0 // indirect golang.org/x/net v0.42.0 // indirect + golang.org/x/term v0.33.0 // indirect golang.org/x/text v0.27.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/spf13/cobra => github.com/gapra-msft/cobra v1.4.1-0.20220411185530-5b83e8ba06dd diff --git a/go.sum b/go.sum index f616005cf..f7f487ce2 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,10 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gapra-msft/cobra v1.4.1-0.20220411185530-5b83e8ba06dd h1:U3d5Jlb0ANsyxk2lnlhYh7/Ov4bZpIBUxJTsVuJM9G0= github.com/gapra-msft/cobra v1.4.1-0.20220411185530-5b83e8ba06dd/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= +github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= +github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= +github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -83,6 +87,7 @@ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -100,8 +105,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= @@ -116,6 +125,12 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= +github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8= +github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -154,6 +169,7 @@ github.com/vibhansa-msft/tlru v0.0.0-20240410102558-9e708419e21f/go.mod h1:7G2C6 github.com/winfsp/cgofuse v1.6.0 h1:re3W+HTd0hj4fISPBqfsrwyvPFpzqhDu8doJ9nOPDB0= github.com/winfsp/cgofuse v1.6.0/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -162,16 +178,38 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -180,18 +218,47 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From d8d5f7096667d2bb61aebb0fd8ec24dbfbccbb23 Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 18 Aug 2025 16:01:54 -0600 Subject: [PATCH 05/21] Add license to cmd/config.go --- cmd/config.go | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 7f9615c08..a0e4db736 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,3 +1,28 @@ +/* + Licensed under the MIT License . + + Copyright © 2023-2025 Seagate Technology LLC and/or its Affiliates + Copyright © 2020-2025 Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +*/ + package cmd import ( @@ -8,11 +33,11 @@ import ( var configCmd = &cobra.Command{ Use: "config", - Short: "Launch the interactive configuration TUI", - Long: "Starts an interactive terminal-based UI to generate or edit your Cloudfuse configuration.", + Short: "Launch the interactive configuration tool.", + Long: "Starts an interactive terminal-based UI to generate your Cloudfuse configuration file.", RunE: func(cmd *cobra.Command, args []string) error { if err := runTUI(); err != nil { - return fmt.Errorf("failed to run TUI: %v", err) + return fmt.Errorf("Failed to run TUI: %v", err) } return nil }, From 52161f95d593e2140ade92116938098aba9a0ed6 Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 18 Aug 2025 16:04:44 -0600 Subject: [PATCH 06/21] Change common/util_windows.go GetAvailFree() to return blocks --- common/util_windows.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/util_windows.go b/common/util_windows.go index cbd898823..475f83207 100644 --- a/common/util_windows.go +++ b/common/util_windows.go @@ -113,7 +113,9 @@ func GetAvailFree(path string) (uint64, uint64, error) { if err != nil { return 0, 0, err } - return avail, free, nil + // Quick fix: Convert bytes to number of blocks to match Linux implementation for now. + // TODO: Eventually, change this back to return bytes instead of blocks, and fix linux implementation to match. + return avail / 4096, free / 4096, nil } // GetFreeRam: Available ram From 0bce243434c60785842c969148cb4b619debd694 Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 18 Aug 2025 16:08:41 -0600 Subject: [PATCH 07/21] Implemented PR feedback, cleaned up code --- cmd/tui.go | 362 ++++++++++++++++++++++------------------------------- 1 file changed, 153 insertions(+), 209 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index cd3794aa5..ee1254a15 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -33,7 +33,6 @@ import ( "slices" "strconv" "strings" - "syscall" "time" "github.com/Seagate/cloudfuse/common" @@ -43,13 +42,11 @@ import ( "gopkg.in/yaml.v3" ) -/* - Constants and global variables used throughout the TUI application. - These include default values, colors, widget configurations, and storage settings. -*/ +// Constants and global variables used throughout the TUI application. +// These include default values, colors, widget configurations, and storage settings. var ( - tuiVersion string = common.CloudfuseVersion - configFilePath string // Sets file_cache.path + tuiVersion string = common.CloudfuseVersion // Mirrors the current version of cloudfuse. + configFilePath string // Sets file_cache.path accountName string // Sets azstorage.account-name accountKey string // Sets azstorage.account-key accessKey string // Sets s3storage.key-id @@ -57,7 +54,7 @@ var ( containerName string // Sets azstorage.container-name bucketName string // Sets s3storage.bucket-name endpointURL string // Sets s3storage.endpoint - containerList = []string {} // Holds list of available buckets retrieved from cloud provider + bucketList = []string {} // Holds list of available buckets retrieved from cloud provider (for s3 only). storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' @@ -91,24 +88,24 @@ var ( navigationWidgetHeight int = 3 ) -type Config struct { +type configuration struct { Components []string `yaml:"components,omitempty"` - Libfuse LibfuseConfig `yaml:"libfuse,omitempty"` - FileCache FileCacheConfig `yaml:"file_cache,omitempty"` - AttrCache AttrCacheConfig `yaml:"attr_cache,omitempty"` - S3Storage S3StorageConfig `yaml:"s3storage,omitempty"` - AzStorage AzureStorageConfig `yaml:"azstorage,omitempty"` + Libfuse libfuseConfig `yaml:"libfuse,omitempty"` + FileCache fileCacheConfig `yaml:"file_cache,omitempty"` + AttrCache attrCacheConfig `yaml:"attr_cache,omitempty"` + S3Storage s3StorageConfig `yaml:"s3storage,omitempty"` + AzStorage azureStorageConfig `yaml:"azstorage,omitempty"` } -type LibfuseConfig struct { +type libfuseConfig struct { NetworkShare bool `yaml:"network-share"` } -type AttrCacheConfig struct { +type attrCacheConfig struct { TimeoutSec int `yaml:"timeout-sec"` } -type FileCacheConfig struct { +type fileCacheConfig struct { Path string `yaml:"path"` TimeOutSec int `yaml:"timeout-sec"` AllowNonEmptyTemp bool `yaml:"allow-non-empty-temp"` @@ -116,7 +113,7 @@ type FileCacheConfig struct { MaxSizeMB int `yaml:"max-size-mb,omitempty"` } -type S3StorageConfig struct { +type s3StorageConfig struct { BucketName string `yaml:"bucket-name,omitempty"` KeyID string `yaml:"key-id"` SecretKey string `yaml:"secret-key"` @@ -124,7 +121,7 @@ type S3StorageConfig struct { EnableDirMarker bool `yaml:"enable-dir-marker"` } -type AzureStorageConfig struct { +type azureStorageConfig struct { Type string `yaml:"type"` AccountName string `yaml:"account-name"` AccountKey string `yaml:"account-key"` @@ -134,10 +131,8 @@ type AzureStorageConfig struct { } -/* - Main function to run the TUI application. - It initializes the tview application, builds the TUI application, and runs it. -*/ +// Main function to run the TUI application. +// Initializes the tview application, builds the TUI application, and runs it. func runTUI() error{ app := tview.NewApplication() app.EnableMouse(true) @@ -154,10 +149,7 @@ func runTUI() error{ } -/* - Function to build the TUI application. - It initializes the main pages and adds them to the page stack. -*/ +// Function to build the TUI application. Initializes the pages and adds them to the page stack. func buildTUI(app *tview.Application) { pages := tview.NewPages() @@ -166,7 +158,7 @@ func buildTUI(app *tview.Application) { page1 := buildStorageProviderPage(app, pages) // --- Page 1: Storage Provider Selection --- page2 := buildEndpointURLPage(app, pages) // --- Page 2: Endpoint URL Entry --- page3 := buildCredentialsPage(app, pages) // --- Page 3: Credentials Entry --- - page4 := buildContainerSelectPage(app, pages) // --- Page 4: Bucket Selection --- + page4 := buildBucketSelectionPage(app, pages) // --- Page 4: Bucket Selection --- page5 := buildCachingPage(app, pages) // --- Page 5: Caching Settings --- // Add pages to the page stack @@ -181,11 +173,9 @@ func buildTUI(app *tview.Application) { } -/* - --- Page 0: Home Page --- - Displays a welcome banner, instructions, and buttons to start or quit the application. - It also includes an "About" section with information about the tool. -*/ +// --- Page 0: Home Page --- +// Function to build the home page of the TUI application. Displays a +// welcome banner, instructions, and buttons to start or quit the application. func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { bannerText := "[#6EBE49::b]░█▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + @@ -258,11 +248,9 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { } -/* - --- Page 1: Storage Provider Selection --- - This page allows users to select their cloud storage provider from a dropdown list. - It provides options for LyveCloud, Microsoft, AWS, and Other S3. -*/ +// --- Page 1: Storage Provider Selection --- +// Function to build the storage provider selection page. Allows users to select their cloud storage provider +// from a dropdown list. The options are: LyveCloud, Microsoft, AWS, and Other S3. func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + @@ -352,11 +340,9 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. } -/* - --- Page 2: Endpoint and Region Page --- - This page allows users to enter the endpoint URL for their cloud storage provider. - It validates the endpoint URL format and provides help text based on the selected provider. -*/ +// --- Page 2: Endpoint and Region Page --- +// Function to build the endpoint URL page. Allows users to enter the endpoint URL for their cloud storage provider. +// It validates the endpoint URL format and provides help text based on the selected provider. func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Primitive { var urlRegionHelpText string @@ -450,12 +436,10 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim } -/* - --- Page 3: Credentials Page --- - This page allows users to enter their cloud storage credentials. - If the storage protocol is "s3", it provides input fields for access key, secret key. - If the storage protocol is "azure", it provides input fields for account name, account key, and container name. -*/ +// --- Page 3: Credentials Page --- +// Function to build the credentials page. Allows users to enter their cloud storage credentials. +// If the storage protocol is "s3", it provides input fields for access key, secret key. +// If the storage protocol is "azure", it provides input fields for account name, account key, and container name. func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { layout := tview.NewFlex() layout.Clear() @@ -495,9 +479,9 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim SetLabel(accessLabel). SetText(accessKey). SetFieldWidth(50). - SetChangedFunc(func(text string) { - accessKey = text - accountName = text + SetChangedFunc(func(key string) { + accessKey = key + accountName = key }). SetPlaceholder("\t\t\t\t"). SetLabelColor(widgetLabelColor). @@ -507,11 +491,11 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim // Secret key field widget with masked input secretKeyFieldWidget := tview.NewInputField(). SetLabel(secretLabel). - SetText(secretKey). + SetText(string(secretKey)). SetFieldWidth(50). - SetChangedFunc(func(text string) { - secretKey = text - accountKey = text + SetChangedFunc(func(key string) { + secretKey = key + accountKey = key }). SetPlaceholder("\t\t\t\t"). SetMaskCharacter('*'). @@ -540,9 +524,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim AddButton(navigationNextLabel, func() { // TODO: Add validation for access key and secret key HERE // For now, just check that they are not empty - // TODO: Add check to make sure containerName is not empty if storageProvider is Microsoft - if (storageProtocol == "s3storage" && (accessKey == "" || secretKey == "")) || (storageProtocol == "azstorage" && (accountName == "" || accountKey == "" || containerName == "")) { - // if accessKey == "" || accountName == "" || secretKey == "" || accountKey == "" { + if (storageProtocol == "s3storage" && (len(accessKey) == 0 || len(secretKey) == 0)) || (storageProtocol == "azstorage" && (len(accountName) == 0 || len(accountKey) == 0 || len(containerName) == 0)) { showErrorModal(app, pages, "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", func() { pages.SwitchToPage("page3") }) @@ -565,7 +547,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim pages.RemovePage("page4") // Remove previous page if it exists pages.SwitchToPage("page5") } else { - page4 := buildContainerSelectPage(app, pages) + page4 := buildBucketSelectionPage(app, pages) pages.AddPage("page4", page4, true, false) pages.SwitchToPage("page4") } @@ -619,13 +601,10 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim } -/* - --- Page 4: Container Name Selection --- - This page allows users to select a bucket name from a dropdown list. - It retrieves the list of available buckets from the cloud storage provider based on the - credentials provided in the previous step. -*/ -func buildContainerSelectPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +// --- Page 4: Bucket Name Selection --- +// Function to build the bucket selection page. Allows users to select a bucket from a dropdown list +// of retrieved buckets based on provided s3 credentials. For s3 storage users only. Azure storage users will skip this page. +func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + @@ -640,9 +619,9 @@ func buildContainerSelectPage(app *tview.Application, pages *tview.Pages) tview. SetText(instructionsText) // Dropdown widget for selecting bucket name - containerSelectionWidget := tview.NewDropDown(). + bucketSelectionWidget := tview.NewDropDown(). SetLabel(" 🪣 Bucket Name: "). - SetOptions(containerList, func(text string, index int) { + SetOptions(bucketList, func(text string, index int) { bucketName = text }). SetCurrentOption(0). @@ -679,7 +658,7 @@ func buildContainerSelectPage(app *tview.Application, pages *tview.Pages) tview. SetDirection(tview.FlexRow). AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). AddItem(nil, 2, 0, false). - AddItem(containerSelectionWidget, 2, 0, false). + AddItem(bucketSelectionWidget, 2, 0, false). AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) @@ -689,11 +668,9 @@ func buildContainerSelectPage(app *tview.Application, pages *tview.Pages) tview. } -/* - --- Page 5: Caching Settings --- - Function to build the caching page that allows users to configure caching settings. - This page includes options for cache location, size, and retention settings. -*/ +// --- Page 5: Caching Settings --- +// Function to build the caching page that allows users to configure caching settings. +// Includes options for enabling/disabling caching, specifying cache location, size, and retention settings. func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { // Main layout container. Must be instantiated first to allow nested items. layout := tview.NewFlex().SetDirection(tview.FlexRow) @@ -881,7 +858,7 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv if storageProtocol == "azstorage" { pages.SwitchToPage("page3") } else { - page4 := buildContainerSelectPage(app, pages) + page4 := buildBucketSelectionPage(app, pages) pages.AddPage("page4", page4, true, false) pages.SwitchToPage("page4") } @@ -943,12 +920,10 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv } -/* - --- Summary Page --- - Function to build the summary page that displays the configuration summary. - This function creates a text view with the summary information and a return button. - It takes an application instance, pages instance, and the preview page name as parameters. -*/ +// --- Summary Page --- +// Function to build the summary page that displays the configuration summary. +// This function creates a text view with the summary information and a return button. +// The preview page parameter allows switching back to the previous page when the user clicks "Return". func buildPreviewPage(app *tview.Application, pages *tview.Pages, previewPage string) tview.Primitive { summaryText := "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n"+ @@ -1009,12 +984,9 @@ func buildPreviewPage(app *tview.Application, pages *tview.Pages, previewPage st } -/* - Function to show a modal dialog with a message and an "OK" button. - This function is used to display error messages or confirmations. - It takes an application instance, pages instance, message string, - and a callback function to execute when the modal is closed. -*/ +// Function to show a modal dialog with a message and an "OK" button. +// This function is used to display error messages or confirmations. +// May specify a callback function to execute when the modal is closed. func showErrorModal(app *tview.Application, pages *tview.Pages, message string, onClose func()) { modal := tview.NewModal(). SetText(message). @@ -1033,11 +1005,8 @@ func showErrorModal(app *tview.Application, pages *tview.Pages, message string, } -/* - Function to show a confirmation modal dialog with "Finish" and "Return" buttons. - Used to confirm cache size before proceeding. It takes an application instance, - pages instance, message string, and two callback functions for the "Finish" and "Return" actions. -*/ +// Function to show a confirmation modal dialog with "Finish" and "Return" buttons. +// Used to confirm cache size before proceeding. Must specify two callback functions for the "Finish" and "Return" actions. func showCacheConfirmationModal(app *tview.Application, pages *tview.Pages, message string, onFinish func(), onReturn func()) { modal := tview.NewModal(). SetText(message). @@ -1060,12 +1029,9 @@ func showCacheConfirmationModal(app *tview.Application, pages *tview.Pages, mess } -/* - Function to show final exit modal when configuration is complete. - This function is called when the user clicks "Finish" on the caching page. - It informs the user that the configuration is complete and they can exit. - It also creates a small processing emoji animation then shows the path to the config file. -*/ +// Function to show final exit modal when configuration is complete. +// Informs the user that the configuration is complete and they can exit. +// This function is called when the user clicks "Finish" on the caching page. func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) { processingEmojis := []string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "✅"} @@ -1109,12 +1075,8 @@ func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) } -/* - Helper function to center lines of text within a specified width. - This function handles color tags and ensures that the text is centered - even if it contains ANSI escape codes or other formatting. - It is used to format text views and other UI elements in the TUI. -*/ +// Helper function to center lines of text within a specified width. +// It is used to format text views and other UI elements in the TUI. func centerText(text string, width int) string { var centeredLines []string lines := strings.Split(text, "\n") @@ -1131,12 +1093,8 @@ func centerText(text string, width int) string { } -/* - Helper function to get the length of the longest line in a string. - It splits the string by newline characters and finds the maximum length of the lines. - It returns 0 if the string is empty. - It is used to determine the width of text views and other UI elements. -*/ +// Helper function to get the length of the longest line in a string. +// It is used to determine the width of text views and other UI elements. func getTextWidth(s string) int { if s == "" { return 0 @@ -1152,12 +1110,8 @@ func getTextWidth(s string) int { } -/* - Helper function to count the number of lines in a string. - It splits the string by newline characters and returns the length of the resulting slice. - It returns 0 if the string is empty. - It is used to determine the height of text views and other UI elements. -*/ +// Helper function to count the number of lines in a string. +// It is used to determine the height of text views and other UI elements. func getTextHeight(s string) int { if s == "" { return 0 @@ -1166,40 +1120,41 @@ func getTextHeight(s string) int { } -/* - Helper function to get the default cache path. - It retrieves the user's home directory and constructs a default cache path: - `~/.cloudfuse/file_cache` - If the directory does not exist, it attempts to create it. - If it fails to retrieve the home directory or create the path, it returns a fallback path. - It returns the full path to the cache directory. -*/ +// Helper function to get a fallback cache path if the home directory cannot be determined. +func getFallbackCachePath() string { + user := os.Getenv("USER") + if user == "" { + uid := os.Getuid() + user = fmt.Sprintf("uid_%d", uid) + } + return filepath.Join(os.TempDir(), "cloudfuse", user) +} + + +// Helper function to get the default cache path. +// It retrieves the user's home directory and constructs a default cache path: +// `~/.cloudfuse/file_cache`. If it fails to retrieve the home directory or create the path, it returns a fallback path. func getDefaultCachePath() string { + // TODO: Add logic to return OS-specific cache paths home, err := os.UserHomeDir() if err != nil { - // TODO: Handle error if home directory cannot be retrieved. - // TODO: Choose a fallback path for cache directory. - return "/tmp/cloudfuse/cloudfuse.log" + fmt.Printf("[red::b]ERROR: Failed to get home directory: %v\nUsing fallback path for cache directory.\n", err) + return getFallbackCachePath() } - filepath := filepath.Join(home, ".cloudfuse", "file_cache") + cachePath := filepath.Join(home, ".cloudfuse", "file_cache") // If the directory doesn't exist, create it - if _, err := os.Stat(filepath); os.IsNotExist(err) { - if err := os.MkdirAll(filepath, 0755); err != nil { - fmt.Printf("Failed to create cache directory: %v\n", err) - // TODO: Handle error if cache directory cannot be created - return "/tmp/cloudfuse/cloudfuse.log" // fallback path + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + if err := os.MkdirAll(cachePath, 0700); err != nil { + fmt.Printf("[red::b]ERROR: Failed to create cache directory: %v\nUsing fallback path for cache directory.\n", err) + return getFallbackCachePath() } } // Return the full path to the cache directory - return filepath + return cachePath } -/* - Helper function to validate the entered cache path. It checks if the - path is not empty, does not contain invalid characters, and if it exists. - It returns an error if any validation fails. -*/ +// Helper function to validate the entered cache path. func validateCachePath() error { // Validate that the path is not empty if strings.TrimSpace(cacheLocation) == "" { @@ -1219,32 +1174,30 @@ func validateCachePath() error { } -/* - Helper function to get the available cache size. - It retrieves the available space in the cache location and calculates - the available cache size in GB based on the user-defined cache size percentage. - It sets the global variable `availableCacheSizeGB` and calculates the current cache size. - If it fails to retrieve the available space, it returns an error. -*/ +// Helper function to get the available disk space at the cache location and calculates +// the cache size in GB based on the user-defined cache size percentage. func getAvailableCacheSize() (error) { - var stat syscall.Statfs_t - if err := syscall.Statfs(cacheLocation, &stat); err != nil { - return fmt.Errorf("[red::b]ERROR: Failed to get available cache size[-::-]: %v", err) + availableBlocks, _, err := common.GetAvailFree(cacheLocation) + if err != nil { + // If we fail to get the available cache size, we default to 80% of the available disk space + cacheSize = "80" + returnMsg := fmt.Errorf("[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ + "Defaulting cache size to 80%% of available disk space.\n\n"+ + "Please manually verify you have enough disk space available for caching.[-::-]", cacheLocation, err) + return returnMsg } - availableCacheSizeBytes := stat.Bavail * uint64(stat.Bsize) // Available space in bytes + + const blockSize = 4096 + availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes availableCacheSizeGB = int(availableCacheSizeBytes / (1024 * 1024 * 1024)) // Convert to GB cacheSizeInt, _ := strconv.Atoi(cacheSize) currentCacheSizeGB = int(availableCacheSizeGB) * cacheSizeInt / 100 + return nil } -/* - Helper function to normalize and validate the endpoint URL. - It checks if the URL starts with "http://" or "https://", and if not, it prepends "https://". - It also parses the URL to ensure it is valid. - It returns the normalized URL or an error if the URL is invalid. -*/ +// Helper function to normalize and validate the user-defined endpoint URL. func validateEndpointURL(rawURL string) (error) { rawURL = strings.TrimSpace(rawURL) @@ -1268,20 +1221,17 @@ func validateEndpointURL(rawURL string) (error) { } -/* - Function to create a temporary YAML configuration file based on user inputs - This function is called when the user clicks "Next" on the endpoint/region page - It creates a temporary config file that can be used for testing credentials - and then removed after the credentials are verified. -*/ +// Function to create a temporary YAML configuration file based on user inputs. +// Used for testing credentials and then removed after the check. +// Called when the user clicks "Next" on the credentials page. func createTmpConfigFile() error { - config := Config{ - + config := configuration{ + Components: []string{storageProtocol}, } if storageProtocol == "azstorage" { - config.AzStorage = AzureStorageConfig{ + config.AzStorage = azureStorageConfig{ Type: "block", AccountName: accountName, AccountKey: accountKey, @@ -1289,7 +1239,7 @@ func createTmpConfigFile() error { Container: containerName, } } else { - config.S3Storage = S3StorageConfig{ + config.S3Storage = s3StorageConfig{ KeyID: accessKey, SecretKey: secretKey, Endpoint: endpointURL, @@ -1303,7 +1253,7 @@ func createTmpConfigFile() error { } tmpFile := "config-tmp.yaml" - if err := os.WriteFile(tmpFile, yamlData, 0644); err != nil { + if err := os.WriteFile(tmpFile, yamlData, 0600); err != nil { return fmt.Errorf("failed to write YAML to file: %v", err) } @@ -1313,74 +1263,67 @@ func createTmpConfigFile() error { } -/* - Function to check the credentials entered by the user - This function is called when the user clicks "Next" on the credentials page - It tries to connect to the storage backend and fetch the container/bucket list - If successful, it removes the temporary config file and returns the container/bucket list - If it fails, it returns the error -*/ +// Function to check the credentials entered by the user. +// Attempts to connect to the storage backend and fetch the bucket list. +// If successful, populates the global `bucketList` variable with the list of available buckets (for s3 providers only). +// Called when the user clicks "Next" on the credentials page. func checkCredentials(app *tview.Application, pages *tview.Pages) error { - // Step 1: Create a temporary config file - createTmpConfigFile() + // Create a temporary config file for testing credentials + if err := createTmpConfigFile(); err != nil { + return fmt.Errorf("Failed to create temporary config file: %v", err) + } - // Step 2: Parse the config - err := parseConfig() - if err != nil { - return err + // Delete the temporary config file regardless of success or failure of the credential check + defer func() { + _ = os.Remove("config-tmp.yaml") + }() + + // Parse and unmarshal the temporary config file + if err := parseConfig(); err != nil { + return fmt.Errorf("Failed to parse config: %v", err) } - err = config.Unmarshal(&options) - if err != nil { - return err + if err := config.Unmarshal(&options); err != nil { + return fmt.Errorf("Failed to unmarshal config: %v", err) } - // Step 3: Try to fetch container/bucket list + // Try to fetch bucket list + var err error if slices.Contains(options.Components, "azstorage") { - containerList, err = getContainerListAzure() + bucketList, err = getContainerListAzure() } else if slices.Contains(options.Components, "s3storage") { - containerList, err = getBucketListS3() - + bucketList, err = getBucketListS3() + } else { err = fmt.Errorf("Unsupported storage backend") } if err != nil { - return err - } - - // Step 4: Remove temporary config file - if err := os.Remove("config-tmp.yaml"); err != nil { - return err + return fmt.Errorf("Failed to get bucket list: %v", err) } return nil } -/* - Function to create a YAML configuration file based on user inputs - This function is called when the user clicks "Finish" on the caching page - It creates a config.yaml file that can be used by CloudFuse. - Returns an error if the YAML creation fails or if the file cannot be written. -*/ +// Function to create the YAML configuration file based on user inputs once all forms are completed. +// Called when the user clicks "Finish" on the caching page. func createYAMLConfig() error { - - config := Config{ + config := configuration{ Components: []string{"libfuse", cacheMode, "attr_cache", storageProtocol}, - Libfuse: LibfuseConfig{ + Libfuse: libfuseConfig{ NetworkShare: true, }, - AttrCache: AttrCacheConfig{ + AttrCache: attrCacheConfig{ TimeoutSec: 7200, }, } if cacheMode == "file_cache" { - config.FileCache = FileCacheConfig{ + config.FileCache = fileCacheConfig{ Path: cacheLocation, TimeOutSec: cacheRetentionDurationSec, AllowNonEmptyTemp: !clearCacheOnStart, @@ -1393,7 +1336,7 @@ func createYAMLConfig() error { } if storageProtocol == "s3storage" { - config.S3Storage = S3StorageConfig{ + config.S3Storage = s3StorageConfig{ BucketName: bucketName, KeyID: accessKey, SecretKey: secretKey, @@ -1401,10 +1344,10 @@ func createYAMLConfig() error { EnableDirMarker: true, } } else { - config.AzStorage = AzureStorageConfig{ + config.AzStorage = azureStorageConfig{ Type: "block", AccountName: accountName, - AccountKey: secretKey, + AccountKey: accountKey, Mode: "key", Container: containerName, } @@ -1417,7 +1360,7 @@ func createYAMLConfig() error { } // Write the YAML to a file - if err := os.WriteFile("config.yaml", yamlData, 0644); err != nil { + if err := os.WriteFile("config.yaml", yamlData, 0600); err != nil { return fmt.Errorf("Failed to write YAML to file: %v", err) } @@ -1426,6 +1369,7 @@ func createYAMLConfig() error { if err != nil { return fmt.Errorf("Error: %v", err) } + configFilePath = filepath.Join(currDir, "config.yaml") return nil From 469081ad3459c23a32dcb4e6cfc5db43f2305dd2 Mon Sep 17 00:00:00 2001 From: brayan Date: Tue, 19 Aug 2025 16:21:16 -0600 Subject: [PATCH 08/21] Fix linting issues --- cmd/config.go | 2 +- cmd/tui.go | 823 ++++++++++++++++++++++++++------------------------ 2 files changed, 437 insertions(+), 388 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index a0e4db736..18edae469 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -45,4 +45,4 @@ var configCmd = &cobra.Command{ func init() { rootCmd.AddCommand(configCmd) -} \ No newline at end of file +} diff --git a/cmd/tui.go b/cmd/tui.go index ee1254a15..ba1a542a1 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -45,95 +45,93 @@ import ( // Constants and global variables used throughout the TUI application. // These include default values, colors, widget configurations, and storage settings. var ( - tuiVersion string = common.CloudfuseVersion // Mirrors the current version of cloudfuse. - configFilePath string // Sets file_cache.path - accountName string // Sets azstorage.account-name - accountKey string // Sets azstorage.account-key - accessKey string // Sets s3storage.key-id - secretKey string // Sets s3storage.secret-key - containerName string // Sets azstorage.container-name - bucketName string // Sets s3storage.bucket-name - endpointURL string // Sets s3storage.endpoint - bucketList = []string {} // Holds list of available buckets retrieved from cloud provider (for s3 only). - storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider - storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. - cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' - enableCaching bool = true // If true, sets cacheMode to file_cache. If false, block_cache - cacheLocation string = getDefaultCachePath() // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache - cacheSize string = "80" // User-defined cache size as % - availableCacheSizeGB int // Total available cache size in GB @ the cache location - currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage - clearCacheOnStart bool = false // If false, sets 'allow-non-empty-temp' to true - cacheRetentionDuration int = 2 // User-defined cache retention duration. Default is '2' - cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' - cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' - + tuiVersion string = common.CloudfuseVersion // Mirrors the current version of cloudfuse. + configFilePath string // Sets file_cache.path + accountName string // Sets azstorage.account-name + accountKey string // Sets azstorage.account-key + accessKey string // Sets s3storage.key-id + secretKey string // Sets s3storage.secret-key + containerName string // Sets azstorage.container-name + bucketName string // Sets s3storage.bucket-name + endpointURL string // Sets s3storage.endpoint + bucketList = []string{} // Holds list of available buckets retrieved from cloud provider (for s3 only). + storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider + storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. + cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' + enableCaching bool = true // If true, sets cacheMode to file_cache. If false, block_cache + cacheLocation string = getDefaultCachePath() // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache + cacheSize string = "80" // User-defined cache size as % + availableCacheSizeGB int // Total available cache size in GB @ the cache location + currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage + clearCacheOnStart bool = false // If false, sets 'allow-non-empty-temp' to true + cacheRetentionDuration int = 2 // User-defined cache retention duration. Default is '2' + cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' + cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' + // Global variables for UI elements - tuiAlignment = tview.AlignLeft - previewPage string = "page1" - yellowColor tcell.Color = tcell.GetColor("#FFD700") - greenColor tcell.Color = tcell.GetColor("#6EBE49") - widgetLabelColor = yellowColor - widgetFieldBackgroundColor = yellowColor - navigationButtonColor = greenColor - navigationButtonTextColor = tcell.ColorBlack - navigationButtonAlignment = tview.AlignLeft - navigationStartLabel string = "[black]🚀 Start[-]" - navigationHomeLabel string = "[black]🏠 Home[-]" - navigationNextLabel string = "[black]🡲 Next[-]" - navigationBackLabel string = "[black]🡰 Back[-]" - navigationPreviewLabel string = "[black]📄 Preview[-]" - navigationQuitLabel string = "[black]❌ Quit[-]" - navigationFinishLabel string = "[black]✅ Finish[-]" - navigationWidgetHeight int = 3 + tuiAlignment = tview.AlignLeft + yellowColor tcell.Color = tcell.GetColor("#FFD700") + greenColor tcell.Color = tcell.GetColor("#6EBE49") + widgetLabelColor = yellowColor + widgetFieldBackgroundColor = yellowColor + navigationButtonColor = greenColor + navigationButtonTextColor = tcell.ColorBlack + navigationButtonAlignment = tview.AlignLeft + navigationStartLabel string = "[black]🚀 Start[-]" + navigationHomeLabel string = "[black]🏠 Home[-]" + navigationNextLabel string = "[black]🡲 Next[-]" + navigationBackLabel string = "[black]🡰 Back[-]" + navigationPreviewLabel string = "[black]📄 Preview[-]" + navigationQuitLabel string = "[black]❌ Quit[-]" + navigationFinishLabel string = "[black]✅ Finish[-]" + navigationWidgetHeight int = 3 ) type configuration struct { - Components []string `yaml:"components,omitempty"` - Libfuse libfuseConfig `yaml:"libfuse,omitempty"` - FileCache fileCacheConfig `yaml:"file_cache,omitempty"` - AttrCache attrCacheConfig `yaml:"attr_cache,omitempty"` - S3Storage s3StorageConfig `yaml:"s3storage,omitempty"` - AzStorage azureStorageConfig `yaml:"azstorage,omitempty"` + Components []string `yaml:"components,omitempty"` + Libfuse libfuseConfig `yaml:"libfuse,omitempty"` + FileCache fileCacheConfig `yaml:"file_cache,omitempty"` + AttrCache attrCacheConfig `yaml:"attr_cache,omitempty"` + S3Storage s3StorageConfig `yaml:"s3storage,omitempty"` + AzStorage azureStorageConfig `yaml:"azstorage,omitempty"` } type libfuseConfig struct { - NetworkShare bool `yaml:"network-share"` + NetworkShare bool `yaml:"network-share"` } type attrCacheConfig struct { - TimeoutSec int `yaml:"timeout-sec"` + TimeoutSec int `yaml:"timeout-sec"` } type fileCacheConfig struct { - Path string `yaml:"path"` - TimeOutSec int `yaml:"timeout-sec"` - AllowNonEmptyTemp bool `yaml:"allow-non-empty-temp"` - IgnoreSync bool `yaml:"ignore-sync"` - MaxSizeMB int `yaml:"max-size-mb,omitempty"` + Path string `yaml:"path"` + TimeOutSec int `yaml:"timeout-sec"` + AllowNonEmptyTemp bool `yaml:"allow-non-empty-temp"` + IgnoreSync bool `yaml:"ignore-sync"` + MaxSizeMB int `yaml:"max-size-mb,omitempty"` } type s3StorageConfig struct { - BucketName string `yaml:"bucket-name,omitempty"` - KeyID string `yaml:"key-id"` - SecretKey string `yaml:"secret-key"` - Endpoint string `yaml:"endpoint"` - EnableDirMarker bool `yaml:"enable-dir-marker"` + BucketName string `yaml:"bucket-name,omitempty"` + KeyID string `yaml:"key-id"` + SecretKey string `yaml:"secret-key"` + Endpoint string `yaml:"endpoint"` + EnableDirMarker bool `yaml:"enable-dir-marker"` } type azureStorageConfig struct { - Type string `yaml:"type"` - AccountName string `yaml:"account-name"` - AccountKey string `yaml:"account-key"` - Endpoint string `yaml:"endpoint,omitempty"` - Mode string `yaml:"mode,omitempty"` - Container string `yaml:"container"` + Type string `yaml:"type"` + AccountName string `yaml:"account-name"` + AccountKey string `yaml:"account-key"` + Endpoint string `yaml:"endpoint,omitempty"` + Mode string `yaml:"mode,omitempty"` + Container string `yaml:"container"` } - -// Main function to run the TUI application. +// Main function to run the TUI application. // Initializes the tview application, builds the TUI application, and runs it. -func runTUI() error{ +func runTUI() error { app := tview.NewApplication() app.EnableMouse(true) app.EnablePaste(true) @@ -148,20 +146,19 @@ func runTUI() error{ return nil } - // Function to build the TUI application. Initializes the pages and adds them to the page stack. func buildTUI(app *tview.Application) { pages := tview.NewPages() // Initialize the pages - homePage := buildHomePage(app, pages) // --- Home Page --- + homePage := buildHomePage(app, pages) // --- Home Page --- page1 := buildStorageProviderPage(app, pages) // --- Page 1: Storage Provider Selection --- - page2 := buildEndpointURLPage(app, pages) // --- Page 2: Endpoint URL Entry --- - page3 := buildCredentialsPage(app, pages) // --- Page 3: Credentials Entry --- + page2 := buildEndpointURLPage(app, pages) // --- Page 2: Endpoint URL Entry --- + page3 := buildCredentialsPage(app, pages) // --- Page 3: Credentials Entry --- page4 := buildBucketSelectionPage(app, pages) // --- Page 4: Bucket Selection --- - page5 := buildCachingPage(app, pages) // --- Page 5: Caching Settings --- + page5 := buildCachingPage(app, pages) // --- Page 5: Caching Settings --- - // Add pages to the page stack + // Add pages to the page stack pages.AddPage("home", homePage, true, true) pages.AddPage("page1", page1, true, false) pages.AddPage("page2", page2, true, false) @@ -172,30 +169,30 @@ func buildTUI(app *tview.Application) { app.SetRoot(pages, true) } - -// --- Page 0: Home Page --- -// Function to build the home page of the TUI application. Displays a -// welcome banner, instructions, and buttons to start or quit the application. +// --- Page 0: Home Page --- +// +// Function to build the home page of the TUI application. Displays a +// welcome banner, instructions, and buttons to start or quit the application. func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { bannerText := "[#6EBE49::b]░█▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + - "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + - "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + - "[white::b]Welcome to the CloudFuse Configuration Tool\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + - "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" - - // Banner text widget + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + + "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + + "[white::b]Welcome to the CloudFuse Configuration Tool\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + + "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" + + // Banner text widget bannerTextWidget := tview.NewTextView(). SetText(centerText(bannerText, 75)). SetTextAlign(tuiAlignment). SetDynamicColors(true). SetWrap(true) - + instructionsText := "[#FFD700::b]Instructions:[::-]\n" + - "[#6EBE49::b]•[-::-] [::]Use your mouse or arrow keys to navigate.[-::-]\n" + - "[#6EBE49::b]•[-::-] [::]Press Enter or left-click to select items.[-::-]\n" + - "[#6EBE49::b]•[-::-] [::]For the best experience, expand terminal window to full size.[-::-]\n" + "[#6EBE49::b]•[-::-] [::]Use your mouse or arrow keys to navigate.[-::-]\n" + + "[#6EBE49::b]•[-::-] [::]Press Enter or left-click to select items.[-::-]\n" + + "[#6EBE49::b]•[-::-] [::]For the best experience, expand terminal window to full size.[-::-]\n" // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -217,11 +214,11 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { SetButtonsAlign(navigationButtonAlignment) aboutText := "[#FFD700::b]ABOUT[-::-]\n" + - "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + - "[grey::i]CloudFuse TUI Configuration Tool\n" + - "Seagate Technology, LLC\n" + - "cloudfuse@seagate.com\n" + - fmt.Sprintf("Version: %s", tuiVersion) + "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + + "[grey::i]CloudFuse TUI Configuration Tool\n" + + "Seagate Technology, LLC\n" + + "cloudfuse@seagate.com\n" + + fmt.Sprintf("Version: %s", tuiVersion) // About text widget aboutTextWidget := tview.NewTextView(). @@ -229,34 +226,34 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { SetDynamicColors(true). SetTextAlign(tuiAlignment). SetWrap(true) - + // Assemble page layout layout := tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(bannerTextWidget, getTextHeight(bannerText), 0, false). // Banner Widget - AddItem(nil, 1, 0, false). // Padding - AddItem(startQuitButtonsWidget, 3, 0, false). // Start/Quit buttons widget - AddItem(nil, 1, 0, false). // Padding - AddItem(instructionsTextWidget, 4, 0, false). // Instructions widget - AddItem(nil, 2, 0, false). // Padding - AddItem(aboutTextWidget, 9, 0, false). // About widget - AddItem(nil, 1, 0, false) // Bottom padding + AddItem(nil, 1, 0, false). // Padding + AddItem(startQuitButtonsWidget, 3, 0, false). // Start/Quit buttons widget + AddItem(nil, 1, 0, false). // Padding + AddItem(instructionsTextWidget, 4, 0, false). // Instructions widget + AddItem(nil, 2, 0, false). // Padding + AddItem(aboutTextWidget, 9, 0, false). // About widget + AddItem(nil, 1, 0, false) // Bottom padding layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) return layout } - -// --- Page 1: Storage Provider Selection --- -// Function to build the storage provider selection page. Allows users to select their cloud storage provider +// --- Page 1: Storage Provider Selection --- +// +// Function to build the storage provider selection page. Allows users to select their cloud storage provider // from a dropdown list. The options are: LyveCloud, Microsoft, AWS, and Other S3. func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + - "[grey::i] If your provider is not listed, choose [darkmagenta::b]Other (s3)[-::-][grey::i]. You’ll be\n" + - " prompted to enter the endpoint URL and region manually.[-::-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + + "[grey::i] If your provider is not listed, choose [darkmagenta::b]Other (s3)[-::-][grey::i]. You’ll be\n" + + " prompted to enter the endpoint URL and region manually.[-::-]\n" // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -286,7 +283,7 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. endpointURL = "" default: storageProtocol = "s3storage" - storageProvider = "LyveCloud" + storageProvider = "LyveCloud" } }). SetCurrentOption(0). @@ -339,8 +336,8 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. return layout } - -// --- Page 2: Endpoint and Region Page --- +// --- Page 2: Endpoint and Region Page --- +// // Function to build the endpoint URL page. Allows users to enter the endpoint URL for their cloud storage provider. // It validates the endpoint URL format and provides help text based on the selected provider. func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Primitive { @@ -350,21 +347,21 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim switch storageProvider { case "LyveCloud": urlRegionHelpText = "[::b]For LyveCloud, the endpoint URL format is generally:[-]\n" + - "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + - "Example:\n[darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + - "[grey::i]Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown.[-::-]" + "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + + "Example:\n[darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + + "[grey::i]Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown.[-::-]" urlRegionHelpText = centerText(urlRegionHelpText, 65) case "Other": urlRegionHelpText = "[::b]You selected a custom s3 provider.[::-]\n\n" + - "Enter the endpoint URL.\n" + - "[grey::i]Refer to your provider’s documentation for valid formats.[-::-]" + "Enter the endpoint URL.\n" + + "[grey::i]Refer to your provider’s documentation for valid formats.[-::-]" urlRegionHelpText = centerText(urlRegionHelpText, 65) } - instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" + - "[white]\n %s", storageProvider, urlRegionHelpText) + instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n"+ + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ + "[white]\n %s", storageProvider, urlRegionHelpText) instructionsTextWidget := tview.NewTextView(). SetText(instructionsText). @@ -392,15 +389,20 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim }). AddButton(navigationNextLabel, func() { if err := validateEndpointURL(endpointURL); err != nil { - showErrorModal(app, pages, fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), func() { - pages.RemovePage("page2") - page2 := buildEndpointURLPage(app, pages) - pages.AddPage("page2", page2, true, false) - pages.SwitchToPage("page2") - }) + showErrorModal( + app, + pages, + fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), + func() { + pages.RemovePage("page2") + page2 := buildEndpointURLPage(app, pages) + pages.AddPage("page2", page2, true, false) + pages.SwitchToPage("page2") + }, + ) return } - pages.RemovePage("page3") + pages.RemovePage("page3") page3 := buildCredentialsPage(app, pages) pages.AddPage("page3", page3, true, false) pages.SwitchToPage("page3") @@ -435,8 +437,8 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim return layout } - -// --- Page 3: Credentials Page --- +// --- Page 3: Credentials Page --- +// // Function to build the credentials page. Allows users to enter their cloud storage credentials. // If the storage protocol is "s3", it provides input fields for access key, secret key. // If the storage protocol is "azure", it provides input fields for account name, account key, and container name. @@ -455,14 +457,14 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim secretLabel = "🔑 Secret Key: " } - instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Your Cloud Storage Credentials[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n" + - "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your unique identifier for accessing your cloud storage.\n" + - "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your secret password for accessing your cloud storage.\n", - strings.Trim(accessLabel, "🔑 "), strings.Trim(secretLabel, "🔑 ")) + instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Your Cloud Storage Credentials[-]\n"+ + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n"+ + "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your unique identifier for accessing your cloud storage.\n"+ + "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your secret password for accessing your cloud storage.\n", + strings.Trim(accessLabel, "🔑 "), strings.Trim(secretLabel, "🔑 ")) if storageProtocol == "azstorage" { - instructionsText += "[#FFD700::b] -[-::-] [#FFD700::b]Container Name:[-::-] This is the name of your Azure Blob Storage container.\n" + instructionsText += "[#FFD700::b] -[-::-] [#FFD700::b]Container Name:[-::-] This is the name of your Azure Blob Storage container.\n" } instructionsText += "\n[darkmagenta::i]\t\t\t*Keep these credentials secure. Do not share.[-]" @@ -477,7 +479,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim // Access key field widget accessKeyFieldWidget := tview.NewInputField(). SetLabel(accessLabel). - SetText(accessKey). + SetText(accessKey). SetFieldWidth(50). SetChangedFunc(func(key string) { accessKey = key @@ -491,7 +493,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim // Secret key field widget with masked input secretKeyFieldWidget := tview.NewInputField(). SetLabel(secretLabel). - SetText(string(secretKey)). + SetText(string(secretKey)). SetFieldWidth(50). SetChangedFunc(func(key string) { secretKey = key @@ -502,7 +504,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim SetLabelColor(widgetLabelColor). SetFieldBackgroundColor(widgetFieldBackgroundColor). SetFieldTextColor(tcell.ColorBlack) - + // Container name field widget for Azure storage containerNameFieldWidget := tview.NewInputField(). SetLabel("🪣 Container Name: "). @@ -522,21 +524,27 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim pages.SwitchToPage("home") }). AddButton(navigationNextLabel, func() { - // TODO: Add validation for access key and secret key HERE + // TODO: Add validation for access key and secret key HERE // For now, just check that they are not empty - if (storageProtocol == "s3storage" && (len(accessKey) == 0 || len(secretKey) == 0)) || (storageProtocol == "azstorage" && (len(accountName) == 0 || len(accountKey) == 0 || len(containerName) == 0)) { - showErrorModal(app, pages, "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", func() { - pages.SwitchToPage("page3") - }) + if (storageProtocol == "s3storage" && (len(accessKey) == 0 || len(secretKey) == 0)) || + (storageProtocol == "azstorage" && (len(accountName) == 0 || len(accountKey) == 0 || len(containerName) == 0)) { + showErrorModal( + app, + pages, + "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", + func() { + pages.SwitchToPage("page3") + }, + ) return } // TODO: Fix bug here where calling listBuckets() in the checkCredentials() function // causes the layout to shift upwards and the widgets to be misaligned if the user incorrectly // enters credentials. if err := checkCredentials(app, pages); err != nil { - showErrorModal(app, pages, fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func (){ - pages.RemovePage("page3") // Remove the current page - page3 := buildCredentialsPage(app, pages) // Rebuild the page + showErrorModal(app, pages, fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { + pages.RemovePage("page3") // Remove the current page + page3 := buildCredentialsPage(app, pages) // Rebuild the page pages.AddPage("page3", page3, true, false) // Add the new page pages.SwitchToPage("page3") }) @@ -554,12 +562,12 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim }). AddButton(navigationBackLabel, func() { if storageProvider == "Microsoft" || storageProvider == "AWS" { - pages.RemovePage("page2") + pages.RemovePage("page2") pages.SwitchToPage("page1") } else { - page2 := buildEndpointURLPage(app, pages) - pages.AddPage("page2", page2, true, false) - pages.SwitchToPage("page2") + page2 := buildEndpointURLPage(app, pages) + pages.AddPage("page2", page2, true, false) + pages.SwitchToPage("page2") } }). AddButton(navigationPreviewLabel, func() { @@ -574,7 +582,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim SetButtonBackgroundColor(navigationButtonColor). SetButtonTextColor(tcell.ColorBlack). SetButtonsAlign(navigationButtonAlignment) - + // Combine all credential widgets into a single form credentialsWidget := tview.NewForm(). AddFormItem(accessKeyFieldWidget). @@ -582,7 +590,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim SetFieldTextColor(tcell.ColorBlack). SetLabelColor(widgetLabelColor). SetFieldBackgroundColor(widgetFieldBackgroundColor) - + // If Azure is selected, add the container name field if storageProvider == "Microsoft" { credentialsWidget.AddFormItem(containerNameFieldWidget) @@ -600,16 +608,16 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim return layout } - -// --- Page 4: Bucket Name Selection --- +// --- Page 4: Bucket Name Selection --- +// // Function to build the bucket selection page. Allows users to select a bucket from a dropdown list // of retrieved buckets based on provided s3 credentials. For s3 storage users only. Azure storage users will skip this page. func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + - "[grey::i] The list of available buckets is retrieved from your cloud storage provider\n " + - "based on the credentials provided in the previous step.[-::-]" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + + "[grey::i] The list of available buckets is retrieved from your cloud storage provider\n " + + "based on the credentials provided in the previous step.[-::-]" // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -667,8 +675,8 @@ func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview. return layout } - -// --- Page 5: Caching Settings --- +// --- Page 5: Caching Settings --- +// // Function to build the caching page that allows users to configure caching settings. // Includes options for enabling/disabling caching, specifying cache location, size, and retention settings. func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { @@ -676,10 +684,10 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv layout := tview.NewFlex().SetDirection(tview.FlexRow) instructionsText := "[#6EBE49::b] Configure Caching Settings[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + - "[white::b] CloudFuse can cache data locally. You control the location, size, and duration.[-::-]\n\n" + - "[#FFD700::b] -[-::-] [#6EBE49::b]Enable[-::-] caching if you frequently re-read data and have ample disk space.\n" + - "[#FFD700::b] -[-::-] [red::b]Disable[-::-] caching if you prefer faster initial access or have limited disk space.\n\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + + "[white::b] CloudFuse can cache data locally. You control the location, size, and duration.[-::-]\n\n" + + "[#FFD700::b] -[-::-] [#6EBE49::b]Enable[-::-] caching if you frequently re-read data and have ample disk space.\n" + + "[#FFD700::b] -[-::-] [red::b]Disable[-::-] caching if you prefer faster initial access or have limited disk space.\n\n" // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -700,23 +708,28 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv cacheLocation = text }) - // Input field widget for cache size percentage - cacheSizeFieldWidget := tview.NewInputField(). - SetLabel("📊 Cache Size (%): "). - SetText(cacheSize). // Default to 80% - SetFieldWidth(4). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). + // Input field widget for cache size percentage + cacheSizeFieldWidget := tview.NewInputField(). + SetLabel("📊 Cache Size (%): "). + SetText(cacheSize). // Default to 80% + SetFieldWidth(4). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). SetFieldTextColor(tcell.ColorBlack). - SetChangedFunc(func(text string) { - if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { - showErrorModal(app, pages, "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", func() { - pages.SwitchToPage("page5") - }) - return - } - cacheSize = text - }) + SetChangedFunc(func(text string) { + if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { + showErrorModal( + app, + pages, + "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", + func() { + pages.SwitchToPage("page5") + }, + ) + return + } + cacheSize = text + }) // Input field widget for cache retention duration cacheRetentionDurationFieldWidget := tview.NewInputField(). @@ -727,8 +740,8 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv if val, err := strconv.Atoi(text); err == nil { cacheRetentionDuration = val } else { - // TODO: Handle invalid input - cacheRetentionDuration = 0 + // TODO: Handle invalid input + cacheRetentionDuration = 0 } }). SetLabelColor(widgetLabelColor). @@ -741,17 +754,17 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv cacheRetentionUnit = option // Convert cache retention duration to seconds switch cacheRetentionUnit { - case "Seconds": - cacheRetentionDurationSec = cacheRetentionDuration - case "Minutes": - minutes := cacheRetentionDuration - cacheRetentionDurationSec = minutes * 60 - case "Hours": - hours := cacheRetentionDuration - cacheRetentionDurationSec = hours * 3600 - case "Days": - days := cacheRetentionDuration - cacheRetentionDurationSec = days * 86400 + case "Seconds": + cacheRetentionDurationSec = cacheRetentionDuration + case "Minutes": + minutes := cacheRetentionDuration + cacheRetentionDurationSec = minutes * 60 + case "Hours": + hours := cacheRetentionDuration + cacheRetentionDurationSec = hours * 3600 + case "Days": + days := cacheRetentionDuration + cacheRetentionDurationSec = days * 86400 } }). SetCurrentOption(3). // Default to Days @@ -759,20 +772,20 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv SetFieldBackgroundColor(widgetFieldBackgroundColor). SetFieldTextColor(tcell.ColorBlack) - // Dropdown widget for enabling/disabling cache cleanup on restart - // If enabled --> allow-non-empty-temp: false - // if disabled --> allow-non-empty-temp: true - clearCacheOnStartDropdownWidget := tview.NewDropDown(). - SetLabel("🧹 Clear Cache On Start: "). - SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { - if text == " Enabled " { + // Dropdown widget for enabling/disabling cache cleanup on restart + // If enabled --> allow-non-empty-temp: false + // if disabled --> allow-non-empty-temp: true + clearCacheOnStartDropdownWidget := tview.NewDropDown(). + SetLabel("🧹 Clear Cache On Start: "). + SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { + if text == " Enabled " { clearCacheOnStart = true - } else { + } else { clearCacheOnStart = false - } - }). - SetCurrentOption(0). - SetLabelColor(widgetLabelColor). + } + }). + SetCurrentOption(0). + SetLabelColor(widgetLabelColor). SetFieldBackgroundColor(widgetFieldBackgroundColor). SetFieldTextColor(tcell.ColorBlack) @@ -782,24 +795,24 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv AddItem(cacheRetentionDurationFieldWidget, 35, 0, false). AddItem(cacheRetentionUnitDropdownWidget, 7, 0, false) - // Group cache field widgets in a container - cacheFields := tview.NewFlex(). + // Group cache field widgets in a container + cacheFields := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(cacheLocationFieldWidget, 2, 0, false). - AddItem(cacheSizeFieldWidget, 2, 0, false). + AddItem(cacheLocationFieldWidget, 2, 0, false). + AddItem(cacheSizeFieldWidget, 2, 0, false). AddItem(cacheRetentionRow, 2, 0, false). AddItem(clearCacheOnStartDropdownWidget, 2, 0, false) - // Tracks whether or not cache fields are currently shown - showCacheFields := true + // Tracks whether or not cache fields are currently shown + showCacheFields := true - // Navigation buttons widget - navigationButtonsWidget := tview.NewForm() + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm() navigationButtonsWidget. - AddButton(navigationHomeLabel, func() { - pages.SwitchToPage("home") - }). - AddButton(navigationFinishLabel, func() { + AddButton(navigationHomeLabel, func() { + pages.SwitchToPage("home") + }). + AddButton(navigationFinishLabel, func() { // Check if caching is enabled and validate cache settings if enableCaching { // Validate the cache location @@ -812,24 +825,41 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv // Check available cache size if err := getAvailableCacheSize(); err != nil { - showErrorModal(app, pages, "Failed to check available cache size:\n"+err.Error(), func() { - pages.SwitchToPage("page5") - }) + showErrorModal( + app, + pages, + "Failed to check available cache size:\n"+err.Error(), + func() { + pages.SwitchToPage("page5") + }, + ) return } - cacheSizeText := fmt.Sprintf("Available Disk Space @ Cache Location: [darkred::b]%d GB[-::-]\n", availableCacheSizeGB) + - fmt.Sprintf("Cache Size Currently Set to: [darkred::b]%.0f GB (%s%%)[-::-]\n\n", float64(currentCacheSizeGB), cacheSize) + - "Would you like to proceed with this cache size?\n\n"+ - "If not, hit [darkred::b]Return[-::-] to adjust cache size accordingly. Otherwise, hit [darkred::b]Finish[-::-] to complete the configuration." + cacheSizeText := fmt.Sprintf( + "Available Disk Space @ Cache Location: [darkred::b]%d GB[-::-]\n", + availableCacheSizeGB, + ) + + fmt.Sprintf( + "Cache Size Currently Set to: [darkred::b]%.0f GB (%s%%)[-::-]\n\n", + float64(currentCacheSizeGB), + cacheSize, + ) + + "Would you like to proceed with this cache size?\n\n" + + "If not, hit [darkred::b]Return[-::-] to adjust cache size accordingly. Otherwise, hit [darkred::b]Finish[-::-] to complete the configuration." showCacheConfirmationModal(app, pages, cacheSizeText, // Callback function if the user selects Finish func() { if err := createYAMLConfig(); err != nil { - showErrorModal(app, pages, "Failed to create YAML config:\n"+err.Error(), func() { - pages.SwitchToPage("page5") - }) + showErrorModal( + app, + pages, + "Failed to create YAML config:\n"+err.Error(), + func() { + pages.SwitchToPage("page5") + }, + ) return } showExitModal(app, pages, func() { @@ -853,94 +883,105 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv app.Stop() }) } - }). - AddButton(navigationBackLabel, func() { - if storageProtocol == "azstorage" { - pages.SwitchToPage("page3") - } else { - page4 := buildBucketSelectionPage(app, pages) - pages.AddPage("page4", page4, true, false) - pages.SwitchToPage("page4") - } - }). - AddButton(navigationPreviewLabel, func() { - previewPage := buildPreviewPage(app, pages, "page5") - pages.AddPage("previewPage", previewPage, true, false) - pages.SwitchToPage("previewPage") - }). - AddButton(navigationQuitLabel, func() { - app.Stop() - }). - SetButtonBackgroundColor(navigationButtonColor). - SetButtonTextColor(tcell.ColorBlack). - SetButtonsAlign(tuiAlignment) - - // Widget to enable/disable caching - enableCachingDropdownWidget := tview.NewDropDown() + }). + AddButton(navigationBackLabel, func() { + if storageProtocol == "azstorage" { + pages.SwitchToPage("page3") + } else { + page4 := buildBucketSelectionPage(app, pages) + pages.AddPage("page4", page4, true, false) + pages.SwitchToPage("page4") + } + }). + AddButton(navigationPreviewLabel, func() { + previewPage := buildPreviewPage(app, pages, "page5") + pages.AddPage("previewPage", previewPage, true, false) + pages.SwitchToPage("previewPage") + }). + AddButton(navigationQuitLabel, func() { + app.Stop() + }). + SetButtonBackgroundColor(navigationButtonColor). + SetButtonTextColor(tcell.ColorBlack). + SetButtonsAlign(tuiAlignment) + + // Widget to enable/disable caching + enableCachingDropdownWidget := tview.NewDropDown() enableCachingDropdownWidget. - SetLabel("💾 Caching: "). - SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { - if text == " Enabled " { + SetLabel("💾 Caching: "). + SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { + if text == " Enabled " { cacheMode = "file_cache" enableCaching = true - if !showCacheFields { + if !showCacheFields { layout.RemoveItem(navigationButtonsWidget) layout.RemoveItem(cacheFields) layout.AddItem(cacheFields, 8, 0, false) layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) - showCacheFields = true - } - } else { + showCacheFields = true + } + } else { cacheMode = "block_cache" enableCaching = false - if showCacheFields { - layout.RemoveItem(cacheFields) - showCacheFields = false - } - } - }). - SetCurrentOption(0). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + if showCacheFields { + layout.RemoveItem(cacheFields) + showCacheFields = false + } + } + }). + SetCurrentOption(0). + SetLabelColor(widgetLabelColor). + SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) - // Assemble page layout - layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) - layout.AddItem(enableCachingDropdownWidget, 2, 0, false) + // Assemble page layout + layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) + layout.AddItem(enableCachingDropdownWidget, 2, 0, false) - if showCacheFields { - layout.AddItem(cacheFields, 8, 0, false) - } + if showCacheFields { + layout.AddItem(cacheFields, 8, 0, false) + } - layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) - layout.AddItem(nil, 1, 0, false) + layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) + layout.AddItem(nil, 1, 0, false) layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) - return layout + return layout } - -// --- Summary Page --- +// --- Summary Page --- +// // Function to build the summary page that displays the configuration summary. // This function creates a text view with the summary information and a return button. // The preview page parameter allows switching back to the previous page when the user clicks "Return". -func buildPreviewPage(app *tview.Application, pages *tview.Pages, previewPage string) tview.Primitive { - summaryText := - "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n"+ +func buildPreviewPage( + app *tview.Application, + pages *tview.Pages, + previewPage string, +) tview.Primitive { + summaryText := + "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]" + - fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", storageProvider)+ - fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", endpointURL)+ - fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", bucketName)+ - fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", cacheMode)+ - fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", cacheLocation)+ - fmt.Sprintf(" Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", cacheSize, currentCacheSizeGB) + fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", storageProvider) + + fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", endpointURL) + + fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", bucketName) + + fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", cacheMode) + + fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", cacheLocation) + + fmt.Sprintf( + " Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", + cacheSize, + currentCacheSizeGB, + ) // Display cache retention duration in seconds and specified unit if cacheRetentionUnit == "Seconds" { - summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", cacheRetentionDurationSec) + summaryText += fmt.Sprintf( + " Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", + cacheRetentionDurationSec, + ) } else { - summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", - cacheRetentionDurationSec, cacheRetentionDuration, cacheRetentionUnit) + summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", + cacheRetentionDurationSec, cacheRetentionDuration, cacheRetentionUnit) } // Set a dynamic width and height for the summary widget @@ -955,9 +996,9 @@ func buildPreviewPage(app *tview.Application, pages *tview.Pages, previewPage st SetScrollable(true) returnButton := tview.NewButton("[black]Return[-]"). - SetSelectedFunc(func() { - pages.SwitchToPage(previewPage) - }) + SetSelectedFunc(func() { + pages.SwitchToPage(previewPage) + }) returnButton.SetBackgroundColor(greenColor) returnButton.SetBorder(true) returnButton.SetBorderColor(yellowColor) @@ -965,25 +1006,24 @@ func buildPreviewPage(app *tview.Application, pages *tview.Pages, previewPage st buttons := tview.NewFlex(). SetDirection(tview.FlexColumn). - AddItem(nil, 0, 1, false). // Left button spacer + AddItem(nil, 0, 1, false). // Left button spacer AddItem(returnButton, 20, 0, true). - AddItem(nil, 0, 1, false) // Right button spacer + AddItem(nil, 0, 1, false) // Right button spacer modal := tview.NewFlex(). - SetDirection(tview.FlexRow). + SetDirection(tview.FlexRow). AddItem(summaryWidget, summaryWidgetHeight, 0, false). - AddItem(nil, 1, 0, false). + AddItem(nil, 1, 0, false). AddItem(buttons, 3, 0, true) leftAlignedModal := tview.NewFlex(). - AddItem(modal, summaryWidgetWidth, 0, true) + AddItem(modal, summaryWidgetWidth, 0, true) leftAlignedModal.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) return leftAlignedModal } - // Function to show a modal dialog with a message and an "OK" button. // This function is used to display error messages or confirmations. // May specify a callback function to execute when the modal is closed. @@ -1004,10 +1044,15 @@ func showErrorModal(app *tview.Application, pages *tview.Pages, message string, pages.AddPage("modal", modal, false, true) } - // Function to show a confirmation modal dialog with "Finish" and "Return" buttons. // Used to confirm cache size before proceeding. Must specify two callback functions for the "Finish" and "Return" actions. -func showCacheConfirmationModal(app *tview.Application, pages *tview.Pages, message string, onFinish func(), onReturn func()) { +func showCacheConfirmationModal( + app *tview.Application, + pages *tview.Pages, + message string, + onFinish func(), + onReturn func(), +) { modal := tview.NewModal(). SetText(message). AddButtons([]string{"Finish", "Return"}). @@ -1028,7 +1073,6 @@ func showCacheConfirmationModal(app *tview.Application, pages *tview.Pages, mess pages.AddPage("modal", modal, true, true) } - // Function to show final exit modal when configuration is complete. // Informs the user that the configuration is complete and they can exit. // This function is called when the user clicks "Finish" on the caching page. @@ -1036,23 +1080,23 @@ func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) processingEmojis := []string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "✅"} - modal := tview.NewModal(). - AddButtons([]string{"Exit"}). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - pages.RemovePage("modal") - if buttonLabel == "Exit" { - onConfirm() - } - }). - SetBackgroundColor(greenColor). - SetTextColor(tcell.ColorBlack) - modal.SetBorder(true) - modal.SetBorderColor(yellowColor) - modal.SetButtonBackgroundColor(yellowColor) - modal.SetButtonTextColor(tcell.ColorBlack) - - pages.AddPage("modal", modal, true, true) - + modal := tview.NewModal(). + AddButtons([]string{"Exit"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + pages.RemovePage("modal") + if buttonLabel == "Exit" { + onConfirm() + } + }). + SetBackgroundColor(greenColor). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(yellowColor) + modal.SetButtonBackgroundColor(yellowColor) + modal.SetButtonTextColor(tcell.ColorBlack) + + pages.AddPage("modal", modal, true, true) + // Simulate processing with emoji animation go func() { // Show initial message with emoji animation @@ -1060,7 +1104,12 @@ func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) currentEmoji := processingEmojis[i] time.Sleep(100 * time.Millisecond) app.QueueUpdateDraw(func() { - modal.SetText(fmt.Sprintf("[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", currentEmoji)) + modal.SetText( + fmt.Sprintf( + "[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", + currentEmoji, + ), + ) }) } @@ -1074,7 +1123,6 @@ func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) }() } - // Helper function to center lines of text within a specified width. // It is used to format text views and other UI elements in the TUI. func centerText(text string, width int) string { @@ -1092,7 +1140,6 @@ func centerText(text string, width int) string { return strings.Join(centeredLines, "\n") } - // Helper function to get the length of the longest line in a string. // It is used to determine the width of text views and other UI elements. func getTextWidth(s string) int { @@ -1109,7 +1156,6 @@ func getTextWidth(s string) int { return longest } - // Helper function to count the number of lines in a string. // It is used to determine the height of text views and other UI elements. func getTextHeight(s string) int { @@ -1119,33 +1165,38 @@ func getTextHeight(s string) int { return len(strings.Split(s, "\n")) } - // Helper function to get a fallback cache path if the home directory cannot be determined. func getFallbackCachePath() string { - user := os.Getenv("USER") - if user == "" { - uid := os.Getuid() - user = fmt.Sprintf("uid_%d", uid) - } - return filepath.Join(os.TempDir(), "cloudfuse", user) + user := os.Getenv("USER") + if user == "" { + uid := os.Getuid() + user = fmt.Sprintf("uid_%d", uid) + } + return filepath.Join(os.TempDir(), "cloudfuse", user) } - // Helper function to get the default cache path. // It retrieves the user's home directory and constructs a default cache path: -// `~/.cloudfuse/file_cache`. If it fails to retrieve the home directory or create the path, it returns a fallback path. +// +// `~/.cloudfuse/file_cache`. If it fails to retrieve the home directory or create the path, it returns a fallback path. func getDefaultCachePath() string { // TODO: Add logic to return OS-specific cache paths home, err := os.UserHomeDir() if err != nil { - fmt.Printf("[red::b]ERROR: Failed to get home directory: %v\nUsing fallback path for cache directory.\n", err) + fmt.Printf( + "[red::b]ERROR: Failed to get home directory: %v\nUsing fallback path for cache directory.\n", + err, + ) return getFallbackCachePath() } cachePath := filepath.Join(home, ".cloudfuse", "file_cache") // If the directory doesn't exist, create it if _, err := os.Stat(cachePath); os.IsNotExist(err) { if err := os.MkdirAll(cachePath, 0700); err != nil { - fmt.Printf("[red::b]ERROR: Failed to create cache directory: %v\nUsing fallback path for cache directory.\n", err) + fmt.Printf( + "[red::b]ERROR: Failed to create cache directory: %v\nUsing fallback path for cache directory.\n", + err, + ) return getFallbackCachePath() } } @@ -1153,13 +1204,12 @@ func getDefaultCachePath() string { return cachePath } - // Helper function to validate the entered cache path. func validateCachePath() error { // Validate that the path is not empty if strings.TrimSpace(cacheLocation) == "" { return fmt.Errorf("[red::b]ERROR: Cache location cannot be empty[-::-]") - } + } // Make sure no invalid path characters are used if strings.ContainsAny(cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { return fmt.Errorf("[red::b]ERROR: Cache location contains invalid characters[-::-]") @@ -1173,22 +1223,25 @@ func validateCachePath() error { return nil } - // Helper function to get the available disk space at the cache location and calculates // the cache size in GB based on the user-defined cache size percentage. -func getAvailableCacheSize() (error) { +func getAvailableCacheSize() error { availableBlocks, _, err := common.GetAvailFree(cacheLocation) if err != nil { // If we fail to get the available cache size, we default to 80% of the available disk space cacheSize = "80" - returnMsg := fmt.Errorf("[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ - "Defaulting cache size to 80%% of available disk space.\n\n"+ - "Please manually verify you have enough disk space available for caching.[-::-]", cacheLocation, err) + returnMsg := fmt.Errorf( + "[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ + "Defaulting cache size to 80%% of available disk space.\n\n"+ + "Please manually verify you have enough disk space available for caching.[-::-]", + cacheLocation, + err, + ) return returnMsg } - const blockSize = 4096 - availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes + const blockSize = 4096 + availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes availableCacheSizeGB = int(availableCacheSizeBytes / (1024 * 1024 * 1024)) // Convert to GB cacheSizeInt, _ := strconv.Atoi(cacheSize) currentCacheSizeGB = int(availableCacheSizeGB) * cacheSizeInt / 100 @@ -1196,9 +1249,8 @@ func getAvailableCacheSize() (error) { return nil } - // Helper function to normalize and validate the user-defined endpoint URL. -func validateEndpointURL(rawURL string) (error) { +func validateEndpointURL(rawURL string) error { rawURL = strings.TrimSpace(rawURL) // Check if the URL is empty @@ -1209,8 +1261,8 @@ func validateEndpointURL(rawURL string) (error) { // Normalize the URL by adding "https://" if it doesn't start with "http://" or "https://" if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { endpointURL = "https://" + rawURL - return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n"+ - "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") + return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n" + + "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") } if _, err := url.ParseRequestURI(rawURL); err != nil { @@ -1220,8 +1272,7 @@ func validateEndpointURL(rawURL string) (error) { return nil } - -// Function to create a temporary YAML configuration file based on user inputs. +// Function to create a temporary YAML configuration file based on user inputs. // Used for testing credentials and then removed after the check. // Called when the user clicks "Next" on the credentials page. func createTmpConfigFile() error { @@ -1240,9 +1291,9 @@ func createTmpConfigFile() error { } } else { config.S3Storage = s3StorageConfig{ - KeyID: accessKey, - SecretKey: secretKey, - Endpoint: endpointURL, + KeyID: accessKey, + SecretKey: secretKey, + Endpoint: endpointURL, EnableDirMarker: true, } } @@ -1262,7 +1313,6 @@ func createTmpConfigFile() error { return nil } - // Function to check the credentials entered by the user. // Attempts to connect to the storage backend and fetch the bucket list. // If successful, populates the global `bucketList` variable with the list of available buckets (for s3 providers only). @@ -1274,9 +1324,9 @@ func checkCredentials(app *tview.Application, pages *tview.Pages) error { } // Delete the temporary config file regardless of success or failure of the credential check - defer func() { - _ = os.Remove("config-tmp.yaml") - }() + defer func() { + _ = os.Remove("config-tmp.yaml") + }() // Parse and unmarshal the temporary config file if err := parseConfig(); err != nil { @@ -1306,7 +1356,6 @@ func checkCredentials(app *tview.Application, pages *tview.Pages) error { return nil } - // Function to create the YAML configuration file based on user inputs once all forms are completed. // Called when the user clicks "Finish" on the caching page. func createYAMLConfig() error { @@ -1319,29 +1368,29 @@ func createYAMLConfig() error { AttrCache: attrCacheConfig{ TimeoutSec: 7200, - }, + }, } if cacheMode == "file_cache" { - config.FileCache = fileCacheConfig{ - Path: cacheLocation, - TimeOutSec: cacheRetentionDurationSec, - AllowNonEmptyTemp: !clearCacheOnStart, - IgnoreSync: true, - } - // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it - if cacheSize != "80" { - config.FileCache.MaxSizeMB = currentCacheSizeGB * 1024 // Convert GB to MB - } - } + config.FileCache = fileCacheConfig{ + Path: cacheLocation, + TimeOutSec: cacheRetentionDurationSec, + AllowNonEmptyTemp: !clearCacheOnStart, + IgnoreSync: true, + } + // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it + if cacheSize != "80" { + config.FileCache.MaxSizeMB = currentCacheSizeGB * 1024 // Convert GB to MB + } + } if storageProtocol == "s3storage" { config.S3Storage = s3StorageConfig{ - BucketName: bucketName, - KeyID: accessKey, - SecretKey: secretKey, - Endpoint: endpointURL, - EnableDirMarker: true, + BucketName: bucketName, + KeyID: accessKey, + SecretKey: secretKey, + Endpoint: endpointURL, + EnableDirMarker: true, } } else { config.AzStorage = azureStorageConfig{ @@ -1351,26 +1400,26 @@ func createYAMLConfig() error { Mode: "key", Container: containerName, } - } + } - // Marshal the struct to YAML (returns []byte and error) - yamlData, err := yaml.Marshal(&config) - if err != nil { + // Marshal the struct to YAML (returns []byte and error) + yamlData, err := yaml.Marshal(&config) + if err != nil { return fmt.Errorf("Failed to marshal YAML: %v", err) - } + } - // Write the YAML to a file - if err := os.WriteFile("config.yaml", yamlData, 0600); err != nil { - return fmt.Errorf("Failed to write YAML to file: %v", err) - } + // Write the YAML to a file + if err := os.WriteFile("config.yaml", yamlData, 0600); err != nil { + return fmt.Errorf("Failed to write YAML to file: %v", err) + } // Update global configFilePath variable currDir, err := os.Getwd() if err != nil { - return fmt.Errorf("Error: %v", err) - } + return fmt.Errorf("Error: %v", err) + } configFilePath = filepath.Join(currDir, "config.yaml") return nil -} \ No newline at end of file +} From 1a8289b7d24981b87cdb646c1a629759a1e8d6d6 Mon Sep 17 00:00:00 2001 From: brayan Date: Wed, 20 Aug 2025 11:00:40 -0600 Subject: [PATCH 09/21] Change to using existing options structs --- cmd/tui.go | 72 ++++++++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 46 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index ba1a542a1..0f3fadefa 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -37,6 +37,10 @@ import ( "github.com/Seagate/cloudfuse/common" "github.com/Seagate/cloudfuse/common/config" + "github.com/Seagate/cloudfuse/component/attr_cache" + "github.com/Seagate/cloudfuse/component/azstorage" + "github.com/Seagate/cloudfuse/component/file_cache" + "github.com/Seagate/cloudfuse/component/libfuse" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "gopkg.in/yaml.v3" @@ -88,28 +92,12 @@ var ( ) type configuration struct { - Components []string `yaml:"components,omitempty"` - Libfuse libfuseConfig `yaml:"libfuse,omitempty"` - FileCache fileCacheConfig `yaml:"file_cache,omitempty"` - AttrCache attrCacheConfig `yaml:"attr_cache,omitempty"` - S3Storage s3StorageConfig `yaml:"s3storage,omitempty"` - AzStorage azureStorageConfig `yaml:"azstorage,omitempty"` -} - -type libfuseConfig struct { - NetworkShare bool `yaml:"network-share"` -} - -type attrCacheConfig struct { - TimeoutSec int `yaml:"timeout-sec"` -} - -type fileCacheConfig struct { - Path string `yaml:"path"` - TimeOutSec int `yaml:"timeout-sec"` - AllowNonEmptyTemp bool `yaml:"allow-non-empty-temp"` - IgnoreSync bool `yaml:"ignore-sync"` - MaxSizeMB int `yaml:"max-size-mb,omitempty"` + Components []string `yaml:"components,omitempty"` + Libfuse libfuse.LibfuseOptions `yaml:"libfuse,omitempty"` + FileCache file_cache.FileCacheOptions `yaml:"file_cache,omitempty"` + AttrCache attr_cache.AttrCacheOptions `yaml:"attr_cache,omitempty"` + S3Storage s3StorageConfig `yaml:"s3storage,omitempty"` + AzStorage azstorage.AzStorageOptions `yaml:"azstorage,omitempty"` } type s3StorageConfig struct { @@ -120,15 +108,6 @@ type s3StorageConfig struct { EnableDirMarker bool `yaml:"enable-dir-marker"` } -type azureStorageConfig struct { - Type string `yaml:"type"` - AccountName string `yaml:"account-name"` - AccountKey string `yaml:"account-key"` - Endpoint string `yaml:"endpoint,omitempty"` - Mode string `yaml:"mode,omitempty"` - Container string `yaml:"container"` -} - // Main function to run the TUI application. // Initializes the tview application, builds the TUI application, and runs it. func runTUI() error { @@ -1282,11 +1261,11 @@ func createTmpConfigFile() error { } if storageProtocol == "azstorage" { - config.AzStorage = azureStorageConfig{ - Type: "block", + config.AzStorage = azstorage.AzStorageOptions{ + AccountType: "block", AccountName: accountName, AccountKey: accountKey, - Mode: "key", + AuthMode: "key", Container: containerName, } } else { @@ -1296,6 +1275,7 @@ func createTmpConfigFile() error { Endpoint: endpointURL, EnableDirMarker: true, } + } yamlData, err := yaml.Marshal(&config) @@ -1362,25 +1342,25 @@ func createYAMLConfig() error { config := configuration{ Components: []string{"libfuse", cacheMode, "attr_cache", storageProtocol}, - Libfuse: libfuseConfig{ + Libfuse: libfuse.LibfuseOptions{ NetworkShare: true, }, - AttrCache: attrCacheConfig{ - TimeoutSec: 7200, + AttrCache: attr_cache.AttrCacheOptions{ + Timeout: uint32(7200), }, } if cacheMode == "file_cache" { - config.FileCache = fileCacheConfig{ - Path: cacheLocation, - TimeOutSec: cacheRetentionDurationSec, - AllowNonEmptyTemp: !clearCacheOnStart, - IgnoreSync: true, + config.FileCache = file_cache.FileCacheOptions{ + TmpPath: cacheLocation, + Timeout: uint32(cacheRetentionDurationSec), + AllowNonEmpty: !clearCacheOnStart, + SyncToFlush: true, } // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it if cacheSize != "80" { - config.FileCache.MaxSizeMB = currentCacheSizeGB * 1024 // Convert GB to MB + config.FileCache.MaxSizeMB = float64(currentCacheSizeGB * 1024) // Convert GB to MB } } @@ -1393,11 +1373,11 @@ func createYAMLConfig() error { EnableDirMarker: true, } } else { - config.AzStorage = azureStorageConfig{ - Type: "block", + config.AzStorage = azstorage.AzStorageOptions{ + AccountType: "block", AccountName: accountName, AccountKey: accountKey, - Mode: "key", + AuthMode: "key", Container: containerName, } } From c4529ddad210840f0af5eb7d6c52941b8be07d65 Mon Sep 17 00:00:00 2001 From: brayan Date: Thu, 21 Aug 2025 09:59:07 -0600 Subject: [PATCH 10/21] Add endpoint URL entry field for AWS s3 provider --- cmd/tui.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index 0f3fadefa..8b7e43b47 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -277,9 +277,8 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. pages.SwitchToPage("home") }). AddButton(navigationNextLabel, func() { - // If Microsoft or AWS is selected, switch to page 3 and skip endpoint/region entry. - // These providers handle endpoint and region internally. - if storageProvider == "Microsoft" || storageProvider == "AWS" { + // If Microsoft is selected, switch to page 3 and skip endpoint entry, handled internally by Azure SDK. + if storageProvider == "Microsoft" { page3 := buildCredentialsPage(app, pages) pages.AddPage("page3", page3, true, false) pages.SwitchToPage("page3") @@ -315,7 +314,7 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. return layout } -// --- Page 2: Endpoint and Region Page --- +// --- Page 2: Endpoint URL Entry Page --- // // Function to build the endpoint URL page. Allows users to enter the endpoint URL for their cloud storage provider. // It validates the endpoint URL format and provides help text based on the selected provider. @@ -325,12 +324,21 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim // Determine URL help text based on selected provider switch storageProvider { case "LyveCloud": - urlRegionHelpText = "[::b]For LyveCloud, the endpoint URL format is generally:[-]\n" + + urlRegionHelpText = "[::b]You selected LyveCloud as your storage provider.[::-]\n\n" + + "For LyveCloud, the endpoint URL format is generally:\n" + "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + "Example:\n[darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + "[grey::i]Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown.[-::-]" urlRegionHelpText = centerText(urlRegionHelpText, 65) + case "AWS": + urlRegionHelpText = "[::b]You selected AWS as your storage provider.[::-]\n\n" + + "The endpoint URL format is generally:\n" + + "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-]\n\n" + + "Example:\n[darkmagenta::b]https://s3.us-east-1.amazonaws.com[-]\n\n" + + "[grey::i]Refer to AWS documentation for valid formats and available regions.[-::-]" + urlRegionHelpText = centerText(urlRegionHelpText, 65) + case "Other": urlRegionHelpText = "[::b]You selected a custom s3 provider.[::-]\n\n" + "Enter the endpoint URL.\n" + @@ -352,8 +360,8 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim SetLabel("🔗 Endpoint URL: "). SetText(endpointURL). SetFieldWidth(50). - SetChangedFunc(func(text string) { - endpointURL = text + SetChangedFunc(func(url string) { + endpointURL = url }). SetPlaceholder("\t\t\t\t"). SetPlaceholderTextColor(tcell.ColorGray). @@ -489,8 +497,9 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim SetLabel("🪣 Container Name: "). SetText(containerName). SetPlaceholder("\t\t\t\t"). - SetChangedFunc(func(text string) { - containerName = text + SetChangedFunc(func(name string) { + containerName = name + bucketName = name }). SetFieldWidth(50). SetLabelColor(widgetLabelColor). @@ -608,8 +617,8 @@ func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview. // Dropdown widget for selecting bucket name bucketSelectionWidget := tview.NewDropDown(). SetLabel(" 🪣 Bucket Name: "). - SetOptions(bucketList, func(text string, index int) { - bucketName = text + SetOptions(bucketList, func(name string, index int) { + bucketName = name }). SetCurrentOption(0). SetLabelColor(widgetLabelColor). @@ -1270,6 +1279,7 @@ func createTmpConfigFile() error { } } else { config.S3Storage = s3StorageConfig{ + BucketName: bucketName, KeyID: accessKey, SecretKey: secretKey, Endpoint: endpointURL, From 2e427c9ba0493df8d482ea6ea82b79f4e038b3e3 Mon Sep 17 00:00:00 2001 From: brayan Date: Fri, 22 Aug 2025 15:12:30 -0600 Subject: [PATCH 11/21] Remove tuiVersion var and align cloudfuse text art in source --- cmd/tui.go | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index 8b7e43b47..e4c5513d9 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -49,28 +49,27 @@ import ( // Constants and global variables used throughout the TUI application. // These include default values, colors, widget configurations, and storage settings. var ( - tuiVersion string = common.CloudfuseVersion // Mirrors the current version of cloudfuse. - configFilePath string // Sets file_cache.path - accountName string // Sets azstorage.account-name - accountKey string // Sets azstorage.account-key - accessKey string // Sets s3storage.key-id - secretKey string // Sets s3storage.secret-key - containerName string // Sets azstorage.container-name - bucketName string // Sets s3storage.bucket-name - endpointURL string // Sets s3storage.endpoint - bucketList = []string{} // Holds list of available buckets retrieved from cloud provider (for s3 only). - storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider - storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. - cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' - enableCaching bool = true // If true, sets cacheMode to file_cache. If false, block_cache - cacheLocation string = getDefaultCachePath() // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache - cacheSize string = "80" // User-defined cache size as % - availableCacheSizeGB int // Total available cache size in GB @ the cache location - currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage - clearCacheOnStart bool = false // If false, sets 'allow-non-empty-temp' to true - cacheRetentionDuration int = 2 // User-defined cache retention duration. Default is '2' - cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' - cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' + configFilePath string // Sets file_cache.path + accountName string // Sets azstorage.account-name + accountKey string // Sets azstorage.account-key + accessKey string // Sets s3storage.key-id + secretKey string // Sets s3storage.secret-key + containerName string // Sets azstorage.container-name + bucketName string // Sets s3storage.bucket-name + endpointURL string // Sets s3storage.endpoint + bucketList = []string{} // Holds list of available buckets retrieved from cloud provider (for s3 only). + storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider + storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. + cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' + enableCaching bool = true // If true, sets cacheMode to file_cache. If false, block_cache + cacheLocation string = getDefaultCachePath() // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache + cacheSize string = "80" // User-defined cache size as % + availableCacheSizeGB int // Total available cache size in GB @ the cache location + currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage + clearCacheOnStart bool = false // If false, sets 'allow-non-empty-temp' to true + cacheRetentionDuration int = 2 // User-defined cache retention duration. Default is '2' + cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' + cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' // Global variables for UI elements tuiAlignment = tview.AlignLeft @@ -153,7 +152,8 @@ func buildTUI(app *tview.Application) { // Function to build the home page of the TUI application. Displays a // welcome banner, instructions, and buttons to start or quit the application. func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { - bannerText := "[#6EBE49::b]░█▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + + bannerText := "[#6EBE49::b]" + + " █▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + "[white::b]Welcome to the CloudFuse Configuration Tool\n" + @@ -197,7 +197,7 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { "[grey::i]CloudFuse TUI Configuration Tool\n" + "Seagate Technology, LLC\n" + "cloudfuse@seagate.com\n" + - fmt.Sprintf("Version: %s", tuiVersion) + fmt.Sprintf("Version: %s", common.CloudfuseVersion) // About text widget aboutTextWidget := tview.NewTextView(). From 1661ce9c6f136b6dbdbd68a910c40f68a3c6855a Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 25 Aug 2025 17:35:28 -0600 Subject: [PATCH 12/21] Refactor code to adopt object-oriented design --- cmd/config.go | 3 +- cmd/tui.go | 946 +++++++++++++++++++++++++++----------------------- 2 files changed, 508 insertions(+), 441 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 18edae469..2cac545e9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -36,7 +36,8 @@ var configCmd = &cobra.Command{ Short: "Launch the interactive configuration tool.", Long: "Starts an interactive terminal-based UI to generate your Cloudfuse configuration file.", RunE: func(cmd *cobra.Command, args []string) error { - if err := runTUI(); err != nil { + ctx := newAppContext() + if err := ctx.runTUI(); err != nil { return fmt.Errorf("Failed to run TUI: %v", err) } return nil diff --git a/cmd/tui.go b/cmd/tui.go index e4c5513d9..6c5c06de8 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -46,51 +46,67 @@ import ( "gopkg.in/yaml.v3" ) -// Constants and global variables used throughout the TUI application. -// These include default values, colors, widget configurations, and storage settings. +// Top-level struct to hold application context, including tview application instance, +// page stack, user configuration data, and UI theme settings. +type appContext struct { + app *tview.Application + pages *tview.Pages + config *userConfig + theme *uiTheme +} + +// Struct to hold user configuration data collected from the TUI session. +type userConfig struct { + configEncryptionPassphrase string // Sets config file encryption passphrase + configFilePath string // Sets file_cache.path + accountName string // Sets azstorage.account-name + accountKey string // Sets azstorage.account-key + accessKey string // Sets s3storage.key-id + secretKey string // Sets s3storage.secret-key + containerName string // Sets azstorage.container-name + bucketName string // Sets s3storage.bucket-name + endpointURL string // Sets s3storage.endpoint + bucketList []string // Holds list of available buckets retrieved from cloud provider (for s3 only). + storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider + storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. + cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' + enableCaching bool // If true, sets cacheMode to file_cache. If false, block_cache + cacheLocation string // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache + cacheSize string // User-defined cache size as % + availableCacheSizeGB int // Total available cache size in GB @ the cache location + currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage + clearCacheOnStart bool // If false, sets 'allow-non-empty-temp' to true + cacheRetentionDuration int // User-defined cache retention duration. Default is '2' + cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' + cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' + theme uiTheme // Holds color and label settings for UI elements +} + +// Struct to hold UI theme settings, including colors and labels for various widgets. +type uiTheme struct { + widgetLabelColor tcell.Color + widgetFieldBackgroundColor tcell.Color + navigationButtonColor tcell.Color + navigationButtonTextColor tcell.Color + navigationStartLabel string + navigationHomeLabel string + navigationNextLabel string + navigationBackLabel string + navigationPreviewLabel string + navigationQuitLabel string + navigationFinishLabel string + navigationWidgetHeight int +} + +// Global general purpose vars var ( - configFilePath string // Sets file_cache.path - accountName string // Sets azstorage.account-name - accountKey string // Sets azstorage.account-key - accessKey string // Sets s3storage.key-id - secretKey string // Sets s3storage.secret-key - containerName string // Sets azstorage.container-name - bucketName string // Sets s3storage.bucket-name - endpointURL string // Sets s3storage.endpoint - bucketList = []string{} // Holds list of available buckets retrieved from cloud provider (for s3 only). - storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider - storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. - cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' - enableCaching bool = true // If true, sets cacheMode to file_cache. If false, block_cache - cacheLocation string = getDefaultCachePath() // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache - cacheSize string = "80" // User-defined cache size as % - availableCacheSizeGB int // Total available cache size in GB @ the cache location - currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage - clearCacheOnStart bool = false // If false, sets 'allow-non-empty-temp' to true - cacheRetentionDuration int = 2 // User-defined cache retention duration. Default is '2' - cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' - cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' - - // Global variables for UI elements - tuiAlignment = tview.AlignLeft - yellowColor tcell.Color = tcell.GetColor("#FFD700") - greenColor tcell.Color = tcell.GetColor("#6EBE49") - widgetLabelColor = yellowColor - widgetFieldBackgroundColor = yellowColor - navigationButtonColor = greenColor - navigationButtonTextColor = tcell.ColorBlack - navigationButtonAlignment = tview.AlignLeft - navigationStartLabel string = "[black]🚀 Start[-]" - navigationHomeLabel string = "[black]🏠 Home[-]" - navigationNextLabel string = "[black]🡲 Next[-]" - navigationBackLabel string = "[black]🡰 Back[-]" - navigationPreviewLabel string = "[black]📄 Preview[-]" - navigationQuitLabel string = "[black]❌ Quit[-]" - navigationFinishLabel string = "[black]✅ Finish[-]" - navigationWidgetHeight int = 3 + colorYellow = tcell.GetColor("#FFD700") + colorGreen = tcell.GetColor("#6EBE49") + colorBlack = tcell.ColorBlack ) -type configuration struct { +// Struct to hold the final configuration data to be written to the YAML config file. +type configOptions struct { Components []string `yaml:"components,omitempty"` Libfuse libfuse.LibfuseOptions `yaml:"libfuse,omitempty"` FileCache file_cache.FileCacheOptions `yaml:"file_cache,omitempty"` @@ -99,6 +115,8 @@ type configuration struct { AzStorage azstorage.AzStorageOptions `yaml:"azstorage,omitempty"` } +// Struct to hold s3 storage configuration options for the YAML config file. +// TODO: change to using s3storage.Options from component/s3storage/config.go type s3StorageConfig struct { BucketName string `yaml:"bucket-name,omitempty"` KeyID string `yaml:"key-id"` @@ -107,17 +125,45 @@ type s3StorageConfig struct { EnableDirMarker bool `yaml:"enable-dir-marker"` } +// Constructor for appContext struct. Initializes default values for userConfig and uiTheme. +func newAppContext() *appContext { + return &appContext{ + app: tview.NewApplication(), + pages: tview.NewPages(), + config: &userConfig{ + enableCaching: true, + cacheLocation: getDefaultCachePath(), + cacheSize: "80", + cacheRetentionDuration: 2, + clearCacheOnStart: false, + }, + theme: &uiTheme{ + widgetLabelColor: colorYellow, + widgetFieldBackgroundColor: colorYellow, + navigationButtonColor: colorGreen, + navigationButtonTextColor: colorBlack, + navigationStartLabel: "[black]🚀 Start[-]", + navigationHomeLabel: "[black]🏠 Home[-]", + navigationNextLabel: "[black]🡲 Next[-]", + navigationBackLabel: "[black]🡰 Back[-]", + navigationPreviewLabel: "[black]📄 Preview[-]", + navigationQuitLabel: "[black]❌ Quit[-]", + navigationFinishLabel: "[black]✅ Finish[-]", + navigationWidgetHeight: 3, + }, + } +} + // Main function to run the TUI application. // Initializes the tview application, builds the TUI application, and runs it. -func runTUI() error { - app := tview.NewApplication() - app.EnableMouse(true) - app.EnablePaste(true) +func (ctx *appContext) runTUI() error { + ctx.app.EnableMouse(true) + ctx.app.EnablePaste(true) - buildTUI(app) + ctx.buildTUI() // Run the application - if err := app.Run(); err != nil { + if err := ctx.app.Run(); err != nil { panic(err) } @@ -125,33 +171,32 @@ func runTUI() error { } // Function to build the TUI application. Initializes the pages and adds them to the page stack. -func buildTUI(app *tview.Application) { - pages := tview.NewPages() +func (ctx *appContext) buildTUI() { // Initialize the pages - homePage := buildHomePage(app, pages) // --- Home Page --- - page1 := buildStorageProviderPage(app, pages) // --- Page 1: Storage Provider Selection --- - page2 := buildEndpointURLPage(app, pages) // --- Page 2: Endpoint URL Entry --- - page3 := buildCredentialsPage(app, pages) // --- Page 3: Credentials Entry --- - page4 := buildBucketSelectionPage(app, pages) // --- Page 4: Bucket Selection --- - page5 := buildCachingPage(app, pages) // --- Page 5: Caching Settings --- + homePage := ctx.buildHomePage() // --- Home Page --- + page1 := ctx.buildStorageProviderPage() // --- Page 1: Storage Provider Selection --- + page2 := ctx.buildEndpointURLPage() // --- Page 2: Endpoint URL Entry --- + page3 := ctx.buildCredentialsPage() // --- Page 3: Credentials Entry --- + page4 := ctx.buildBucketSelectionPage() // --- Page 4: Bucket Selection --- + page5 := ctx.buildCachingPage() // --- Page 5: Caching Settings --- // Add pages to the page stack - pages.AddPage("home", homePage, true, true) - pages.AddPage("page1", page1, true, false) - pages.AddPage("page2", page2, true, false) - pages.AddPage("page3", page3, true, false) - pages.AddPage("page4", page4, true, false) - pages.AddPage("page5", page5, true, false) - - app.SetRoot(pages, true) + ctx.pages.AddPage("home", homePage, true, true) + ctx.pages.AddPage("page1", page1, true, false) + ctx.pages.AddPage("page2", page2, true, false) + ctx.pages.AddPage("page3", page3, true, false) + ctx.pages.AddPage("page4", page4, true, false) + ctx.pages.AddPage("page5", page5, true, false) + + ctx.app.SetRoot(ctx.pages, true) } // --- Page 0: Home Page --- // // Function to build the home page of the TUI application. Displays a // welcome banner, instructions, and buttons to start or quit the application. -func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { +func (ctx *appContext) buildHomePage() tview.Primitive { bannerText := "[#6EBE49::b]" + " █▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + @@ -164,7 +209,6 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { // Banner text widget bannerTextWidget := tview.NewTextView(). SetText(centerText(bannerText, 75)). - SetTextAlign(tuiAlignment). SetDynamicColors(true). SetWrap(true) @@ -177,20 +221,18 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { instructionsTextWidget := tview.NewTextView(). SetText(instructionsText). SetDynamicColors(true). - SetTextAlign(tuiAlignment). SetWrap(true) // Start/Quit buttons widget startQuitButtonsWidget := tview.NewForm(). - AddButton(navigationStartLabel, func() { - pages.SwitchToPage("page1") + AddButton(ctx.theme.navigationStartLabel, func() { + ctx.pages.SwitchToPage("page1") }). - AddButton(navigationQuitLabel, func() { - app.Stop() + AddButton(ctx.theme.navigationQuitLabel, func() { + ctx.app.Stop() }). - SetButtonBackgroundColor(navigationButtonColor). - SetButtonTextColor(navigationButtonTextColor). - SetButtonsAlign(navigationButtonAlignment) + SetButtonBackgroundColor(ctx.theme.navigationButtonColor). + SetButtonTextColor(ctx.theme.navigationButtonTextColor) aboutText := "[#FFD700::b]ABOUT[-::-]\n" + "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + @@ -203,7 +245,6 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { aboutTextWidget := tview.NewTextView(). SetText(centerText(aboutText, 75)). SetDynamicColors(true). - SetTextAlign(tuiAlignment). SetWrap(true) // Assemble page layout @@ -218,7 +259,7 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { AddItem(aboutTextWidget, 9, 0, false). // About widget AddItem(nil, 1, 0, false) // Bottom padding - layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return layout } @@ -227,7 +268,7 @@ func buildHomePage(app *tview.Application, pages *tview.Pages) tview.Primitive { // // Function to build the storage provider selection page. Allows users to select their cloud storage provider // from a dropdown list. The options are: LyveCloud, Microsoft, AWS, and Other S3. -func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +func (ctx *appContext) buildStorageProviderPage() tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + @@ -237,7 +278,6 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. // Instructions text widget instructionsTextWidget := tview.NewTextView(). SetText(instructionsText). - SetTextAlign(tuiAlignment). SetDynamicColors(true). SetWrap(true) @@ -245,60 +285,59 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. storageProviderDropdownWidget := tview.NewDropDown(). SetLabel("📦 Storage Provider: "). SetOptions([]string{" LyveCloud ⬇️", " Microsoft ", " AWS ", " Other (s3) "}, func(option string, index int) { - storageProvider = option + ctx.config.storageProvider = option switch option { case " LyveCloud ⬇️": - storageProtocol = "s3storage" - storageProvider = "LyveCloud" + ctx.config.storageProtocol = "s3storage" + ctx.config.storageProvider = "LyveCloud" case " Microsoft ": - storageProtocol = "azstorage" - storageProvider = "Microsoft" + ctx.config.storageProtocol = "azstorage" + ctx.config.storageProvider = "Microsoft" case " AWS ": - storageProtocol = "s3storage" - storageProvider = "AWS" + ctx.config.storageProtocol = "s3storage" + ctx.config.storageProvider = "AWS" case " Other (s3) ": - storageProtocol = "s3storage" - storageProvider = "Other" - endpointURL = "" + ctx.config.storageProtocol = "s3storage" + ctx.config.storageProvider = "Other" + ctx.config.endpointURL = "" default: - storageProtocol = "s3storage" - storageProvider = "LyveCloud" + ctx.config.storageProtocol = "s3storage" + ctx.config.storageProvider = "LyveCloud" } }). SetCurrentOption(0). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack). + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). SetFieldWidth(14) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(navigationHomeLabel, func() { - pages.SwitchToPage("home") + AddButton(ctx.theme.navigationHomeLabel, func() { + ctx.pages.SwitchToPage("home") }). - AddButton(navigationNextLabel, func() { + AddButton(ctx.theme.navigationNextLabel, func() { // If Microsoft is selected, switch to page 3 and skip endpoint entry, handled internally by Azure SDK. - if storageProvider == "Microsoft" { - page3 := buildCredentialsPage(app, pages) - pages.AddPage("page3", page3, true, false) - pages.SwitchToPage("page3") + if ctx.config.storageProvider == "Microsoft" { + page3 := ctx.buildCredentialsPage() + ctx.pages.AddPage("page3", page3, true, false) + ctx.pages.SwitchToPage("page3") } else { - page2 := buildEndpointURLPage(app, pages) - pages.AddPage("page2", page2, true, false) - pages.SwitchToPage("page2") + page2 := ctx.buildEndpointURLPage() + ctx.pages.AddPage("page2", page2, true, false) + ctx.pages.SwitchToPage("page2") } }). - AddButton(navigationPreviewLabel, func() { - previewPage := buildPreviewPage(app, pages, "page1") - pages.AddPage("previewPage", previewPage, true, false) - pages.SwitchToPage("previewPage") + AddButton(ctx.theme.navigationPreviewLabel, func() { + previewPage := ctx.buildPreviewPage("page1") + ctx.pages.AddPage("previewPage", previewPage, true, false) + ctx.pages.SwitchToPage("previewPage") }). - AddButton(navigationQuitLabel, func() { - app.Stop() + AddButton(ctx.theme.navigationQuitLabel, func() { + ctx.app.Stop() }). - SetButtonBackgroundColor(navigationButtonColor). - SetButtonTextColor(navigationButtonTextColor). - SetButtonsAlign(navigationButtonAlignment) + SetButtonBackgroundColor(ctx.theme.navigationButtonColor). + SetButtonTextColor(ctx.theme.navigationButtonTextColor) // Assemble page layout layout := tview.NewFlex(). @@ -306,10 +345,10 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). AddItem(nil, 1, 0, false). AddItem(storageProviderDropdownWidget, 2, 0, false). - AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false). + AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) - layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return layout } @@ -318,11 +357,11 @@ func buildStorageProviderPage(app *tview.Application, pages *tview.Pages) tview. // // Function to build the endpoint URL page. Allows users to enter the endpoint URL for their cloud storage provider. // It validates the endpoint URL format and provides help text based on the selected provider. -func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +func (ctx *appContext) buildEndpointURLPage() tview.Primitive { var urlRegionHelpText string // Determine URL help text based on selected provider - switch storageProvider { + switch ctx.config.storageProvider { case "LyveCloud": urlRegionHelpText = "[::b]You selected LyveCloud as your storage provider.[::-]\n\n" + "For LyveCloud, the endpoint URL format is generally:\n" + @@ -348,67 +387,63 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n"+ "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ - "[white]\n %s", storageProvider, urlRegionHelpText) + "[white]\n %s", ctx.config.storageProvider, urlRegionHelpText) instructionsTextWidget := tview.NewTextView(). SetText(instructionsText). - SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true) endpointURLFieldWidget := tview.NewInputField(). SetLabel("🔗 Endpoint URL: "). - SetText(endpointURL). + SetText(ctx.config.endpointURL). SetFieldWidth(50). SetChangedFunc(func(url string) { - endpointURL = url + ctx.config.endpointURL = url }). SetPlaceholder("\t\t\t\t"). SetPlaceholderTextColor(tcell.ColorGray). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(navigationHomeLabel, func() { - pages.SwitchToPage("home") + AddButton(ctx.theme.navigationHomeLabel, func() { + ctx.pages.SwitchToPage("home") }). - AddButton(navigationNextLabel, func() { - if err := validateEndpointURL(endpointURL); err != nil { - showErrorModal( - app, - pages, + AddButton(ctx.theme.navigationNextLabel, func() { + if err := ctx.validateEndpointURL(ctx.config.endpointURL); err != nil { + ctx.showErrorModal( fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), func() { - pages.RemovePage("page2") - page2 := buildEndpointURLPage(app, pages) - pages.AddPage("page2", page2, true, false) - pages.SwitchToPage("page2") + ctx.pages.RemovePage("page2") + page2 := ctx.buildEndpointURLPage() + ctx.pages.AddPage("page2", page2, true, false) + ctx.pages.SwitchToPage("page2") }, ) return } - pages.RemovePage("page3") - page3 := buildCredentialsPage(app, pages) - pages.AddPage("page3", page3, true, false) - pages.SwitchToPage("page3") + ctx.pages.RemovePage("page3") + page3 := ctx.buildCredentialsPage() + ctx.pages.AddPage("page3", page3, true, false) + ctx.pages.SwitchToPage("page3") }). - AddButton(navigationBackLabel, func() { - pages.SwitchToPage("page1") + AddButton(ctx.theme.navigationBackLabel, func() { + ctx.pages.SwitchToPage("page1") }). - AddButton(navigationPreviewLabel, func() { - previewPage := buildPreviewPage(app, pages, "page2") - pages.AddPage("previewPage", previewPage, true, false) - pages.SwitchToPage("previewPage") + AddButton(ctx.theme.navigationPreviewLabel, func() { + previewPage := ctx.buildPreviewPage("page2") + ctx.pages.AddPage("previewPage", previewPage, true, false) + ctx.pages.SwitchToPage("previewPage") }). - AddButton(navigationQuitLabel, func() { - app.Stop() + AddButton(ctx.theme.navigationQuitLabel, func() { + ctx.app.Stop() }). - SetButtonBackgroundColor(navigationButtonColor). - SetLabelColor(widgetLabelColor). - SetButtonTextColor(navigationButtonTextColor). - SetButtonsAlign(navigationButtonAlignment) + SetButtonBackgroundColor(ctx.theme.navigationButtonColor). + SetLabelColor(ctx.theme.widgetLabelColor). + SetButtonTextColor(ctx.theme.navigationButtonTextColor) // Assemble page layout layout := tview.NewFlex(). @@ -416,10 +451,10 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). AddItem(nil, 2, 0, false). AddItem(endpointURLFieldWidget, 2, 0, false). - AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false). + AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) - layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return layout } @@ -429,14 +464,14 @@ func buildEndpointURLPage(app *tview.Application, pages *tview.Pages) tview.Prim // Function to build the credentials page. Allows users to enter their cloud storage credentials. // If the storage protocol is "s3", it provides input fields for access key, secret key. // If the storage protocol is "azure", it provides input fields for account name, account key, and container name. -func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +func (ctx *appContext) buildCredentialsPage() tview.Primitive { layout := tview.NewFlex() layout.Clear() // Determine labels for input fields based on storage protocol. accessLabel := "" secretLabel := "" - if storageProtocol == "azstorage" { + if ctx.config.storageProtocol == "azstorage" { accessLabel = "🔑 Account Name: " secretLabel = "🔑 Account Key: " } else { @@ -444,21 +479,40 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim secretLabel = "🔑 Secret Key: " } - instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Your Cloud Storage Credentials[-]\n"+ - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n"+ - "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your unique identifier for accessing your cloud storage.\n"+ - "[#FFD700::b] -[-::-] [#FFD700::b]%s[-::-] This is your secret password for accessing your cloud storage.\n", - strings.Trim(accessLabel, "🔑 "), strings.Trim(secretLabel, "🔑 ")) + instructionsText := fmt.Sprintf( + "[%s::b] Enter Your Cloud Storage Credentials[-]\n", + colorGreen, + ) + + fmt.Sprintf( + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n", + colorYellow, + ) + + fmt.Sprintf( + "[%s::b] -%s[-::-] This is your unique identifier for accessing your cloud storage.\n", + colorYellow, + strings.Trim(accessLabel, "🔑 "), + ) + + fmt.Sprintf( + "[%s::b] -%s[-::-] This is your secret password for accessing your cloud storage.\n", + colorYellow, + strings.Trim(secretLabel, "🔑 "), + ) + + fmt.Sprintf( + "[%s::b] -Passphrase:[-::-] This is used to encrypt your configuration file.\n", + colorYellow, + ) - if storageProtocol == "azstorage" { - instructionsText += "[#FFD700::b] -[-::-] [#FFD700::b]Container Name:[-::-] This is the name of your Azure Blob Storage container.\n" + if ctx.config.storageProtocol == "azstorage" { + instructionsText += fmt.Sprintf( + "[%s::b] -Container Name:[-::-] This is the name of your Azure Blob Storage container.\n", + colorYellow, + ) } instructionsText += "\n[darkmagenta::i]\t\t\t*Keep these credentials secure. Do not share.[-]" // Instructions text widget instructionsTextWidget := tview.NewTextView(). - SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true). SetText(instructionsText) @@ -466,62 +520,75 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim // Access key field widget accessKeyFieldWidget := tview.NewInputField(). SetLabel(accessLabel). - SetText(accessKey). + SetText(ctx.config.accessKey). SetFieldWidth(50). SetChangedFunc(func(key string) { - accessKey = key - accountName = key + ctx.config.accessKey = key + ctx.config.accountName = key }). SetPlaceholder("\t\t\t\t"). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) // Secret key field widget with masked input secretKeyFieldWidget := tview.NewInputField(). SetLabel(secretLabel). - SetText(string(secretKey)). + SetText(string(ctx.config.secretKey)). SetFieldWidth(50). SetChangedFunc(func(key string) { - secretKey = key - accountKey = key + ctx.config.secretKey = key + ctx.config.accountKey = key }). SetPlaceholder("\t\t\t\t"). SetMaskCharacter('*'). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) // Container name field widget for Azure storage containerNameFieldWidget := tview.NewInputField(). SetLabel("🪣 Container Name: "). - SetText(containerName). + SetText(ctx.config.containerName). SetPlaceholder("\t\t\t\t"). SetChangedFunc(func(name string) { - containerName = name - bucketName = name + ctx.config.containerName = name + ctx.config.bucketName = name }). SetFieldWidth(50). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Passphrase field widget for config file encryption + passphraseFieldWidget := tview.NewInputField(). + SetLabel("🔒 Passphrase: "). + SetText(ctx.config.configEncryptionPassphrase). + SetFieldWidth(50). + SetChangedFunc(func(passphrase string) { + ctx.config.configEncryptionPassphrase = strings.TrimSpace(passphrase) + }). + SetPlaceholder("\t\t\t "). + SetMaskCharacter('*'). + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(navigationHomeLabel, func() { - pages.SwitchToPage("home") + AddButton(ctx.theme.navigationHomeLabel, func() { + ctx.pages.SwitchToPage("home") }). - AddButton(navigationNextLabel, func() { + AddButton(ctx.theme.navigationNextLabel, func() { // TODO: Add validation for access key and secret key HERE // For now, just check that they are not empty - if (storageProtocol == "s3storage" && (len(accessKey) == 0 || len(secretKey) == 0)) || - (storageProtocol == "azstorage" && (len(accountName) == 0 || len(accountKey) == 0 || len(containerName) == 0)) { - showErrorModal( - app, - pages, + if (ctx.config.storageProtocol == "s3storage" && (len(ctx.config.accessKey) == 0 || len(ctx.config.secretKey) == 0)) || + (ctx.config.storageProtocol == "azstorage" && (len(ctx.config.accountName) == 0 || len(ctx.config.accountKey) == 0 || len(ctx.config.containerName) == 0)) || + len(ctx.config.configEncryptionPassphrase) == 0 { + ctx.showErrorModal( "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", func() { - pages.SwitchToPage("page3") + ctx.pages.SwitchToPage("page3") }, ) return @@ -529,58 +596,58 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim // TODO: Fix bug here where calling listBuckets() in the checkCredentials() function // causes the layout to shift upwards and the widgets to be misaligned if the user incorrectly // enters credentials. - if err := checkCredentials(app, pages); err != nil { - showErrorModal(app, pages, fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { - pages.RemovePage("page3") // Remove the current page - page3 := buildCredentialsPage(app, pages) // Rebuild the page - pages.AddPage("page3", page3, true, false) // Add the new page - pages.SwitchToPage("page3") + if err := ctx.checkCredentials(); err != nil { + ctx.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { + ctx.pages.RemovePage("page3") // Remove the current page + page3 := ctx.buildCredentialsPage() // Rebuild the page + ctx.pages.AddPage("page3", page3, true, false) // Add the new page + ctx.pages.SwitchToPage("page3") }) return } - if storageProtocol == "azstorage" { - pages.RemovePage("page4") // Remove previous page if it exists - pages.SwitchToPage("page5") + if ctx.config.storageProtocol == "azstorage" { + ctx.pages.RemovePage("page4") // Remove previous page if it exists + ctx.pages.SwitchToPage("page5") } else { - page4 := buildBucketSelectionPage(app, pages) - pages.AddPage("page4", page4, true, false) - pages.SwitchToPage("page4") + page4 := ctx.buildBucketSelectionPage() + ctx.pages.AddPage("page4", page4, true, false) + ctx.pages.SwitchToPage("page4") } }). - AddButton(navigationBackLabel, func() { - if storageProvider == "Microsoft" || storageProvider == "AWS" { - pages.RemovePage("page2") - pages.SwitchToPage("page1") + AddButton(ctx.theme.navigationBackLabel, func() { + if ctx.config.storageProvider == "Microsoft" || ctx.config.storageProvider == "AWS" { + ctx.pages.RemovePage("page2") + ctx.pages.SwitchToPage("page1") } else { - page2 := buildEndpointURLPage(app, pages) - pages.AddPage("page2", page2, true, false) - pages.SwitchToPage("page2") + page2 := ctx.buildEndpointURLPage() + ctx.pages.AddPage("page2", page2, true, false) + ctx.pages.SwitchToPage("page2") } }). - AddButton(navigationPreviewLabel, func() { - previewPage := buildPreviewPage(app, pages, "page3") - pages.AddPage("previewPage", previewPage, true, false) - pages.SwitchToPage("previewPage") + AddButton(ctx.theme.navigationPreviewLabel, func() { + previewPage := ctx.buildPreviewPage("page3") + ctx.pages.AddPage("previewPage", previewPage, true, false) + ctx.pages.SwitchToPage("previewPage") }). - AddButton(navigationQuitLabel, func() { - app.Stop() + AddButton(ctx.theme.navigationQuitLabel, func() { + ctx.app.Stop() }). - SetLabelColor(widgetLabelColor). - SetButtonBackgroundColor(navigationButtonColor). - SetButtonTextColor(tcell.ColorBlack). - SetButtonsAlign(navigationButtonAlignment) + SetLabelColor(ctx.theme.widgetLabelColor). + SetButtonBackgroundColor(ctx.theme.navigationButtonColor). + SetButtonTextColor(ctx.theme.navigationButtonTextColor) // Combine all credential widgets into a single form credentialsWidget := tview.NewForm(). AddFormItem(accessKeyFieldWidget). AddFormItem(secretKeyFieldWidget). + AddFormItem(passphraseFieldWidget). SetFieldTextColor(tcell.ColorBlack). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor) + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor) // If Azure is selected, add the container name field - if storageProvider == "Microsoft" { + if ctx.config.storageProvider == "Microsoft" { credentialsWidget.AddFormItem(containerNameFieldWidget) } @@ -589,9 +656,9 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) layout.AddItem(nil, 1, 0, false) layout.AddItem(credentialsWidget, credentialsWidget.GetFormItemCount()*2+1, 0, false) - layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) + layout.AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false) layout.AddItem(nil, 1, 0, false) - layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return layout } @@ -600,7 +667,7 @@ func buildCredentialsPage(app *tview.Application, pages *tview.Pages) tview.Prim // // Function to build the bucket selection page. Allows users to select a bucket from a dropdown list // of retrieved buckets based on provided s3 credentials. For s3 storage users only. Azure storage users will skip this page. -func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +func (ctx *appContext) buildBucketSelectionPage() tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + @@ -609,7 +676,6 @@ func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview. // Instructions text widget instructionsTextWidget := tview.NewTextView(). - SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true). SetText(instructionsText) @@ -617,37 +683,36 @@ func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview. // Dropdown widget for selecting bucket name bucketSelectionWidget := tview.NewDropDown(). SetLabel(" 🪣 Bucket Name: "). - SetOptions(bucketList, func(name string, index int) { - bucketName = name + SetOptions(ctx.config.bucketList, func(name string, index int) { + ctx.config.bucketName = name }). SetCurrentOption(0). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack). + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). SetFieldWidth(25) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(navigationHomeLabel, func() { - pages.SwitchToPage("home") + AddButton(ctx.theme.navigationHomeLabel, func() { + ctx.pages.SwitchToPage("home") }). - AddButton(navigationNextLabel, func() { - pages.SwitchToPage("page5") + AddButton(ctx.theme.navigationNextLabel, func() { + ctx.pages.SwitchToPage("page5") }). - AddButton(navigationBackLabel, func() { - pages.SwitchToPage("page3") + AddButton(ctx.theme.navigationBackLabel, func() { + ctx.pages.SwitchToPage("page3") }). - AddButton(navigationPreviewLabel, func() { - previewPage := buildPreviewPage(app, pages, "page4") - pages.AddPage("previewPage", previewPage, true, false) - pages.SwitchToPage("previewPage") + AddButton(ctx.theme.navigationPreviewLabel, func() { + previewPage := ctx.buildPreviewPage("page4") + ctx.pages.AddPage("previewPage", previewPage, true, false) + ctx.pages.SwitchToPage("previewPage") }). - AddButton(navigationQuitLabel, func() { - app.Stop() + AddButton(ctx.theme.navigationQuitLabel, func() { + ctx.app.Stop() }). - SetButtonBackgroundColor(navigationButtonColor). - SetButtonTextColor(tcell.ColorBlack). - SetButtonsAlign(navigationButtonAlignment) + SetButtonBackgroundColor(ctx.theme.navigationButtonColor). + SetButtonTextColor(ctx.theme.navigationButtonTextColor) // Assemble page layout layout := tview.NewFlex(). @@ -655,10 +720,10 @@ func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview. AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). AddItem(nil, 2, 0, false). AddItem(bucketSelectionWidget, 2, 0, false). - AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false). + AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) - layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return layout } @@ -667,7 +732,7 @@ func buildBucketSelectionPage(app *tview.Application, pages *tview.Pages) tview. // // Function to build the caching page that allows users to configure caching settings. // Includes options for enabling/disabling caching, specifying cache location, size, and retention settings. -func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitive { +func (ctx *appContext) buildCachingPage() tview.Primitive { // Main layout container. Must be instantiated first to allow nested items. layout := tview.NewFlex().SetDirection(tview.FlexRow) @@ -679,7 +744,6 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv // Instructions text widget instructionsTextWidget := tview.NewTextView(). - SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true). SetText(instructionsText) @@ -687,95 +751,92 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv // Dropdown widget for enabling/disabling caching cacheLocationFieldWidget := tview.NewInputField(). SetLabel("📁 Cache Location: "). - SetText(cacheLocation). + SetText(ctx.config.cacheLocation). SetFieldWidth(40). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack). + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). SetChangedFunc(func(text string) { - cacheLocation = text + ctx.config.cacheLocation = text }) // Input field widget for cache size percentage cacheSizeFieldWidget := tview.NewInputField(). SetLabel("📊 Cache Size (%): "). - SetText(cacheSize). // Default to 80% + SetText(ctx.config.cacheSize). // Default to 80% SetFieldWidth(4). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack). - SetChangedFunc(func(text string) { - if size, err := strconv.Atoi(text); err != nil || size < 1 || size > 100 { - showErrorModal( - app, - pages, + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). + SetChangedFunc(func(size string) { + if size, err := strconv.Atoi(size); err != nil || size < 1 || size > 100 { + ctx.showErrorModal( "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", func() { - pages.SwitchToPage("page5") + ctx.pages.SwitchToPage("page5") }, ) return } - cacheSize = text + ctx.config.cacheSize = size }) // Input field widget for cache retention duration cacheRetentionDurationFieldWidget := tview.NewInputField(). SetLabel("⌛ Cache Retention Duration: "). - SetText(fmt.Sprintf("%d", cacheRetentionDuration)). + SetText(fmt.Sprintf("%d", ctx.config.cacheRetentionDuration)). SetFieldWidth(5). SetChangedFunc(func(text string) { if val, err := strconv.Atoi(text); err == nil { - cacheRetentionDuration = val + ctx.config.cacheRetentionDuration = val } else { // TODO: Handle invalid input - cacheRetentionDuration = 0 + ctx.config.cacheRetentionDuration = 0 } }). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) // Dropdown widget for cache retention unit cacheRetentionUnitDropdownWidget := tview.NewDropDown(). SetOptions([]string{"Seconds", "Minutes", "Hours", "Days"}, func(option string, index int) { - cacheRetentionUnit = option + ctx.config.cacheRetentionUnit = option // Convert cache retention duration to seconds - switch cacheRetentionUnit { + switch ctx.config.cacheRetentionUnit { case "Seconds": - cacheRetentionDurationSec = cacheRetentionDuration + ctx.config.cacheRetentionDurationSec = ctx.config.cacheRetentionDuration case "Minutes": - minutes := cacheRetentionDuration - cacheRetentionDurationSec = minutes * 60 + minutes := ctx.config.cacheRetentionDuration + ctx.config.cacheRetentionDurationSec = minutes * 60 case "Hours": - hours := cacheRetentionDuration - cacheRetentionDurationSec = hours * 3600 + hours := ctx.config.cacheRetentionDuration + ctx.config.cacheRetentionDurationSec = hours * 3600 case "Days": - days := cacheRetentionDuration - cacheRetentionDurationSec = days * 86400 + days := ctx.config.cacheRetentionDuration + ctx.config.cacheRetentionDurationSec = days * 86400 } }). SetCurrentOption(3). // Default to Days - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) // Dropdown widget for enabling/disabling cache cleanup on restart // If enabled --> allow-non-empty-temp: false // if disabled --> allow-non-empty-temp: true clearCacheOnStartDropdownWidget := tview.NewDropDown(). SetLabel("🧹 Clear Cache On Start: "). - SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { - if text == " Enabled " { - clearCacheOnStart = true + SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { + if option == " Enabled " { + ctx.config.clearCacheOnStart = true } else { - clearCacheOnStart = false + ctx.config.clearCacheOnStart = false } - }). - SetCurrentOption(0). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) + }).SetCurrentOption(0). + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) // Horizontal container to place retention duration and unit side by side cacheRetentionRow := tview.NewFlex(). @@ -797,28 +858,26 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv // Navigation buttons widget navigationButtonsWidget := tview.NewForm() navigationButtonsWidget. - AddButton(navigationHomeLabel, func() { - pages.SwitchToPage("home") + AddButton(ctx.theme.navigationHomeLabel, func() { + ctx.pages.SwitchToPage("home") }). - AddButton(navigationFinishLabel, func() { + AddButton(ctx.theme.navigationFinishLabel, func() { // Check if caching is enabled and validate cache settings - if enableCaching { + if ctx.config.enableCaching { // Validate the cache location - if err := validateCachePath(); err != nil { - showErrorModal(app, pages, "Invalid cache location:\n"+err.Error(), func() { - pages.SwitchToPage("page5") + if err := ctx.validateCachePath(); err != nil { + ctx.showErrorModal("Invalid cache location:\n"+err.Error(), func() { + ctx.pages.SwitchToPage("page5") }) return } // Check available cache size - if err := getAvailableCacheSize(); err != nil { - showErrorModal( - app, - pages, + if err := ctx.getAvailableCacheSize(); err != nil { + ctx.showErrorModal( "Failed to check available cache size:\n"+err.Error(), func() { - pages.SwitchToPage("page5") + ctx.pages.SwitchToPage("page5") }, ) return @@ -826,91 +885,93 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv cacheSizeText := fmt.Sprintf( "Available Disk Space @ Cache Location: [darkred::b]%d GB[-::-]\n", - availableCacheSizeGB, + ctx.config.availableCacheSizeGB, ) + fmt.Sprintf( "Cache Size Currently Set to: [darkred::b]%.0f GB (%s%%)[-::-]\n\n", - float64(currentCacheSizeGB), - cacheSize, + float64(ctx.config.currentCacheSizeGB), + ctx.config.cacheSize, ) + "Would you like to proceed with this cache size?\n\n" + "If not, hit [darkred::b]Return[-::-] to adjust cache size accordingly. Otherwise, hit [darkred::b]Finish[-::-] to complete the configuration." - showCacheConfirmationModal(app, pages, cacheSizeText, + ctx.showCacheConfirmationModal(cacheSizeText, // Callback function if the user selects Finish func() { - if err := createYAMLConfig(); err != nil { - showErrorModal( - app, - pages, + if err := ctx.createYAMLConfig(); err != nil { + ctx.showErrorModal( "Failed to create YAML config:\n"+err.Error(), func() { - pages.SwitchToPage("page5") + ctx.pages.SwitchToPage("page5") }, ) return } - showExitModal(app, pages, func() { - app.Stop() + ctx.showExitModal(func() { + ctx.app.Stop() }) }, // Callback function if the user selects Return func() { - pages.SwitchToPage("page5") + ctx.pages.SwitchToPage("page5") }) } else { // If caching is disabled, just finish the configuration - if err := createYAMLConfig(); err != nil { - showErrorModal(app, pages, "Failed to create YAML config:\n"+err.Error(), func() { - pages.SwitchToPage("page5") + if err := ctx.createYAMLConfig(); err != nil { + ctx.showErrorModal("Failed to create YAML config:\n"+err.Error(), func() { + ctx.pages.SwitchToPage("page5") }) return } - showExitModal(app, pages, func() { - app.Stop() + ctx.showExitModal(func() { + ctx.app.Stop() }) } }). - AddButton(navigationBackLabel, func() { - if storageProtocol == "azstorage" { - pages.SwitchToPage("page3") + AddButton(ctx.theme.navigationBackLabel, func() { + if ctx.config.storageProtocol == "azstorage" { + ctx.pages.SwitchToPage("page3") } else { - page4 := buildBucketSelectionPage(app, pages) - pages.AddPage("page4", page4, true, false) - pages.SwitchToPage("page4") + page4 := ctx.buildBucketSelectionPage() + ctx.pages.AddPage("page4", page4, true, false) + ctx.pages.SwitchToPage("page4") } }). - AddButton(navigationPreviewLabel, func() { - previewPage := buildPreviewPage(app, pages, "page5") - pages.AddPage("previewPage", previewPage, true, false) - pages.SwitchToPage("previewPage") + AddButton(ctx.theme.navigationPreviewLabel, func() { + previewPage := ctx.buildPreviewPage("page5") + ctx.pages.AddPage("previewPage", previewPage, true, false) + ctx.pages.SwitchToPage("previewPage") }). - AddButton(navigationQuitLabel, func() { - app.Stop() + AddButton(ctx.theme.navigationQuitLabel, func() { + ctx.app.Stop() }). - SetButtonBackgroundColor(navigationButtonColor). - SetButtonTextColor(tcell.ColorBlack). - SetButtonsAlign(tuiAlignment) + SetButtonBackgroundColor(ctx.theme.navigationButtonColor). + SetButtonTextColor(colorBlack) // Widget to enable/disable caching enableCachingDropdownWidget := tview.NewDropDown() enableCachingDropdownWidget. SetLabel("💾 Caching: "). - SetOptions([]string{" Enabled ", " Disabled "}, func(text string, index int) { - if text == " Enabled " { - cacheMode = "file_cache" - enableCaching = true + SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { + if option == " Enabled " { + ctx.config.cacheMode = "file_cache" + ctx.config.enableCaching = true if !showCacheFields { layout.RemoveItem(navigationButtonsWidget) layout.RemoveItem(cacheFields) layout.AddItem(cacheFields, 8, 0, false) - layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) + layout.AddItem( + navigationButtonsWidget, + ctx.theme.navigationWidgetHeight, + 0, + false, + ) showCacheFields = true } } else { - cacheMode = "block_cache" - enableCaching = false + ctx.config.cacheMode = "block_cache" + ctx.config.enableCaching = false if showCacheFields { layout.RemoveItem(cacheFields) showCacheFields = false @@ -918,8 +979,8 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv } }). SetCurrentOption(0). - SetLabelColor(widgetLabelColor). - SetFieldBackgroundColor(widgetFieldBackgroundColor). + SetLabelColor(ctx.theme.widgetLabelColor). + SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). SetFieldTextColor(tcell.ColorBlack) // Assemble page layout @@ -930,9 +991,9 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv layout.AddItem(cacheFields, 8, 0, false) } - layout.AddItem(navigationButtonsWidget, navigationWidgetHeight, 0, false) + layout.AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false) layout.AddItem(nil, 1, 0, false) - layout.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return layout } @@ -942,34 +1003,30 @@ func buildCachingPage(app *tview.Application, pages *tview.Pages) tview.Primitiv // Function to build the summary page that displays the configuration summary. // This function creates a text view with the summary information and a return button. // The preview page parameter allows switching back to the previous page when the user clicks "Return". -func buildPreviewPage( - app *tview.Application, - pages *tview.Pages, - previewPage string, -) tview.Primitive { +func (ctx *appContext) buildPreviewPage(previewPage string) tview.Primitive { summaryText := "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]" + - fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", storageProvider) + - fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", endpointURL) + - fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", bucketName) + - fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", cacheMode) + - fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", cacheLocation) + + fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", ctx.config.storageProvider) + + fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", ctx.config.endpointURL) + + fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", ctx.config.bucketName) + + fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", ctx.config.cacheMode) + + fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", ctx.config.cacheLocation) + fmt.Sprintf( " Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", - cacheSize, - currentCacheSizeGB, + ctx.config.cacheSize, + ctx.config.currentCacheSizeGB, ) // Display cache retention duration in seconds and specified unit - if cacheRetentionUnit == "Seconds" { + if ctx.config.cacheRetentionUnit == "Seconds" { summaryText += fmt.Sprintf( " Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", - cacheRetentionDurationSec, + ctx.config.cacheRetentionDurationSec, ) } else { summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", - cacheRetentionDurationSec, cacheRetentionDuration, cacheRetentionUnit) + ctx.config.cacheRetentionDurationSec, ctx.config.cacheRetentionDuration, ctx.config.cacheRetentionUnit) } // Set a dynamic width and height for the summary widget @@ -977,7 +1034,6 @@ func buildPreviewPage( summaryWidgetWidth := getTextWidth(summaryText) / 3 summaryWidget := tview.NewTextView(). - SetTextAlign(tuiAlignment). SetWrap(true). SetDynamicColors(true). SetText(summaryText). @@ -985,12 +1041,12 @@ func buildPreviewPage( returnButton := tview.NewButton("[black]Return[-]"). SetSelectedFunc(func() { - pages.SwitchToPage(previewPage) + ctx.pages.SwitchToPage(previewPage) }) - returnButton.SetBackgroundColor(greenColor) + returnButton.SetBackgroundColor(colorGreen) returnButton.SetBorder(true) - returnButton.SetBorderColor(yellowColor) - returnButton.SetBackgroundColorActivated(greenColor) + returnButton.SetBorderColor(colorYellow) + returnButton.SetBackgroundColorActivated(colorGreen) buttons := tview.NewFlex(). SetDirection(tview.FlexColumn). @@ -1007,7 +1063,7 @@ func buildPreviewPage( leftAlignedModal := tview.NewFlex(). AddItem(modal, summaryWidgetWidth, 0, true) - leftAlignedModal.SetBorder(true).SetBorderColor(greenColor).SetBorderPadding(1, 1, 1, 1) + leftAlignedModal.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return leftAlignedModal } @@ -1015,28 +1071,26 @@ func buildPreviewPage( // Function to show a modal dialog with a message and an "OK" button. // This function is used to display error messages or confirmations. // May specify a callback function to execute when the modal is closed. -func showErrorModal(app *tview.Application, pages *tview.Pages, message string, onClose func()) { +func (ctx *appContext) showErrorModal(message string, onClose func()) { modal := tview.NewModal(). SetText(message). AddButtons([]string{"OK"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { - pages.RemovePage("modal") + ctx.pages.RemovePage("modal") onClose() }). - SetBackgroundColor(greenColor). + SetBackgroundColor(colorGreen). SetTextColor(tcell.ColorBlack) modal.SetBorder(true) - modal.SetBorderColor(yellowColor) - modal.SetButtonBackgroundColor(yellowColor) + modal.SetBorderColor(colorYellow) + modal.SetButtonBackgroundColor(colorYellow) modal.SetButtonTextColor(tcell.ColorBlack) - pages.AddPage("modal", modal, false, true) + ctx.pages.AddPage("modal", modal, false, true) } // Function to show a confirmation modal dialog with "Finish" and "Return" buttons. // Used to confirm cache size before proceeding. Must specify two callback functions for the "Finish" and "Return" actions. -func showCacheConfirmationModal( - app *tview.Application, - pages *tview.Pages, +func (ctx *appContext) showCacheConfirmationModal( message string, onFinish func(), onReturn func(), @@ -1045,45 +1099,45 @@ func showCacheConfirmationModal( SetText(message). AddButtons([]string{"Finish", "Return"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { - pages.RemovePage("modal") + ctx.pages.RemovePage("modal") if buttonLabel == "Finish" { onFinish() } else { onReturn() } }). - SetBackgroundColor(greenColor). + SetBackgroundColor(colorGreen). SetTextColor(tcell.ColorBlack) modal.SetBorder(true) - modal.SetBorderColor(yellowColor) - modal.SetButtonBackgroundColor(yellowColor) + modal.SetBorderColor(colorYellow) + modal.SetButtonBackgroundColor(colorYellow) modal.SetButtonTextColor(tcell.ColorBlack) - pages.AddPage("modal", modal, true, true) + ctx.pages.AddPage("modal", modal, true, true) } // Function to show final exit modal when configuration is complete. // Informs the user that the configuration is complete and they can exit. // This function is called when the user clicks "Finish" on the caching page. -func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) { +func (ctx *appContext) showExitModal(onConfirm func()) { processingEmojis := []string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "✅"} modal := tview.NewModal(). AddButtons([]string{"Exit"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { - pages.RemovePage("modal") + ctx.pages.RemovePage("modal") if buttonLabel == "Exit" { onConfirm() } }). - SetBackgroundColor(greenColor). + SetBackgroundColor(colorGreen). SetTextColor(tcell.ColorBlack) modal.SetBorder(true) - modal.SetBorderColor(yellowColor) - modal.SetButtonBackgroundColor(yellowColor) + modal.SetBorderColor(colorYellow) + modal.SetButtonBackgroundColor(colorYellow) modal.SetButtonTextColor(tcell.ColorBlack) - pages.AddPage("modal", modal, true, true) + ctx.pages.AddPage("modal", modal, true, true) // Simulate processing with emoji animation go func() { @@ -1091,7 +1145,7 @@ func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) for i := 0; i < len(processingEmojis); i++ { currentEmoji := processingEmojis[i] time.Sleep(100 * time.Millisecond) - app.QueueUpdateDraw(func() { + ctx.app.QueueUpdateDraw(func() { modal.SetText( fmt.Sprintf( "[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", @@ -1102,11 +1156,11 @@ func showExitModal(app *tview.Application, pages *tview.Pages, onConfirm func()) } // After animation, show final message - app.QueueUpdateDraw(func() { + ctx.app.QueueUpdateDraw(func() { modal.SetText(fmt.Sprintf("[#6EBE49::b]Configuration Complete![-::-]\n\n%s\n\n"+ "Your CloudFuse configuration file has been created at:\n\n[blue:white:b]%s[-:-:-]\n\n"+ "You can now exit the application.\n\n"+ - "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], configFilePath)) + "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], ctx.config.configFilePath)) }) }() } @@ -1193,19 +1247,22 @@ func getDefaultCachePath() string { } // Helper function to validate the entered cache path. -func validateCachePath() error { +func (ctx *appContext) validateCachePath() error { // Validate that the path is not empty - if strings.TrimSpace(cacheLocation) == "" { + if strings.TrimSpace(ctx.config.cacheLocation) == "" { return fmt.Errorf("[red::b]ERROR: Cache location cannot be empty[-::-]") } // Make sure no invalid path characters are used - if strings.ContainsAny(cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { + if strings.ContainsAny(ctx.config.cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { return fmt.Errorf("[red::b]ERROR: Cache location contains invalid characters[-::-]") } // Validate that the cache path exists - if cacheLocation != getDefaultCachePath() && cacheMode == "file_cache" { - if _, err := os.Stat(cacheLocation); os.IsNotExist(err) { - return fmt.Errorf("[red::b]ERROR: '%s': No such file or directory[-::-]", cacheLocation) + if ctx.config.cacheLocation != getDefaultCachePath() && ctx.config.cacheMode == "file_cache" { + if _, err := os.Stat(ctx.config.cacheLocation); os.IsNotExist(err) { + return fmt.Errorf( + "[red::b]ERROR: '%s': No such file or directory[-::-]", + ctx.config.cacheLocation, + ) } } return nil @@ -1213,32 +1270,34 @@ func validateCachePath() error { // Helper function to get the available disk space at the cache location and calculates // the cache size in GB based on the user-defined cache size percentage. -func getAvailableCacheSize() error { - availableBlocks, _, err := common.GetAvailFree(cacheLocation) +func (ctx *appContext) getAvailableCacheSize() error { + availableBlocks, _, err := common.GetAvailFree(ctx.config.cacheLocation) if err != nil { // If we fail to get the available cache size, we default to 80% of the available disk space - cacheSize = "80" + ctx.config.cacheSize = "80" returnMsg := fmt.Errorf( "[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ "Defaulting cache size to 80%% of available disk space.\n\n"+ "Please manually verify you have enough disk space available for caching.[-::-]", - cacheLocation, + ctx.config.cacheLocation, err, ) return returnMsg } const blockSize = 4096 - availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes - availableCacheSizeGB = int(availableCacheSizeBytes / (1024 * 1024 * 1024)) // Convert to GB - cacheSizeInt, _ := strconv.Atoi(cacheSize) - currentCacheSizeGB = int(availableCacheSizeGB) * cacheSizeInt / 100 + availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes + ctx.config.availableCacheSizeGB = int( + availableCacheSizeBytes / (1024 * 1024 * 1024), + ) // Convert to GB + cacheSizeInt, _ := strconv.Atoi(ctx.config.cacheSize) + ctx.config.currentCacheSizeGB = int(ctx.config.availableCacheSizeGB) * cacheSizeInt / 100 return nil } // Helper function to normalize and validate the user-defined endpoint URL. -func validateEndpointURL(rawURL string) error { +func (ctx *appContext) validateEndpointURL(rawURL string) error { rawURL = strings.TrimSpace(rawURL) // Check if the URL is empty @@ -1248,7 +1307,7 @@ func validateEndpointURL(rawURL string) error { // Normalize the URL by adding "https://" if it doesn't start with "http://" or "https://" if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { - endpointURL = "https://" + rawURL + ctx.config.endpointURL = "https://" + rawURL return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n" + "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") } @@ -1263,26 +1322,26 @@ func validateEndpointURL(rawURL string) error { // Function to create a temporary YAML configuration file based on user inputs. // Used for testing credentials and then removed after the check. // Called when the user clicks "Next" on the credentials page. -func createTmpConfigFile() error { - config := configuration{ +func (ctx *appContext) createTmpConfigFile() error { + config := configOptions{ - Components: []string{storageProtocol}, + Components: []string{ctx.config.storageProtocol}, } - if storageProtocol == "azstorage" { + if ctx.config.storageProtocol == "azstorage" { config.AzStorage = azstorage.AzStorageOptions{ AccountType: "block", - AccountName: accountName, - AccountKey: accountKey, + AccountName: ctx.config.accountName, + AccountKey: ctx.config.accountKey, AuthMode: "key", - Container: containerName, + Container: ctx.config.containerName, } } else { config.S3Storage = s3StorageConfig{ - BucketName: bucketName, - KeyID: accessKey, - SecretKey: secretKey, - Endpoint: endpointURL, + BucketName: ctx.config.bucketName, + KeyID: ctx.config.accessKey, + SecretKey: ctx.config.secretKey, + Endpoint: ctx.config.endpointURL, EnableDirMarker: true, } @@ -1307,9 +1366,9 @@ func createTmpConfigFile() error { // Attempts to connect to the storage backend and fetch the bucket list. // If successful, populates the global `bucketList` variable with the list of available buckets (for s3 providers only). // Called when the user clicks "Next" on the credentials page. -func checkCredentials(app *tview.Application, pages *tview.Pages) error { +func (ctx *appContext) checkCredentials() error { // Create a temporary config file for testing credentials - if err := createTmpConfigFile(); err != nil { + if err := ctx.createTmpConfigFile(); err != nil { return fmt.Errorf("Failed to create temporary config file: %v", err) } @@ -1330,10 +1389,10 @@ func checkCredentials(app *tview.Application, pages *tview.Pages) error { // Try to fetch bucket list var err error if slices.Contains(options.Components, "azstorage") { - bucketList, err = getContainerListAzure() + ctx.config.bucketList, err = getContainerListAzure() } else if slices.Contains(options.Components, "s3storage") { - bucketList, err = getBucketListS3() + ctx.config.bucketList, err = getBucketListS3() } else { err = fmt.Errorf("Unsupported storage backend") @@ -1348,9 +1407,14 @@ func checkCredentials(app *tview.Application, pages *tview.Pages) error { // Function to create the YAML configuration file based on user inputs once all forms are completed. // Called when the user clicks "Finish" on the caching page. -func createYAMLConfig() error { - config := configuration{ - Components: []string{"libfuse", cacheMode, "attr_cache", storageProtocol}, +func (ctx *appContext) createYAMLConfig() error { + config := configOptions{ + Components: []string{ + "libfuse", + ctx.config.cacheMode, + "attr_cache", + ctx.config.storageProtocol, + }, Libfuse: libfuse.LibfuseOptions{ NetworkShare: true, @@ -1361,34 +1425,36 @@ func createYAMLConfig() error { }, } - if cacheMode == "file_cache" { + if ctx.config.cacheMode == "file_cache" { config.FileCache = file_cache.FileCacheOptions{ - TmpPath: cacheLocation, - Timeout: uint32(cacheRetentionDurationSec), - AllowNonEmpty: !clearCacheOnStart, + TmpPath: ctx.config.cacheLocation, + Timeout: uint32(ctx.config.cacheRetentionDurationSec), + AllowNonEmpty: !ctx.config.clearCacheOnStart, SyncToFlush: true, } // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it - if cacheSize != "80" { - config.FileCache.MaxSizeMB = float64(currentCacheSizeGB * 1024) // Convert GB to MB + if ctx.config.cacheSize != "80" { + config.FileCache.MaxSizeMB = float64( + ctx.config.currentCacheSizeGB * 1024, + ) // Convert GB to MB } } - if storageProtocol == "s3storage" { + if ctx.config.storageProtocol == "s3storage" { config.S3Storage = s3StorageConfig{ - BucketName: bucketName, - KeyID: accessKey, - SecretKey: secretKey, - Endpoint: endpointURL, + BucketName: ctx.config.bucketName, + KeyID: ctx.config.accessKey, + SecretKey: ctx.config.secretKey, + Endpoint: ctx.config.endpointURL, EnableDirMarker: true, } } else { config.AzStorage = azstorage.AzStorageOptions{ AccountType: "block", - AccountName: accountName, - AccountKey: accountKey, + AccountName: ctx.config.accountName, + AccountKey: ctx.config.accountKey, AuthMode: "key", - Container: containerName, + Container: ctx.config.containerName, } } @@ -1409,7 +1475,7 @@ func createYAMLConfig() error { return fmt.Errorf("Error: %v", err) } - configFilePath = filepath.Join(currDir, "config.yaml") + ctx.config.configFilePath = filepath.Join(currDir, "config.yaml") return nil } From 833de03b369aa78bd7762a41c7d2e05b4d1ac0bd Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 25 Aug 2025 20:12:16 -0600 Subject: [PATCH 13/21] change method receiver name from ctx to tui --- cmd/tui.go | 647 ++++++++++++++++++++++++++--------------------------- 1 file changed, 323 insertions(+), 324 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index 6c5c06de8..bfab723db 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -79,7 +79,6 @@ type userConfig struct { cacheRetentionDuration int // User-defined cache retention duration. Default is '2' cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' - theme uiTheme // Holds color and label settings for UI elements } // Struct to hold UI theme settings, including colors and labels for various widgets. @@ -156,14 +155,14 @@ func newAppContext() *appContext { // Main function to run the TUI application. // Initializes the tview application, builds the TUI application, and runs it. -func (ctx *appContext) runTUI() error { - ctx.app.EnableMouse(true) - ctx.app.EnablePaste(true) +func (tui *appContext) runTUI() error { + tui.app.EnableMouse(true) + tui.app.EnablePaste(true) - ctx.buildTUI() + tui.buildTUI() // Run the application - if err := ctx.app.Run(); err != nil { + if err := tui.app.Run(); err != nil { panic(err) } @@ -171,32 +170,32 @@ func (ctx *appContext) runTUI() error { } // Function to build the TUI application. Initializes the pages and adds them to the page stack. -func (ctx *appContext) buildTUI() { +func (tui *appContext) buildTUI() { // Initialize the pages - homePage := ctx.buildHomePage() // --- Home Page --- - page1 := ctx.buildStorageProviderPage() // --- Page 1: Storage Provider Selection --- - page2 := ctx.buildEndpointURLPage() // --- Page 2: Endpoint URL Entry --- - page3 := ctx.buildCredentialsPage() // --- Page 3: Credentials Entry --- - page4 := ctx.buildBucketSelectionPage() // --- Page 4: Bucket Selection --- - page5 := ctx.buildCachingPage() // --- Page 5: Caching Settings --- + homePage := tui.buildHomePage() // --- Home Page --- + page1 := tui.buildStorageProviderPage() // --- Page 1: Storage Provider Selection --- + page2 := tui.buildEndpointURLPage() // --- Page 2: Endpoint URL Entry --- + page3 := tui.buildCredentialsPage() // --- Page 3: Credentials Entry --- + page4 := tui.buildBucketSelectionPage() // --- Page 4: Bucket Selection --- + page5 := tui.buildCachingPage() // --- Page 5: Caching Settings --- // Add pages to the page stack - ctx.pages.AddPage("home", homePage, true, true) - ctx.pages.AddPage("page1", page1, true, false) - ctx.pages.AddPage("page2", page2, true, false) - ctx.pages.AddPage("page3", page3, true, false) - ctx.pages.AddPage("page4", page4, true, false) - ctx.pages.AddPage("page5", page5, true, false) - - ctx.app.SetRoot(ctx.pages, true) + tui.pages.AddPage("home", homePage, true, true) + tui.pages.AddPage("page1", page1, true, false) + tui.pages.AddPage("page2", page2, true, false) + tui.pages.AddPage("page3", page3, true, false) + tui.pages.AddPage("page4", page4, true, false) + tui.pages.AddPage("page5", page5, true, false) + + tui.app.SetRoot(tui.pages, true) } // --- Page 0: Home Page --- // // Function to build the home page of the TUI application. Displays a // welcome banner, instructions, and buttons to start or quit the application. -func (ctx *appContext) buildHomePage() tview.Primitive { +func (tui *appContext) buildHomePage() tview.Primitive { bannerText := "[#6EBE49::b]" + " █▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + @@ -225,14 +224,14 @@ func (ctx *appContext) buildHomePage() tview.Primitive { // Start/Quit buttons widget startQuitButtonsWidget := tview.NewForm(). - AddButton(ctx.theme.navigationStartLabel, func() { - ctx.pages.SwitchToPage("page1") + AddButton(tui.theme.navigationStartLabel, func() { + tui.pages.SwitchToPage("page1") }). - AddButton(ctx.theme.navigationQuitLabel, func() { - ctx.app.Stop() + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() }). - SetButtonBackgroundColor(ctx.theme.navigationButtonColor). - SetButtonTextColor(ctx.theme.navigationButtonTextColor) + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) aboutText := "[#FFD700::b]ABOUT[-::-]\n" + "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + @@ -268,7 +267,7 @@ func (ctx *appContext) buildHomePage() tview.Primitive { // // Function to build the storage provider selection page. Allows users to select their cloud storage provider // from a dropdown list. The options are: LyveCloud, Microsoft, AWS, and Other S3. -func (ctx *appContext) buildStorageProviderPage() tview.Primitive { +func (tui *appContext) buildStorageProviderPage() tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + @@ -285,59 +284,59 @@ func (ctx *appContext) buildStorageProviderPage() tview.Primitive { storageProviderDropdownWidget := tview.NewDropDown(). SetLabel("📦 Storage Provider: "). SetOptions([]string{" LyveCloud ⬇️", " Microsoft ", " AWS ", " Other (s3) "}, func(option string, index int) { - ctx.config.storageProvider = option + tui.config.storageProvider = option switch option { case " LyveCloud ⬇️": - ctx.config.storageProtocol = "s3storage" - ctx.config.storageProvider = "LyveCloud" + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "LyveCloud" case " Microsoft ": - ctx.config.storageProtocol = "azstorage" - ctx.config.storageProvider = "Microsoft" + tui.config.storageProtocol = "azstorage" + tui.config.storageProvider = "Microsoft" case " AWS ": - ctx.config.storageProtocol = "s3storage" - ctx.config.storageProvider = "AWS" + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "AWS" case " Other (s3) ": - ctx.config.storageProtocol = "s3storage" - ctx.config.storageProvider = "Other" - ctx.config.endpointURL = "" + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "Other" + tui.config.endpointURL = "" default: - ctx.config.storageProtocol = "s3storage" - ctx.config.storageProvider = "LyveCloud" + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "LyveCloud" } }). SetCurrentOption(0). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack). SetFieldWidth(14) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(ctx.theme.navigationHomeLabel, func() { - ctx.pages.SwitchToPage("home") + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") }). - AddButton(ctx.theme.navigationNextLabel, func() { + AddButton(tui.theme.navigationNextLabel, func() { // If Microsoft is selected, switch to page 3 and skip endpoint entry, handled internally by Azure SDK. - if ctx.config.storageProvider == "Microsoft" { - page3 := ctx.buildCredentialsPage() - ctx.pages.AddPage("page3", page3, true, false) - ctx.pages.SwitchToPage("page3") + if tui.config.storageProvider == "Microsoft" { + page3 := tui.buildCredentialsPage() + tui.pages.AddPage("page3", page3, true, false) + tui.pages.SwitchToPage("page3") } else { - page2 := ctx.buildEndpointURLPage() - ctx.pages.AddPage("page2", page2, true, false) - ctx.pages.SwitchToPage("page2") + page2 := tui.buildEndpointURLPage() + tui.pages.AddPage("page2", page2, true, false) + tui.pages.SwitchToPage("page2") } }). - AddButton(ctx.theme.navigationPreviewLabel, func() { - previewPage := ctx.buildPreviewPage("page1") - ctx.pages.AddPage("previewPage", previewPage, true, false) - ctx.pages.SwitchToPage("previewPage") + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page1") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") }). - AddButton(ctx.theme.navigationQuitLabel, func() { - ctx.app.Stop() + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() }). - SetButtonBackgroundColor(ctx.theme.navigationButtonColor). - SetButtonTextColor(ctx.theme.navigationButtonTextColor) + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) // Assemble page layout layout := tview.NewFlex(). @@ -345,7 +344,7 @@ func (ctx *appContext) buildStorageProviderPage() tview.Primitive { AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). AddItem(nil, 1, 0, false). AddItem(storageProviderDropdownWidget, 2, 0, false). - AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false). + AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) @@ -357,11 +356,11 @@ func (ctx *appContext) buildStorageProviderPage() tview.Primitive { // // Function to build the endpoint URL page. Allows users to enter the endpoint URL for their cloud storage provider. // It validates the endpoint URL format and provides help text based on the selected provider. -func (ctx *appContext) buildEndpointURLPage() tview.Primitive { +func (tui *appContext) buildEndpointURLPage() tview.Primitive { var urlRegionHelpText string // Determine URL help text based on selected provider - switch ctx.config.storageProvider { + switch tui.config.storageProvider { case "LyveCloud": urlRegionHelpText = "[::b]You selected LyveCloud as your storage provider.[::-]\n\n" + "For LyveCloud, the endpoint URL format is generally:\n" + @@ -387,7 +386,7 @@ func (ctx *appContext) buildEndpointURLPage() tview.Primitive { instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n"+ "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ - "[white]\n %s", ctx.config.storageProvider, urlRegionHelpText) + "[white]\n %s", tui.config.storageProvider, urlRegionHelpText) instructionsTextWidget := tview.NewTextView(). SetText(instructionsText). @@ -396,54 +395,54 @@ func (ctx *appContext) buildEndpointURLPage() tview.Primitive { endpointURLFieldWidget := tview.NewInputField(). SetLabel("🔗 Endpoint URL: "). - SetText(ctx.config.endpointURL). + SetText(tui.config.endpointURL). SetFieldWidth(50). SetChangedFunc(func(url string) { - ctx.config.endpointURL = url + tui.config.endpointURL = url }). SetPlaceholder("\t\t\t\t"). SetPlaceholderTextColor(tcell.ColorGray). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(ctx.theme.navigationHomeLabel, func() { - ctx.pages.SwitchToPage("home") + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") }). - AddButton(ctx.theme.navigationNextLabel, func() { - if err := ctx.validateEndpointURL(ctx.config.endpointURL); err != nil { - ctx.showErrorModal( + AddButton(tui.theme.navigationNextLabel, func() { + if err := tui.validateEndpointURL(tui.config.endpointURL); err != nil { + tui.showErrorModal( fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), func() { - ctx.pages.RemovePage("page2") - page2 := ctx.buildEndpointURLPage() - ctx.pages.AddPage("page2", page2, true, false) - ctx.pages.SwitchToPage("page2") + tui.pages.RemovePage("page2") + page2 := tui.buildEndpointURLPage() + tui.pages.AddPage("page2", page2, true, false) + tui.pages.SwitchToPage("page2") }, ) return } - ctx.pages.RemovePage("page3") - page3 := ctx.buildCredentialsPage() - ctx.pages.AddPage("page3", page3, true, false) - ctx.pages.SwitchToPage("page3") + tui.pages.RemovePage("page3") + page3 := tui.buildCredentialsPage() + tui.pages.AddPage("page3", page3, true, false) + tui.pages.SwitchToPage("page3") }). - AddButton(ctx.theme.navigationBackLabel, func() { - ctx.pages.SwitchToPage("page1") + AddButton(tui.theme.navigationBackLabel, func() { + tui.pages.SwitchToPage("page1") }). - AddButton(ctx.theme.navigationPreviewLabel, func() { - previewPage := ctx.buildPreviewPage("page2") - ctx.pages.AddPage("previewPage", previewPage, true, false) - ctx.pages.SwitchToPage("previewPage") + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page2") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") }). - AddButton(ctx.theme.navigationQuitLabel, func() { - ctx.app.Stop() + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() }). - SetButtonBackgroundColor(ctx.theme.navigationButtonColor). - SetLabelColor(ctx.theme.widgetLabelColor). - SetButtonTextColor(ctx.theme.navigationButtonTextColor) + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) // Assemble page layout layout := tview.NewFlex(). @@ -451,7 +450,7 @@ func (ctx *appContext) buildEndpointURLPage() tview.Primitive { AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). AddItem(nil, 2, 0, false). AddItem(endpointURLFieldWidget, 2, 0, false). - AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false). + AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) @@ -464,14 +463,14 @@ func (ctx *appContext) buildEndpointURLPage() tview.Primitive { // Function to build the credentials page. Allows users to enter their cloud storage credentials. // If the storage protocol is "s3", it provides input fields for access key, secret key. // If the storage protocol is "azure", it provides input fields for account name, account key, and container name. -func (ctx *appContext) buildCredentialsPage() tview.Primitive { +func (tui *appContext) buildCredentialsPage() tview.Primitive { layout := tview.NewFlex() layout.Clear() // Determine labels for input fields based on storage protocol. accessLabel := "" secretLabel := "" - if ctx.config.storageProtocol == "azstorage" { + if tui.config.storageProtocol == "azstorage" { accessLabel = "🔑 Account Name: " secretLabel = "🔑 Account Key: " } else { @@ -502,7 +501,7 @@ func (ctx *appContext) buildCredentialsPage() tview.Primitive { colorYellow, ) - if ctx.config.storageProtocol == "azstorage" { + if tui.config.storageProtocol == "azstorage" { instructionsText += fmt.Sprintf( "[%s::b] -Container Name:[-::-] This is the name of your Azure Blob Storage container.\n", colorYellow, @@ -520,75 +519,75 @@ func (ctx *appContext) buildCredentialsPage() tview.Primitive { // Access key field widget accessKeyFieldWidget := tview.NewInputField(). SetLabel(accessLabel). - SetText(ctx.config.accessKey). + SetText(tui.config.accessKey). SetFieldWidth(50). SetChangedFunc(func(key string) { - ctx.config.accessKey = key - ctx.config.accountName = key + tui.config.accessKey = key + tui.config.accountName = key }). SetPlaceholder("\t\t\t\t"). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Secret key field widget with masked input secretKeyFieldWidget := tview.NewInputField(). SetLabel(secretLabel). - SetText(string(ctx.config.secretKey)). + SetText(string(tui.config.secretKey)). SetFieldWidth(50). SetChangedFunc(func(key string) { - ctx.config.secretKey = key - ctx.config.accountKey = key + tui.config.secretKey = key + tui.config.accountKey = key }). SetPlaceholder("\t\t\t\t"). SetMaskCharacter('*'). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Container name field widget for Azure storage containerNameFieldWidget := tview.NewInputField(). SetLabel("🪣 Container Name: "). - SetText(ctx.config.containerName). + SetText(tui.config.containerName). SetPlaceholder("\t\t\t\t"). SetChangedFunc(func(name string) { - ctx.config.containerName = name - ctx.config.bucketName = name + tui.config.containerName = name + tui.config.bucketName = name }). SetFieldWidth(50). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Passphrase field widget for config file encryption passphraseFieldWidget := tview.NewInputField(). SetLabel("🔒 Passphrase: "). - SetText(ctx.config.configEncryptionPassphrase). + SetText(tui.config.configEncryptionPassphrase). SetFieldWidth(50). SetChangedFunc(func(passphrase string) { - ctx.config.configEncryptionPassphrase = strings.TrimSpace(passphrase) + tui.config.configEncryptionPassphrase = strings.TrimSpace(passphrase) }). SetPlaceholder("\t\t\t "). SetMaskCharacter('*'). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(ctx.theme.navigationHomeLabel, func() { - ctx.pages.SwitchToPage("home") + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") }). - AddButton(ctx.theme.navigationNextLabel, func() { + AddButton(tui.theme.navigationNextLabel, func() { // TODO: Add validation for access key and secret key HERE // For now, just check that they are not empty - if (ctx.config.storageProtocol == "s3storage" && (len(ctx.config.accessKey) == 0 || len(ctx.config.secretKey) == 0)) || - (ctx.config.storageProtocol == "azstorage" && (len(ctx.config.accountName) == 0 || len(ctx.config.accountKey) == 0 || len(ctx.config.containerName) == 0)) || - len(ctx.config.configEncryptionPassphrase) == 0 { - ctx.showErrorModal( + if (tui.config.storageProtocol == "s3storage" && (len(tui.config.accessKey) == 0 || len(tui.config.secretKey) == 0)) || + (tui.config.storageProtocol == "azstorage" && (len(tui.config.accountName) == 0 || len(tui.config.accountKey) == 0 || len(tui.config.containerName) == 0)) || + len(tui.config.configEncryptionPassphrase) == 0 { + tui.showErrorModal( "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", func() { - ctx.pages.SwitchToPage("page3") + tui.pages.SwitchToPage("page3") }, ) return @@ -596,46 +595,46 @@ func (ctx *appContext) buildCredentialsPage() tview.Primitive { // TODO: Fix bug here where calling listBuckets() in the checkCredentials() function // causes the layout to shift upwards and the widgets to be misaligned if the user incorrectly // enters credentials. - if err := ctx.checkCredentials(); err != nil { - ctx.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { - ctx.pages.RemovePage("page3") // Remove the current page - page3 := ctx.buildCredentialsPage() // Rebuild the page - ctx.pages.AddPage("page3", page3, true, false) // Add the new page - ctx.pages.SwitchToPage("page3") + if err := tui.checkCredentials(); err != nil { + tui.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { + tui.pages.RemovePage("page3") // Remove the current page + page3 := tui.buildCredentialsPage() // Rebuild the page + tui.pages.AddPage("page3", page3, true, false) // Add the new page + tui.pages.SwitchToPage("page3") }) return } - if ctx.config.storageProtocol == "azstorage" { - ctx.pages.RemovePage("page4") // Remove previous page if it exists - ctx.pages.SwitchToPage("page5") + if tui.config.storageProtocol == "azstorage" { + tui.pages.RemovePage("page4") // Remove previous page if it exists + tui.pages.SwitchToPage("page5") } else { - page4 := ctx.buildBucketSelectionPage() - ctx.pages.AddPage("page4", page4, true, false) - ctx.pages.SwitchToPage("page4") + page4 := tui.buildBucketSelectionPage() + tui.pages.AddPage("page4", page4, true, false) + tui.pages.SwitchToPage("page4") } }). - AddButton(ctx.theme.navigationBackLabel, func() { - if ctx.config.storageProvider == "Microsoft" || ctx.config.storageProvider == "AWS" { - ctx.pages.RemovePage("page2") - ctx.pages.SwitchToPage("page1") + AddButton(tui.theme.navigationBackLabel, func() { + if tui.config.storageProvider == "Microsoft" || tui.config.storageProvider == "AWS" { + tui.pages.RemovePage("page2") + tui.pages.SwitchToPage("page1") } else { - page2 := ctx.buildEndpointURLPage() - ctx.pages.AddPage("page2", page2, true, false) - ctx.pages.SwitchToPage("page2") + page2 := tui.buildEndpointURLPage() + tui.pages.AddPage("page2", page2, true, false) + tui.pages.SwitchToPage("page2") } }). - AddButton(ctx.theme.navigationPreviewLabel, func() { - previewPage := ctx.buildPreviewPage("page3") - ctx.pages.AddPage("previewPage", previewPage, true, false) - ctx.pages.SwitchToPage("previewPage") + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page3") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") }). - AddButton(ctx.theme.navigationQuitLabel, func() { - ctx.app.Stop() + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() }). - SetLabelColor(ctx.theme.widgetLabelColor). - SetButtonBackgroundColor(ctx.theme.navigationButtonColor). - SetButtonTextColor(ctx.theme.navigationButtonTextColor) + SetLabelColor(tui.theme.widgetLabelColor). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) // Combine all credential widgets into a single form credentialsWidget := tview.NewForm(). @@ -643,11 +642,11 @@ func (ctx *appContext) buildCredentialsPage() tview.Primitive { AddFormItem(secretKeyFieldWidget). AddFormItem(passphraseFieldWidget). SetFieldTextColor(tcell.ColorBlack). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor) + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor) // If Azure is selected, add the container name field - if ctx.config.storageProvider == "Microsoft" { + if tui.config.storageProvider == "Microsoft" { credentialsWidget.AddFormItem(containerNameFieldWidget) } @@ -656,7 +655,7 @@ func (ctx *appContext) buildCredentialsPage() tview.Primitive { layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) layout.AddItem(nil, 1, 0, false) layout.AddItem(credentialsWidget, credentialsWidget.GetFormItemCount()*2+1, 0, false) - layout.AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false) + layout.AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false) layout.AddItem(nil, 1, 0, false) layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) @@ -667,7 +666,7 @@ func (ctx *appContext) buildCredentialsPage() tview.Primitive { // // Function to build the bucket selection page. Allows users to select a bucket from a dropdown list // of retrieved buckets based on provided s3 credentials. For s3 storage users only. Azure storage users will skip this page. -func (ctx *appContext) buildBucketSelectionPage() tview.Primitive { +func (tui *appContext) buildBucketSelectionPage() tview.Primitive { instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + @@ -683,36 +682,36 @@ func (ctx *appContext) buildBucketSelectionPage() tview.Primitive { // Dropdown widget for selecting bucket name bucketSelectionWidget := tview.NewDropDown(). SetLabel(" 🪣 Bucket Name: "). - SetOptions(ctx.config.bucketList, func(name string, index int) { - ctx.config.bucketName = name + SetOptions(tui.config.bucketList, func(name string, index int) { + tui.config.bucketName = name }). SetCurrentOption(0). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack). SetFieldWidth(25) // Navigation buttons widget navigationButtonsWidget := tview.NewForm(). - AddButton(ctx.theme.navigationHomeLabel, func() { - ctx.pages.SwitchToPage("home") + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") }). - AddButton(ctx.theme.navigationNextLabel, func() { - ctx.pages.SwitchToPage("page5") + AddButton(tui.theme.navigationNextLabel, func() { + tui.pages.SwitchToPage("page5") }). - AddButton(ctx.theme.navigationBackLabel, func() { - ctx.pages.SwitchToPage("page3") + AddButton(tui.theme.navigationBackLabel, func() { + tui.pages.SwitchToPage("page3") }). - AddButton(ctx.theme.navigationPreviewLabel, func() { - previewPage := ctx.buildPreviewPage("page4") - ctx.pages.AddPage("previewPage", previewPage, true, false) - ctx.pages.SwitchToPage("previewPage") + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page4") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") }). - AddButton(ctx.theme.navigationQuitLabel, func() { - ctx.app.Stop() + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() }). - SetButtonBackgroundColor(ctx.theme.navigationButtonColor). - SetButtonTextColor(ctx.theme.navigationButtonTextColor) + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) // Assemble page layout layout := tview.NewFlex(). @@ -720,7 +719,7 @@ func (ctx *appContext) buildBucketSelectionPage() tview.Primitive { AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). AddItem(nil, 2, 0, false). AddItem(bucketSelectionWidget, 2, 0, false). - AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false). + AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). AddItem(nil, 1, 0, false) layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) @@ -732,7 +731,7 @@ func (ctx *appContext) buildBucketSelectionPage() tview.Primitive { // // Function to build the caching page that allows users to configure caching settings. // Includes options for enabling/disabling caching, specifying cache location, size, and retention settings. -func (ctx *appContext) buildCachingPage() tview.Primitive { +func (tui *appContext) buildCachingPage() tview.Primitive { // Main layout container. Must be instantiated first to allow nested items. layout := tview.NewFlex().SetDirection(tview.FlexRow) @@ -751,75 +750,75 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { // Dropdown widget for enabling/disabling caching cacheLocationFieldWidget := tview.NewInputField(). SetLabel("📁 Cache Location: "). - SetText(ctx.config.cacheLocation). + SetText(tui.config.cacheLocation). SetFieldWidth(40). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack). SetChangedFunc(func(text string) { - ctx.config.cacheLocation = text + tui.config.cacheLocation = text }) // Input field widget for cache size percentage cacheSizeFieldWidget := tview.NewInputField(). SetLabel("📊 Cache Size (%): "). - SetText(ctx.config.cacheSize). // Default to 80% + SetText(tui.config.cacheSize). // Default to 80% SetFieldWidth(4). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack). SetChangedFunc(func(size string) { if size, err := strconv.Atoi(size); err != nil || size < 1 || size > 100 { - ctx.showErrorModal( + tui.showErrorModal( "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", func() { - ctx.pages.SwitchToPage("page5") + tui.pages.SwitchToPage("page5") }, ) return } - ctx.config.cacheSize = size + tui.config.cacheSize = size }) // Input field widget for cache retention duration cacheRetentionDurationFieldWidget := tview.NewInputField(). SetLabel("⌛ Cache Retention Duration: "). - SetText(fmt.Sprintf("%d", ctx.config.cacheRetentionDuration)). + SetText(fmt.Sprintf("%d", tui.config.cacheRetentionDuration)). SetFieldWidth(5). SetChangedFunc(func(text string) { if val, err := strconv.Atoi(text); err == nil { - ctx.config.cacheRetentionDuration = val + tui.config.cacheRetentionDuration = val } else { // TODO: Handle invalid input - ctx.config.cacheRetentionDuration = 0 + tui.config.cacheRetentionDuration = 0 } }). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Dropdown widget for cache retention unit cacheRetentionUnitDropdownWidget := tview.NewDropDown(). SetOptions([]string{"Seconds", "Minutes", "Hours", "Days"}, func(option string, index int) { - ctx.config.cacheRetentionUnit = option + tui.config.cacheRetentionUnit = option // Convert cache retention duration to seconds - switch ctx.config.cacheRetentionUnit { + switch tui.config.cacheRetentionUnit { case "Seconds": - ctx.config.cacheRetentionDurationSec = ctx.config.cacheRetentionDuration + tui.config.cacheRetentionDurationSec = tui.config.cacheRetentionDuration case "Minutes": - minutes := ctx.config.cacheRetentionDuration - ctx.config.cacheRetentionDurationSec = minutes * 60 + minutes := tui.config.cacheRetentionDuration + tui.config.cacheRetentionDurationSec = minutes * 60 case "Hours": - hours := ctx.config.cacheRetentionDuration - ctx.config.cacheRetentionDurationSec = hours * 3600 + hours := tui.config.cacheRetentionDuration + tui.config.cacheRetentionDurationSec = hours * 3600 case "Days": - days := ctx.config.cacheRetentionDuration - ctx.config.cacheRetentionDurationSec = days * 86400 + days := tui.config.cacheRetentionDuration + tui.config.cacheRetentionDurationSec = days * 86400 } }). SetCurrentOption(3). // Default to Days - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Dropdown widget for enabling/disabling cache cleanup on restart @@ -829,13 +828,13 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { SetLabel("🧹 Clear Cache On Start: "). SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { if option == " Enabled " { - ctx.config.clearCacheOnStart = true + tui.config.clearCacheOnStart = true } else { - ctx.config.clearCacheOnStart = false + tui.config.clearCacheOnStart = false } }).SetCurrentOption(0). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) // Horizontal container to place retention duration and unit side by side @@ -858,26 +857,26 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { // Navigation buttons widget navigationButtonsWidget := tview.NewForm() navigationButtonsWidget. - AddButton(ctx.theme.navigationHomeLabel, func() { - ctx.pages.SwitchToPage("home") + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") }). - AddButton(ctx.theme.navigationFinishLabel, func() { + AddButton(tui.theme.navigationFinishLabel, func() { // Check if caching is enabled and validate cache settings - if ctx.config.enableCaching { + if tui.config.enableCaching { // Validate the cache location - if err := ctx.validateCachePath(); err != nil { - ctx.showErrorModal("Invalid cache location:\n"+err.Error(), func() { - ctx.pages.SwitchToPage("page5") + if err := tui.validateCachePath(); err != nil { + tui.showErrorModal("Invalid cache location:\n"+err.Error(), func() { + tui.pages.SwitchToPage("page5") }) return } // Check available cache size - if err := ctx.getAvailableCacheSize(); err != nil { - ctx.showErrorModal( + if err := tui.getAvailableCacheSize(); err != nil { + tui.showErrorModal( "Failed to check available cache size:\n"+err.Error(), func() { - ctx.pages.SwitchToPage("page5") + tui.pages.SwitchToPage("page5") }, ) return @@ -885,68 +884,68 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { cacheSizeText := fmt.Sprintf( "Available Disk Space @ Cache Location: [darkred::b]%d GB[-::-]\n", - ctx.config.availableCacheSizeGB, + tui.config.availableCacheSizeGB, ) + fmt.Sprintf( "Cache Size Currently Set to: [darkred::b]%.0f GB (%s%%)[-::-]\n\n", - float64(ctx.config.currentCacheSizeGB), - ctx.config.cacheSize, + float64(tui.config.currentCacheSizeGB), + tui.config.cacheSize, ) + "Would you like to proceed with this cache size?\n\n" + "If not, hit [darkred::b]Return[-::-] to adjust cache size accordingly. Otherwise, hit [darkred::b]Finish[-::-] to complete the configuration." - ctx.showCacheConfirmationModal(cacheSizeText, + tui.showCacheConfirmationModal(cacheSizeText, // Callback function if the user selects Finish func() { - if err := ctx.createYAMLConfig(); err != nil { - ctx.showErrorModal( + if err := tui.createYAMLConfig(); err != nil { + tui.showErrorModal( "Failed to create YAML config:\n"+err.Error(), func() { - ctx.pages.SwitchToPage("page5") + tui.pages.SwitchToPage("page5") }, ) return } - ctx.showExitModal(func() { - ctx.app.Stop() + tui.showExitModal(func() { + tui.app.Stop() }) }, // Callback function if the user selects Return func() { - ctx.pages.SwitchToPage("page5") + tui.pages.SwitchToPage("page5") }) } else { // If caching is disabled, just finish the configuration - if err := ctx.createYAMLConfig(); err != nil { - ctx.showErrorModal("Failed to create YAML config:\n"+err.Error(), func() { - ctx.pages.SwitchToPage("page5") + if err := tui.createYAMLConfig(); err != nil { + tui.showErrorModal("Failed to create YAML config:\n"+err.Error(), func() { + tui.pages.SwitchToPage("page5") }) return } - ctx.showExitModal(func() { - ctx.app.Stop() + tui.showExitModal(func() { + tui.app.Stop() }) } }). - AddButton(ctx.theme.navigationBackLabel, func() { - if ctx.config.storageProtocol == "azstorage" { - ctx.pages.SwitchToPage("page3") + AddButton(tui.theme.navigationBackLabel, func() { + if tui.config.storageProtocol == "azstorage" { + tui.pages.SwitchToPage("page3") } else { - page4 := ctx.buildBucketSelectionPage() - ctx.pages.AddPage("page4", page4, true, false) - ctx.pages.SwitchToPage("page4") + page4 := tui.buildBucketSelectionPage() + tui.pages.AddPage("page4", page4, true, false) + tui.pages.SwitchToPage("page4") } }). - AddButton(ctx.theme.navigationPreviewLabel, func() { - previewPage := ctx.buildPreviewPage("page5") - ctx.pages.AddPage("previewPage", previewPage, true, false) - ctx.pages.SwitchToPage("previewPage") + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page5") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") }). - AddButton(ctx.theme.navigationQuitLabel, func() { - ctx.app.Stop() + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() }). - SetButtonBackgroundColor(ctx.theme.navigationButtonColor). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). SetButtonTextColor(colorBlack) // Widget to enable/disable caching @@ -955,23 +954,23 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { SetLabel("💾 Caching: "). SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { if option == " Enabled " { - ctx.config.cacheMode = "file_cache" - ctx.config.enableCaching = true + tui.config.cacheMode = "file_cache" + tui.config.enableCaching = true if !showCacheFields { layout.RemoveItem(navigationButtonsWidget) layout.RemoveItem(cacheFields) layout.AddItem(cacheFields, 8, 0, false) layout.AddItem( navigationButtonsWidget, - ctx.theme.navigationWidgetHeight, + tui.theme.navigationWidgetHeight, 0, false, ) showCacheFields = true } } else { - ctx.config.cacheMode = "block_cache" - ctx.config.enableCaching = false + tui.config.cacheMode = "block_cache" + tui.config.enableCaching = false if showCacheFields { layout.RemoveItem(cacheFields) showCacheFields = false @@ -979,8 +978,8 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { } }). SetCurrentOption(0). - SetLabelColor(ctx.theme.widgetLabelColor). - SetFieldBackgroundColor(ctx.theme.widgetFieldBackgroundColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(tcell.ColorBlack) // Assemble page layout @@ -991,7 +990,7 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { layout.AddItem(cacheFields, 8, 0, false) } - layout.AddItem(navigationButtonsWidget, ctx.theme.navigationWidgetHeight, 0, false) + layout.AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false) layout.AddItem(nil, 1, 0, false) layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) @@ -1003,30 +1002,30 @@ func (ctx *appContext) buildCachingPage() tview.Primitive { // Function to build the summary page that displays the configuration summary. // This function creates a text view with the summary information and a return button. // The preview page parameter allows switching back to the previous page when the user clicks "Return". -func (ctx *appContext) buildPreviewPage(previewPage string) tview.Primitive { +func (tui *appContext) buildPreviewPage(previewPage string) tview.Primitive { summaryText := "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n" + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]" + - fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", ctx.config.storageProvider) + - fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", ctx.config.endpointURL) + - fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", ctx.config.bucketName) + - fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", ctx.config.cacheMode) + - fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", ctx.config.cacheLocation) + + fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", tui.config.storageProvider) + + fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", tui.config.endpointURL) + + fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", tui.config.bucketName) + + fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", tui.config.cacheMode) + + fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", tui.config.cacheLocation) + fmt.Sprintf( " Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", - ctx.config.cacheSize, - ctx.config.currentCacheSizeGB, + tui.config.cacheSize, + tui.config.currentCacheSizeGB, ) // Display cache retention duration in seconds and specified unit - if ctx.config.cacheRetentionUnit == "Seconds" { + if tui.config.cacheRetentionUnit == "Seconds" { summaryText += fmt.Sprintf( " Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", - ctx.config.cacheRetentionDurationSec, + tui.config.cacheRetentionDurationSec, ) } else { summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", - ctx.config.cacheRetentionDurationSec, ctx.config.cacheRetentionDuration, ctx.config.cacheRetentionUnit) + tui.config.cacheRetentionDurationSec, tui.config.cacheRetentionDuration, tui.config.cacheRetentionUnit) } // Set a dynamic width and height for the summary widget @@ -1041,7 +1040,7 @@ func (ctx *appContext) buildPreviewPage(previewPage string) tview.Primitive { returnButton := tview.NewButton("[black]Return[-]"). SetSelectedFunc(func() { - ctx.pages.SwitchToPage(previewPage) + tui.pages.SwitchToPage(previewPage) }) returnButton.SetBackgroundColor(colorGreen) returnButton.SetBorder(true) @@ -1071,12 +1070,12 @@ func (ctx *appContext) buildPreviewPage(previewPage string) tview.Primitive { // Function to show a modal dialog with a message and an "OK" button. // This function is used to display error messages or confirmations. // May specify a callback function to execute when the modal is closed. -func (ctx *appContext) showErrorModal(message string, onClose func()) { +func (tui *appContext) showErrorModal(message string, onClose func()) { modal := tview.NewModal(). SetText(message). AddButtons([]string{"OK"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { - ctx.pages.RemovePage("modal") + tui.pages.RemovePage("modal") onClose() }). SetBackgroundColor(colorGreen). @@ -1085,12 +1084,12 @@ func (ctx *appContext) showErrorModal(message string, onClose func()) { modal.SetBorderColor(colorYellow) modal.SetButtonBackgroundColor(colorYellow) modal.SetButtonTextColor(tcell.ColorBlack) - ctx.pages.AddPage("modal", modal, false, true) + tui.pages.AddPage("modal", modal, false, true) } // Function to show a confirmation modal dialog with "Finish" and "Return" buttons. // Used to confirm cache size before proceeding. Must specify two callback functions for the "Finish" and "Return" actions. -func (ctx *appContext) showCacheConfirmationModal( +func (tui *appContext) showCacheConfirmationModal( message string, onFinish func(), onReturn func(), @@ -1099,7 +1098,7 @@ func (ctx *appContext) showCacheConfirmationModal( SetText(message). AddButtons([]string{"Finish", "Return"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { - ctx.pages.RemovePage("modal") + tui.pages.RemovePage("modal") if buttonLabel == "Finish" { onFinish() } else { @@ -1112,20 +1111,20 @@ func (ctx *appContext) showCacheConfirmationModal( modal.SetBorderColor(colorYellow) modal.SetButtonBackgroundColor(colorYellow) modal.SetButtonTextColor(tcell.ColorBlack) - ctx.pages.AddPage("modal", modal, true, true) + tui.pages.AddPage("modal", modal, true, true) } // Function to show final exit modal when configuration is complete. // Informs the user that the configuration is complete and they can exit. // This function is called when the user clicks "Finish" on the caching page. -func (ctx *appContext) showExitModal(onConfirm func()) { +func (tui *appContext) showExitModal(onConfirm func()) { processingEmojis := []string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "✅"} modal := tview.NewModal(). AddButtons([]string{"Exit"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { - ctx.pages.RemovePage("modal") + tui.pages.RemovePage("modal") if buttonLabel == "Exit" { onConfirm() } @@ -1137,7 +1136,7 @@ func (ctx *appContext) showExitModal(onConfirm func()) { modal.SetButtonBackgroundColor(colorYellow) modal.SetButtonTextColor(tcell.ColorBlack) - ctx.pages.AddPage("modal", modal, true, true) + tui.pages.AddPage("modal", modal, true, true) // Simulate processing with emoji animation go func() { @@ -1145,7 +1144,7 @@ func (ctx *appContext) showExitModal(onConfirm func()) { for i := 0; i < len(processingEmojis); i++ { currentEmoji := processingEmojis[i] time.Sleep(100 * time.Millisecond) - ctx.app.QueueUpdateDraw(func() { + tui.app.QueueUpdateDraw(func() { modal.SetText( fmt.Sprintf( "[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", @@ -1156,11 +1155,11 @@ func (ctx *appContext) showExitModal(onConfirm func()) { } // After animation, show final message - ctx.app.QueueUpdateDraw(func() { + tui.app.QueueUpdateDraw(func() { modal.SetText(fmt.Sprintf("[#6EBE49::b]Configuration Complete![-::-]\n\n%s\n\n"+ "Your CloudFuse configuration file has been created at:\n\n[blue:white:b]%s[-:-:-]\n\n"+ "You can now exit the application.\n\n"+ - "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], ctx.config.configFilePath)) + "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], tui.config.configFilePath)) }) }() } @@ -1247,21 +1246,21 @@ func getDefaultCachePath() string { } // Helper function to validate the entered cache path. -func (ctx *appContext) validateCachePath() error { +func (tui *appContext) validateCachePath() error { // Validate that the path is not empty - if strings.TrimSpace(ctx.config.cacheLocation) == "" { + if strings.TrimSpace(tui.config.cacheLocation) == "" { return fmt.Errorf("[red::b]ERROR: Cache location cannot be empty[-::-]") } // Make sure no invalid path characters are used - if strings.ContainsAny(ctx.config.cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { + if strings.ContainsAny(tui.config.cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { return fmt.Errorf("[red::b]ERROR: Cache location contains invalid characters[-::-]") } // Validate that the cache path exists - if ctx.config.cacheLocation != getDefaultCachePath() && ctx.config.cacheMode == "file_cache" { - if _, err := os.Stat(ctx.config.cacheLocation); os.IsNotExist(err) { + if tui.config.cacheLocation != getDefaultCachePath() && tui.config.cacheMode == "file_cache" { + if _, err := os.Stat(tui.config.cacheLocation); os.IsNotExist(err) { return fmt.Errorf( "[red::b]ERROR: '%s': No such file or directory[-::-]", - ctx.config.cacheLocation, + tui.config.cacheLocation, ) } } @@ -1270,16 +1269,16 @@ func (ctx *appContext) validateCachePath() error { // Helper function to get the available disk space at the cache location and calculates // the cache size in GB based on the user-defined cache size percentage. -func (ctx *appContext) getAvailableCacheSize() error { - availableBlocks, _, err := common.GetAvailFree(ctx.config.cacheLocation) +func (tui *appContext) getAvailableCacheSize() error { + availableBlocks, _, err := common.GetAvailFree(tui.config.cacheLocation) if err != nil { // If we fail to get the available cache size, we default to 80% of the available disk space - ctx.config.cacheSize = "80" + tui.config.cacheSize = "80" returnMsg := fmt.Errorf( "[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ "Defaulting cache size to 80%% of available disk space.\n\n"+ "Please manually verify you have enough disk space available for caching.[-::-]", - ctx.config.cacheLocation, + tui.config.cacheLocation, err, ) return returnMsg @@ -1287,17 +1286,17 @@ func (ctx *appContext) getAvailableCacheSize() error { const blockSize = 4096 availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes - ctx.config.availableCacheSizeGB = int( + tui.config.availableCacheSizeGB = int( availableCacheSizeBytes / (1024 * 1024 * 1024), ) // Convert to GB - cacheSizeInt, _ := strconv.Atoi(ctx.config.cacheSize) - ctx.config.currentCacheSizeGB = int(ctx.config.availableCacheSizeGB) * cacheSizeInt / 100 + cacheSizeInt, _ := strconv.Atoi(tui.config.cacheSize) + tui.config.currentCacheSizeGB = int(tui.config.availableCacheSizeGB) * cacheSizeInt / 100 return nil } // Helper function to normalize and validate the user-defined endpoint URL. -func (ctx *appContext) validateEndpointURL(rawURL string) error { +func (tui *appContext) validateEndpointURL(rawURL string) error { rawURL = strings.TrimSpace(rawURL) // Check if the URL is empty @@ -1307,7 +1306,7 @@ func (ctx *appContext) validateEndpointURL(rawURL string) error { // Normalize the URL by adding "https://" if it doesn't start with "http://" or "https://" if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { - ctx.config.endpointURL = "https://" + rawURL + tui.config.endpointURL = "https://" + rawURL return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n" + "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") } @@ -1322,26 +1321,26 @@ func (ctx *appContext) validateEndpointURL(rawURL string) error { // Function to create a temporary YAML configuration file based on user inputs. // Used for testing credentials and then removed after the check. // Called when the user clicks "Next" on the credentials page. -func (ctx *appContext) createTmpConfigFile() error { +func (tui *appContext) createTmpConfigFile() error { config := configOptions{ - Components: []string{ctx.config.storageProtocol}, + Components: []string{tui.config.storageProtocol}, } - if ctx.config.storageProtocol == "azstorage" { + if tui.config.storageProtocol == "azstorage" { config.AzStorage = azstorage.AzStorageOptions{ AccountType: "block", - AccountName: ctx.config.accountName, - AccountKey: ctx.config.accountKey, + AccountName: tui.config.accountName, + AccountKey: tui.config.accountKey, AuthMode: "key", - Container: ctx.config.containerName, + Container: tui.config.containerName, } } else { config.S3Storage = s3StorageConfig{ - BucketName: ctx.config.bucketName, - KeyID: ctx.config.accessKey, - SecretKey: ctx.config.secretKey, - Endpoint: ctx.config.endpointURL, + BucketName: tui.config.bucketName, + KeyID: tui.config.accessKey, + SecretKey: tui.config.secretKey, + Endpoint: tui.config.endpointURL, EnableDirMarker: true, } @@ -1366,9 +1365,9 @@ func (ctx *appContext) createTmpConfigFile() error { // Attempts to connect to the storage backend and fetch the bucket list. // If successful, populates the global `bucketList` variable with the list of available buckets (for s3 providers only). // Called when the user clicks "Next" on the credentials page. -func (ctx *appContext) checkCredentials() error { +func (tui *appContext) checkCredentials() error { // Create a temporary config file for testing credentials - if err := ctx.createTmpConfigFile(); err != nil { + if err := tui.createTmpConfigFile(); err != nil { return fmt.Errorf("Failed to create temporary config file: %v", err) } @@ -1389,10 +1388,10 @@ func (ctx *appContext) checkCredentials() error { // Try to fetch bucket list var err error if slices.Contains(options.Components, "azstorage") { - ctx.config.bucketList, err = getContainerListAzure() + tui.config.bucketList, err = getContainerListAzure() } else if slices.Contains(options.Components, "s3storage") { - ctx.config.bucketList, err = getBucketListS3() + tui.config.bucketList, err = getBucketListS3() } else { err = fmt.Errorf("Unsupported storage backend") @@ -1407,13 +1406,13 @@ func (ctx *appContext) checkCredentials() error { // Function to create the YAML configuration file based on user inputs once all forms are completed. // Called when the user clicks "Finish" on the caching page. -func (ctx *appContext) createYAMLConfig() error { +func (tui *appContext) createYAMLConfig() error { config := configOptions{ Components: []string{ "libfuse", - ctx.config.cacheMode, + tui.config.cacheMode, "attr_cache", - ctx.config.storageProtocol, + tui.config.storageProtocol, }, Libfuse: libfuse.LibfuseOptions{ @@ -1425,36 +1424,36 @@ func (ctx *appContext) createYAMLConfig() error { }, } - if ctx.config.cacheMode == "file_cache" { + if tui.config.cacheMode == "file_cache" { config.FileCache = file_cache.FileCacheOptions{ - TmpPath: ctx.config.cacheLocation, - Timeout: uint32(ctx.config.cacheRetentionDurationSec), - AllowNonEmpty: !ctx.config.clearCacheOnStart, + TmpPath: tui.config.cacheLocation, + Timeout: uint32(tui.config.cacheRetentionDurationSec), + AllowNonEmpty: !tui.config.clearCacheOnStart, SyncToFlush: true, } // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it - if ctx.config.cacheSize != "80" { + if tui.config.cacheSize != "80" { config.FileCache.MaxSizeMB = float64( - ctx.config.currentCacheSizeGB * 1024, + tui.config.currentCacheSizeGB * 1024, ) // Convert GB to MB } } - if ctx.config.storageProtocol == "s3storage" { + if tui.config.storageProtocol == "s3storage" { config.S3Storage = s3StorageConfig{ - BucketName: ctx.config.bucketName, - KeyID: ctx.config.accessKey, - SecretKey: ctx.config.secretKey, - Endpoint: ctx.config.endpointURL, + BucketName: tui.config.bucketName, + KeyID: tui.config.accessKey, + SecretKey: tui.config.secretKey, + Endpoint: tui.config.endpointURL, EnableDirMarker: true, } } else { config.AzStorage = azstorage.AzStorageOptions{ AccountType: "block", - AccountName: ctx.config.accountName, - AccountKey: ctx.config.accountKey, + AccountName: tui.config.accountName, + AccountKey: tui.config.accountKey, AuthMode: "key", - Container: ctx.config.containerName, + Container: tui.config.containerName, } } @@ -1475,7 +1474,7 @@ func (ctx *appContext) createYAMLConfig() error { return fmt.Errorf("Error: %v", err) } - ctx.config.configFilePath = filepath.Join(currDir, "config.yaml") + tui.config.configFilePath = filepath.Join(currDir, "config.yaml") return nil } From 3318ec3eb00f21d47f3d2953b7c2112f290116c6 Mon Sep 17 00:00:00 2001 From: brayan Date: Mon, 25 Aug 2025 21:01:15 -0600 Subject: [PATCH 14/21] remove createTmpConfigFile() and read from buffer instead --- cmd/tui.go | 54 +++++++++++++----------------------------------------- 1 file changed, 13 insertions(+), 41 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index bfab723db..88f412ca4 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -1318,17 +1318,18 @@ func (tui *appContext) validateEndpointURL(rawURL string) error { return nil } -// Function to create a temporary YAML configuration file based on user inputs. -// Used for testing credentials and then removed after the check. +// Function to check the credentials entered by the user. +// Attempts to connect to the storage backend and fetch the bucket list. +// If successful, populates the `bucketList` variable with the list of available buckets (for s3 providers only). // Called when the user clicks "Next" on the credentials page. -func (tui *appContext) createTmpConfigFile() error { - config := configOptions{ - +func (tui *appContext) checkCredentials() error { + // Create a temporary configOptions struct with only the storage component + tmpConfig := configOptions{ Components: []string{tui.config.storageProtocol}, } if tui.config.storageProtocol == "azstorage" { - config.AzStorage = azstorage.AzStorageOptions{ + tmpConfig.AzStorage = azstorage.AzStorageOptions{ AccountType: "block", AccountName: tui.config.accountName, AccountKey: tui.config.accountKey, @@ -1336,50 +1337,21 @@ func (tui *appContext) createTmpConfigFile() error { Container: tui.config.containerName, } } else { - config.S3Storage = s3StorageConfig{ + tmpConfig.S3Storage = s3StorageConfig{ BucketName: tui.config.bucketName, KeyID: tui.config.accessKey, SecretKey: tui.config.secretKey, Endpoint: tui.config.endpointURL, EnableDirMarker: true, } - - } - - yamlData, err := yaml.Marshal(&config) - if err != nil { - return fmt.Errorf("failed to marshal YAML: %v", err) } - tmpFile := "config-tmp.yaml" - if err := os.WriteFile(tmpFile, yamlData, 0600); err != nil { - return fmt.Errorf("failed to write YAML to file: %v", err) - } + // Marshal the temporary struct to YAML + tmpConfigData, _ := yaml.Marshal(&tmpConfig) - // Update options.ConfigFile to point to the temporary file - options.ConfigFile = "config-tmp.yaml" - return nil -} - -// Function to check the credentials entered by the user. -// Attempts to connect to the storage backend and fetch the bucket list. -// If successful, populates the global `bucketList` variable with the list of available buckets (for s3 providers only). -// Called when the user clicks "Next" on the credentials page. -func (tui *appContext) checkCredentials() error { - // Create a temporary config file for testing credentials - if err := tui.createTmpConfigFile(); err != nil { - return fmt.Errorf("Failed to create temporary config file: %v", err) - } - - // Delete the temporary config file regardless of success or failure of the credential check - defer func() { - _ = os.Remove("config-tmp.yaml") - }() - - // Parse and unmarshal the temporary config file - if err := parseConfig(); err != nil { - return fmt.Errorf("Failed to parse config: %v", err) - } + // Write the temporary config data into the global options struct instead of a temporary file. + // This avoids the need to create and delete a temporary file on disk. + _ = config.ReadFromConfigBuffer(tmpConfigData) if err := config.Unmarshal(&options); err != nil { return fmt.Errorf("Failed to unmarshal config: %v", err) From f90abfd69c14e9abdeca9563cceffd59cf1bc85e Mon Sep 17 00:00:00 2001 From: brayan Date: Tue, 26 Aug 2025 11:01:36 -0600 Subject: [PATCH 15/21] add config file encryption before writing to disk --- cmd/tui.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/cmd/tui.go b/cmd/tui.go index 88f412ca4..b954f7c61 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -41,6 +41,7 @@ import ( "github.com/Seagate/cloudfuse/component/azstorage" "github.com/Seagate/cloudfuse/component/file_cache" "github.com/Seagate/cloudfuse/component/libfuse" + "github.com/awnumar/memguard" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "gopkg.in/yaml.v3" @@ -1346,12 +1347,14 @@ func (tui *appContext) checkCredentials() error { } } - // Marshal the temporary struct to YAML + // Marshal the temporary struct to YAML format tmpConfigData, _ := yaml.Marshal(&tmpConfig) // Write the temporary config data into the global options struct instead of a temporary file. // This avoids the need to create and delete a temporary file on disk. - _ = config.ReadFromConfigBuffer(tmpConfigData) + if err := config.ReadFromConfigBuffer(tmpConfigData); err != nil { + return fmt.Errorf("Failed to read config from buffer: %v", err) + } if err := config.Unmarshal(&options); err != nil { return fmt.Errorf("Failed to unmarshal config: %v", err) @@ -1429,24 +1432,31 @@ func (tui *appContext) createYAMLConfig() error { } } - // Marshal the struct to YAML (returns []byte and error) - yamlData, err := yaml.Marshal(&config) + // Marshal the struct to YAML format + configData, err := yaml.Marshal(&config) + if err != nil { + return fmt.Errorf("Failed to marshal configuration data to YAML: %v", err) + } + + // Encrypt the YAML config data using the user-provided passphrase + encryptedPassphrase := memguard.NewEnclave([]byte(tui.config.configEncryptionPassphrase)) + cipherText, err := common.EncryptData(configData, encryptedPassphrase) if err != nil { - return fmt.Errorf("Failed to marshal YAML: %v", err) + return fmt.Errorf("Failed to encrypt configuration data: %v", err) } - // Write the YAML to a file - if err := os.WriteFile("config.yaml", yamlData, 0600); err != nil { - return fmt.Errorf("Failed to write YAML to file: %v", err) + // Write the encrypted YAML config data to a file + if err := os.WriteFile("config.aes", cipherText, 0600); err != nil { + return fmt.Errorf("Failed to create encrypted config.aes file: %v", err) } - // Update global configFilePath variable + // Update configFilePath member to point to the created config file currDir, err := os.Getwd() if err != nil { return fmt.Errorf("Error: %v", err) } - tui.config.configFilePath = filepath.Join(currDir, "config.yaml") + tui.config.configFilePath = filepath.Join(currDir, "config.aes") return nil } From 2886ed2fca2c9d9c70cd6f4d6eefec6d84af92d4 Mon Sep 17 00:00:00 2001 From: brayan Date: Tue, 26 Aug 2025 14:22:13 -0600 Subject: [PATCH 16/21] remove tui.go and migrate code into config.go --- cmd/config.go | 1435 +++++++++++++++++++++++++++++++++++++++++++++++- cmd/tui.go | 1462 ------------------------------------------------- 2 files changed, 1433 insertions(+), 1464 deletions(-) delete mode 100644 cmd/tui.go diff --git a/cmd/config.go b/cmd/config.go index 2cac545e9..aca44e1b6 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -27,17 +27,141 @@ package cmd import ( "fmt" + "net/url" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + "time" + "github.com/Seagate/cloudfuse/common" + "github.com/Seagate/cloudfuse/common/config" + "github.com/Seagate/cloudfuse/component/attr_cache" + "github.com/Seagate/cloudfuse/component/azstorage" + "github.com/Seagate/cloudfuse/component/file_cache" + "github.com/Seagate/cloudfuse/component/libfuse" + "github.com/awnumar/memguard" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" ) +// Top-level struct to hold application context, including tview application instance, +// page stack, user configuration data, and UI theme settings. +type appContext struct { + app *tview.Application + pages *tview.Pages + config *userConfig + theme *uiTheme +} + +// Struct to hold user configuration data collected from the TUI session. +type userConfig struct { + configEncryptionPassphrase string // Sets config file encryption passphrase + configFilePath string // Sets file_cache.path + accountName string // Sets azstorage.account-name + accountKey string // Sets azstorage.account-key + accessKey string // Sets s3storage.key-id + secretKey string // Sets s3storage.secret-key + containerName string // Sets azstorage.container-name + bucketName string // Sets s3storage.bucket-name + endpointURL string // Sets s3storage.endpoint + bucketList []string // Holds list of available buckets retrieved from cloud provider (for s3 only). + storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider + storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. + cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' + enableCaching bool // If true, sets cacheMode to file_cache. If false, block_cache + cacheLocation string // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache + cacheSize string // User-defined cache size as % + availableCacheSizeGB int // Total available cache size in GB @ the cache location + currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage + clearCacheOnStart bool // If false, sets 'allow-non-empty-temp' to true + cacheRetentionDuration int // User-defined cache retention duration. Default is '2' + cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' + cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' +} + +// Struct to hold UI theme settings, including colors and labels for various widgets. +type uiTheme struct { + widgetLabelColor tcell.Color + widgetFieldBackgroundColor tcell.Color + navigationButtonColor tcell.Color + navigationButtonTextColor tcell.Color + navigationStartLabel string + navigationHomeLabel string + navigationNextLabel string + navigationBackLabel string + navigationPreviewLabel string + navigationQuitLabel string + navigationFinishLabel string + navigationWidgetHeight int +} + +// Global general purpose vars +var ( + colorYellow = tcell.GetColor("#FFD700") + colorGreen = tcell.GetColor("#6EBE49") + colorBlack = tcell.ColorBlack +) + +// Struct to hold the final configuration data to be written to the YAML config file. +type configOptions struct { + Components []string `yaml:"components,omitempty"` + Libfuse libfuse.LibfuseOptions `yaml:"libfuse,omitempty"` + FileCache file_cache.FileCacheOptions `yaml:"file_cache,omitempty"` + AttrCache attr_cache.AttrCacheOptions `yaml:"attr_cache,omitempty"` + S3Storage s3StorageConfig `yaml:"s3storage,omitempty"` + AzStorage azstorage.AzStorageOptions `yaml:"azstorage,omitempty"` +} + +// Struct to hold s3 storage configuration options for the YAML config file. +// TODO: change to using s3storage.Options from component/s3storage/config.go +type s3StorageConfig struct { + BucketName string `yaml:"bucket-name,omitempty"` + KeyID string `yaml:"key-id"` + SecretKey string `yaml:"secret-key"` + Endpoint string `yaml:"endpoint"` + EnableDirMarker bool `yaml:"enable-dir-marker"` +} + +// Constructor for appContext struct. Initializes default values for userConfig and uiTheme. +func newAppContext() *appContext { + return &appContext{ + app: tview.NewApplication(), + pages: tview.NewPages(), + config: &userConfig{ + enableCaching: true, + cacheLocation: getDefaultCachePath(), + cacheSize: "80", + cacheRetentionDuration: 2, + clearCacheOnStart: false, + }, + theme: &uiTheme{ + widgetLabelColor: colorYellow, + widgetFieldBackgroundColor: colorYellow, + navigationButtonColor: colorGreen, + navigationButtonTextColor: colorBlack, + navigationStartLabel: "[black]🚀 Start[-]", + navigationHomeLabel: "[black]🏠 Home[-]", + navigationNextLabel: "[black]🡲 Next[-]", + navigationBackLabel: "[black]🡰 Back[-]", + navigationPreviewLabel: "[black]📄 Preview[-]", + navigationQuitLabel: "[black]❌ Quit[-]", + navigationFinishLabel: "[black]✅ Finish[-]", + navigationWidgetHeight: 3, + }, + } +} + var configCmd = &cobra.Command{ Use: "config", Short: "Launch the interactive configuration tool.", Long: "Starts an interactive terminal-based UI to generate your Cloudfuse configuration file.", RunE: func(cmd *cobra.Command, args []string) error { - ctx := newAppContext() - if err := ctx.runTUI(); err != nil { + tui := newAppContext() + if err := tui.runTUI(); err != nil { return fmt.Errorf("Failed to run TUI: %v", err) } return nil @@ -47,3 +171,1310 @@ var configCmd = &cobra.Command{ func init() { rootCmd.AddCommand(configCmd) } + +// Main function to run the TUI application. +// Initializes the tview application, builds the TUI application, and runs it. +func (tui *appContext) runTUI() error { + tui.app.EnableMouse(true) + tui.app.EnablePaste(true) + + tui.buildTUI() + + // Run the application + if err := tui.app.Run(); err != nil { + panic(err) + } + + return nil +} + +// Function to build the TUI application. Initializes the pages and adds them to the page stack. +func (tui *appContext) buildTUI() { + + // Initialize the pages + homePage := tui.buildHomePage() // --- Home Page --- + page1 := tui.buildStorageProviderPage() // --- Page 1: Storage Provider Selection --- + page2 := tui.buildEndpointURLPage() // --- Page 2: Endpoint URL Entry --- + page3 := tui.buildCredentialsPage() // --- Page 3: Credentials Entry --- + page4 := tui.buildBucketSelectionPage() // --- Page 4: Bucket Selection --- + page5 := tui.buildCachingPage() // --- Page 5: Caching Settings --- + + // Add pages to the page stack + tui.pages.AddPage("home", homePage, true, true) + tui.pages.AddPage("page1", page1, true, false) + tui.pages.AddPage("page2", page2, true, false) + tui.pages.AddPage("page3", page3, true, false) + tui.pages.AddPage("page4", page4, true, false) + tui.pages.AddPage("page5", page5, true, false) + + tui.app.SetRoot(tui.pages, true) +} + +// --- Page 0: Home Page --- +// +// Function to build the home page of the TUI application. Displays a +// welcome banner, instructions, and buttons to start or quit the application. +func (tui *appContext) buildHomePage() tview.Primitive { + bannerText := "[#6EBE49::b]" + + " █▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + + "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + + "[white::b]Welcome to the CloudFuse Configuration Tool\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + + "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" + + // Banner text widget + bannerTextWidget := tview.NewTextView(). + SetText(centerText(bannerText, 75)). + SetDynamicColors(true). + SetWrap(true) + + instructionsText := "[#FFD700::b]Instructions:[::-]\n" + + "[#6EBE49::b]•[-::-] [::]Use your mouse or arrow keys to navigate.[-::-]\n" + + "[#6EBE49::b]•[-::-] [::]Press Enter or left-click to select items.[-::-]\n" + + "[#6EBE49::b]•[-::-] [::]For the best experience, expand terminal window to full size.[-::-]\n" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetText(instructionsText). + SetDynamicColors(true). + SetWrap(true) + + // Start/Quit buttons widget + startQuitButtonsWidget := tview.NewForm(). + AddButton(tui.theme.navigationStartLabel, func() { + tui.pages.SwitchToPage("page1") + }). + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() + }). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) + + aboutText := "[#FFD700::b]ABOUT[-::-]\n" + + "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + + "[grey::i]CloudFuse TUI Configuration Tool\n" + + "Seagate Technology, LLC\n" + + "cloudfuse@seagate.com\n" + + fmt.Sprintf("Version: %s", common.CloudfuseVersion) + + // About text widget + aboutTextWidget := tview.NewTextView(). + SetText(centerText(aboutText, 75)). + SetDynamicColors(true). + SetWrap(true) + + // Assemble page layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(bannerTextWidget, getTextHeight(bannerText), 0, false). // Banner Widget + AddItem(nil, 1, 0, false). // Padding + AddItem(startQuitButtonsWidget, 3, 0, false). // Start/Quit buttons widget + AddItem(nil, 1, 0, false). // Padding + AddItem(instructionsTextWidget, 4, 0, false). // Instructions widget + AddItem(nil, 2, 0, false). // Padding + AddItem(aboutTextWidget, 9, 0, false). // About widget + AddItem(nil, 1, 0, false) // Bottom padding + + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) + + return layout +} + +// --- Page 1: Storage Provider Selection --- +// +// Function to build the storage provider selection page. Allows users to select their cloud storage provider +// from a dropdown list. The options are: LyveCloud, Microsoft, AWS, and Other S3. +func (tui *appContext) buildStorageProviderPage() tview.Primitive { + instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + + "[grey::i] If your provider is not listed, choose [darkmagenta::b]Other (s3)[-::-][grey::i]. You’ll be\n" + + " prompted to enter the endpoint URL and region manually.[-::-]\n" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetText(instructionsText). + SetDynamicColors(true). + SetWrap(true) + + // Dropdown widget for selecting storage provider + storageProviderDropdownWidget := tview.NewDropDown(). + SetLabel("📦 Storage Provider: "). + SetOptions([]string{" LyveCloud ⬇️", " Microsoft ", " AWS ", " Other (s3) "}, func(option string, index int) { + tui.config.storageProvider = option + switch option { + case " LyveCloud ⬇️": + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "LyveCloud" + case " Microsoft ": + tui.config.storageProtocol = "azstorage" + tui.config.storageProvider = "Microsoft" + case " AWS ": + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "AWS" + case " Other (s3) ": + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "Other" + tui.config.endpointURL = "" + default: + tui.config.storageProtocol = "s3storage" + tui.config.storageProvider = "LyveCloud" + } + }). + SetCurrentOption(0). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). + SetFieldWidth(14) + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") + }). + AddButton(tui.theme.navigationNextLabel, func() { + // If Microsoft is selected, switch to page 3 and skip endpoint entry, handled internally by Azure SDK. + if tui.config.storageProvider == "Microsoft" { + page3 := tui.buildCredentialsPage() + tui.pages.AddPage("page3", page3, true, false) + tui.pages.SwitchToPage("page3") + } else { + page2 := tui.buildEndpointURLPage() + tui.pages.AddPage("page2", page2, true, false) + tui.pages.SwitchToPage("page2") + } + }). + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page1") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") + }). + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() + }). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) + + // Assemble page layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). + AddItem(nil, 1, 0, false). + AddItem(storageProviderDropdownWidget, 2, 0, false). + AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). + AddItem(nil, 1, 0, false) + + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) + + return layout +} + +// --- Page 2: Endpoint URL Entry Page --- +// +// Function to build the endpoint URL page. Allows users to enter the endpoint URL for their cloud storage provider. +// It validates the endpoint URL format and provides help text based on the selected provider. +func (tui *appContext) buildEndpointURLPage() tview.Primitive { + var urlRegionHelpText string + + // Determine URL help text based on selected provider + switch tui.config.storageProvider { + case "LyveCloud": + urlRegionHelpText = "[::b]You selected LyveCloud as your storage provider.[::-]\n\n" + + "For LyveCloud, the endpoint URL format is generally:\n" + + "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + + "Example:\n[darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + + "[grey::i]Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown.[-::-]" + urlRegionHelpText = centerText(urlRegionHelpText, 65) + + case "AWS": + urlRegionHelpText = "[::b]You selected AWS as your storage provider.[::-]\n\n" + + "The endpoint URL format is generally:\n" + + "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-]\n\n" + + "Example:\n[darkmagenta::b]https://s3.us-east-1.amazonaws.com[-]\n\n" + + "[grey::i]Refer to AWS documentation for valid formats and available regions.[-::-]" + urlRegionHelpText = centerText(urlRegionHelpText, 65) + + case "Other": + urlRegionHelpText = "[::b]You selected a custom s3 provider.[::-]\n\n" + + "Enter the endpoint URL.\n" + + "[grey::i]Refer to your provider’s documentation for valid formats.[-::-]" + urlRegionHelpText = centerText(urlRegionHelpText, 65) + } + + instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n"+ + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ + "[white]\n %s", tui.config.storageProvider, urlRegionHelpText) + + instructionsTextWidget := tview.NewTextView(). + SetText(instructionsText). + SetWrap(true). + SetDynamicColors(true) + + endpointURLFieldWidget := tview.NewInputField(). + SetLabel("🔗 Endpoint URL: "). + SetText(tui.config.endpointURL). + SetFieldWidth(50). + SetChangedFunc(func(url string) { + tui.config.endpointURL = url + }). + SetPlaceholder("\t\t\t\t"). + SetPlaceholderTextColor(tcell.ColorGray). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") + }). + AddButton(tui.theme.navigationNextLabel, func() { + if err := tui.validateEndpointURL(tui.config.endpointURL); err != nil { + tui.showErrorModal( + fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), + func() { + tui.pages.RemovePage("page2") + page2 := tui.buildEndpointURLPage() + tui.pages.AddPage("page2", page2, true, false) + tui.pages.SwitchToPage("page2") + }, + ) + return + } + tui.pages.RemovePage("page3") + page3 := tui.buildCredentialsPage() + tui.pages.AddPage("page3", page3, true, false) + tui.pages.SwitchToPage("page3") + }). + AddButton(tui.theme.navigationBackLabel, func() { + tui.pages.SwitchToPage("page1") + }). + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page2") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") + }). + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() + }). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetLabelColor(tui.theme.widgetLabelColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) + + // Assemble page layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). + AddItem(nil, 2, 0, false). + AddItem(endpointURLFieldWidget, 2, 0, false). + AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). + AddItem(nil, 1, 0, false) + + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) + + return layout +} + +// --- Page 3: Credentials Page --- +// +// Function to build the credentials page. Allows users to enter their cloud storage credentials. +// If the storage protocol is "s3", it provides input fields for access key, secret key. +// If the storage protocol is "azure", it provides input fields for account name, account key, and container name. +func (tui *appContext) buildCredentialsPage() tview.Primitive { + layout := tview.NewFlex() + layout.Clear() + + // Determine labels for input fields based on storage protocol. + accessLabel := "" + secretLabel := "" + if tui.config.storageProtocol == "azstorage" { + accessLabel = "🔑 Account Name: " + secretLabel = "🔑 Account Key: " + } else { + accessLabel = "🔑 Access Key: " + secretLabel = "🔑 Secret Key: " + } + + instructionsText := fmt.Sprintf( + "[%s::b] Enter Your Cloud Storage Credentials[-]\n", + colorGreen, + ) + + fmt.Sprintf( + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n", + colorYellow, + ) + + fmt.Sprintf( + "[%s::b] -%s[-::-] This is your unique identifier for accessing your cloud storage.\n", + colorYellow, + strings.Trim(accessLabel, "🔑 "), + ) + + fmt.Sprintf( + "[%s::b] -%s[-::-] This is your secret password for accessing your cloud storage.\n", + colorYellow, + strings.Trim(secretLabel, "🔑 "), + ) + + fmt.Sprintf( + "[%s::b] -Passphrase:[-::-] This is used to encrypt your configuration file.\n", + colorYellow, + ) + + if tui.config.storageProtocol == "azstorage" { + instructionsText += fmt.Sprintf( + "[%s::b] -Container Name:[-::-] This is the name of your Azure Blob Storage container.\n", + colorYellow, + ) + } + + instructionsText += "\n[darkmagenta::i]\t\t\t*Keep these credentials secure. Do not share.[-]" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetWrap(true). + SetDynamicColors(true). + SetText(instructionsText) + + // Access key field widget + accessKeyFieldWidget := tview.NewInputField(). + SetLabel(accessLabel). + SetText(tui.config.accessKey). + SetFieldWidth(50). + SetChangedFunc(func(key string) { + tui.config.accessKey = key + tui.config.accountName = key + }). + SetPlaceholder("\t\t\t\t"). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Secret key field widget with masked input + secretKeyFieldWidget := tview.NewInputField(). + SetLabel(secretLabel). + SetText(string(tui.config.secretKey)). + SetFieldWidth(50). + SetChangedFunc(func(key string) { + tui.config.secretKey = key + tui.config.accountKey = key + }). + SetPlaceholder("\t\t\t\t"). + SetMaskCharacter('*'). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Container name field widget for Azure storage + containerNameFieldWidget := tview.NewInputField(). + SetLabel("🪣 Container Name: "). + SetText(tui.config.containerName). + SetPlaceholder("\t\t\t\t"). + SetChangedFunc(func(name string) { + tui.config.containerName = name + tui.config.bucketName = name + }). + SetFieldWidth(50). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Passphrase field widget for config file encryption + passphraseFieldWidget := tview.NewInputField(). + SetLabel("🔒 Passphrase: "). + SetText(tui.config.configEncryptionPassphrase). + SetFieldWidth(50). + SetChangedFunc(func(passphrase string) { + tui.config.configEncryptionPassphrase = strings.TrimSpace(passphrase) + }). + SetPlaceholder("\t\t\t "). + SetMaskCharacter('*'). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") + }). + AddButton(tui.theme.navigationNextLabel, func() { + // TODO: Add validation for access key and secret key HERE + // For now, just check that they are not empty + if (tui.config.storageProtocol == "s3storage" && (len(tui.config.accessKey) == 0 || len(tui.config.secretKey) == 0)) || + (tui.config.storageProtocol == "azstorage" && (len(tui.config.accountName) == 0 || len(tui.config.accountKey) == 0 || len(tui.config.containerName) == 0)) || + len(tui.config.configEncryptionPassphrase) == 0 { + tui.showErrorModal( + "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", + func() { + tui.pages.SwitchToPage("page3") + }, + ) + return + } + // TODO: Fix bug here where calling listBuckets() in the checkCredentials() function + // causes the layout to shift upwards and the widgets to be misaligned if the user incorrectly + // enters credentials. + if err := tui.checkCredentials(); err != nil { + tui.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { + tui.pages.RemovePage("page3") // Remove the current page + page3 := tui.buildCredentialsPage() // Rebuild the page + tui.pages.AddPage("page3", page3, true, false) // Add the new page + tui.pages.SwitchToPage("page3") + }) + return + } + + if tui.config.storageProtocol == "azstorage" { + tui.pages.RemovePage("page4") // Remove previous page if it exists + tui.pages.SwitchToPage("page5") + } else { + page4 := tui.buildBucketSelectionPage() + tui.pages.AddPage("page4", page4, true, false) + tui.pages.SwitchToPage("page4") + } + }). + AddButton(tui.theme.navigationBackLabel, func() { + if tui.config.storageProvider == "Microsoft" || tui.config.storageProvider == "AWS" { + tui.pages.RemovePage("page2") + tui.pages.SwitchToPage("page1") + } else { + page2 := tui.buildEndpointURLPage() + tui.pages.AddPage("page2", page2, true, false) + tui.pages.SwitchToPage("page2") + } + }). + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page3") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") + }). + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() + }). + SetLabelColor(tui.theme.widgetLabelColor). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) + + // Combine all credential widgets into a single form + credentialsWidget := tview.NewForm(). + AddFormItem(accessKeyFieldWidget). + AddFormItem(secretKeyFieldWidget). + AddFormItem(passphraseFieldWidget). + SetFieldTextColor(tcell.ColorBlack). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor) + + // If Azure is selected, add the container name field + if tui.config.storageProvider == "Microsoft" { + credentialsWidget.AddFormItem(containerNameFieldWidget) + } + + // Assemble page layout + layout.SetDirection(tview.FlexRow) + layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) + layout.AddItem(nil, 1, 0, false) + layout.AddItem(credentialsWidget, credentialsWidget.GetFormItemCount()*2+1, 0, false) + layout.AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false) + layout.AddItem(nil, 1, 0, false) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) + + return layout +} + +// --- Page 4: Bucket Name Selection --- +// +// Function to build the bucket selection page. Allows users to select a bucket from a dropdown list +// of retrieved buckets based on provided s3 credentials. For s3 storage users only. Azure storage users will skip this page. +func (tui *appContext) buildBucketSelectionPage() tview.Primitive { + instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + + "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + + "[grey::i] The list of available buckets is retrieved from your cloud storage provider\n " + + "based on the credentials provided in the previous step.[-::-]" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetWrap(true). + SetDynamicColors(true). + SetText(instructionsText) + + // Dropdown widget for selecting bucket name + bucketSelectionWidget := tview.NewDropDown(). + SetLabel(" 🪣 Bucket Name: "). + SetOptions(tui.config.bucketList, func(name string, index int) { + tui.config.bucketName = name + }). + SetCurrentOption(0). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). + SetFieldWidth(25) + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm(). + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") + }). + AddButton(tui.theme.navigationNextLabel, func() { + tui.pages.SwitchToPage("page5") + }). + AddButton(tui.theme.navigationBackLabel, func() { + tui.pages.SwitchToPage("page3") + }). + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page4") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") + }). + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() + }). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(tui.theme.navigationButtonTextColor) + + // Assemble page layout + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). + AddItem(nil, 2, 0, false). + AddItem(bucketSelectionWidget, 2, 0, false). + AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). + AddItem(nil, 1, 0, false) + + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) + + return layout +} + +// --- Page 5: Caching Settings --- +// +// Function to build the caching page that allows users to configure caching settings. +// Includes options for enabling/disabling caching, specifying cache location, size, and retention settings. +func (tui *appContext) buildCachingPage() tview.Primitive { + // Main layout container. Must be instantiated first to allow nested items. + layout := tview.NewFlex().SetDirection(tview.FlexRow) + + instructionsText := "[#6EBE49::b] Configure Caching Settings[-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + + "[white::b] CloudFuse can cache data locally. You control the location, size, and duration.[-::-]\n\n" + + "[#FFD700::b] -[-::-] [#6EBE49::b]Enable[-::-] caching if you frequently re-read data and have ample disk space.\n" + + "[#FFD700::b] -[-::-] [red::b]Disable[-::-] caching if you prefer faster initial access or have limited disk space.\n\n" + + // Instructions text widget + instructionsTextWidget := tview.NewTextView(). + SetWrap(true). + SetDynamicColors(true). + SetText(instructionsText) + + // Dropdown widget for enabling/disabling caching + cacheLocationFieldWidget := tview.NewInputField(). + SetLabel("📁 Cache Location: "). + SetText(tui.config.cacheLocation). + SetFieldWidth(40). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). + SetChangedFunc(func(text string) { + tui.config.cacheLocation = text + }) + + // Input field widget for cache size percentage + cacheSizeFieldWidget := tview.NewInputField(). + SetLabel("📊 Cache Size (%): "). + SetText(tui.config.cacheSize). // Default to 80% + SetFieldWidth(4). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack). + SetChangedFunc(func(size string) { + if size, err := strconv.Atoi(size); err != nil || size < 1 || size > 100 { + tui.showErrorModal( + "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", + func() { + tui.pages.SwitchToPage("page5") + }, + ) + return + } + tui.config.cacheSize = size + }) + + // Input field widget for cache retention duration + cacheRetentionDurationFieldWidget := tview.NewInputField(). + SetLabel("⌛ Cache Retention Duration: "). + SetText(fmt.Sprintf("%d", tui.config.cacheRetentionDuration)). + SetFieldWidth(5). + SetChangedFunc(func(text string) { + if val, err := strconv.Atoi(text); err == nil { + tui.config.cacheRetentionDuration = val + } else { + // TODO: Handle invalid input + tui.config.cacheRetentionDuration = 0 + } + }). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Dropdown widget for cache retention unit + cacheRetentionUnitDropdownWidget := tview.NewDropDown(). + SetOptions([]string{"Seconds", "Minutes", "Hours", "Days"}, func(option string, index int) { + tui.config.cacheRetentionUnit = option + // Convert cache retention duration to seconds + switch tui.config.cacheRetentionUnit { + case "Seconds": + tui.config.cacheRetentionDurationSec = tui.config.cacheRetentionDuration + case "Minutes": + minutes := tui.config.cacheRetentionDuration + tui.config.cacheRetentionDurationSec = minutes * 60 + case "Hours": + hours := tui.config.cacheRetentionDuration + tui.config.cacheRetentionDurationSec = hours * 3600 + case "Days": + days := tui.config.cacheRetentionDuration + tui.config.cacheRetentionDurationSec = days * 86400 + } + }). + SetCurrentOption(3). // Default to Days + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Dropdown widget for enabling/disabling cache cleanup on restart + // If enabled --> allow-non-empty-temp: false + // if disabled --> allow-non-empty-temp: true + clearCacheOnStartDropdownWidget := tview.NewDropDown(). + SetLabel("🧹 Clear Cache On Start: "). + SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { + if option == " Enabled " { + tui.config.clearCacheOnStart = true + } else { + tui.config.clearCacheOnStart = false + } + }).SetCurrentOption(0). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(colorBlack) + + // Horizontal container to place retention duration and unit side by side + cacheRetentionRow := tview.NewFlex(). + SetDirection(tview.FlexColumn). + AddItem(cacheRetentionDurationFieldWidget, 35, 0, false). + AddItem(cacheRetentionUnitDropdownWidget, 7, 0, false) + + // Group cache field widgets in a container + cacheFields := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(cacheLocationFieldWidget, 2, 0, false). + AddItem(cacheSizeFieldWidget, 2, 0, false). + AddItem(cacheRetentionRow, 2, 0, false). + AddItem(clearCacheOnStartDropdownWidget, 2, 0, false) + + // Tracks whether or not cache fields are currently shown + showCacheFields := true + + // Navigation buttons widget + navigationButtonsWidget := tview.NewForm() + navigationButtonsWidget. + AddButton(tui.theme.navigationHomeLabel, func() { + tui.pages.SwitchToPage("home") + }). + AddButton(tui.theme.navigationFinishLabel, func() { + // Check if caching is enabled and validate cache settings + if tui.config.enableCaching { + // Validate the cache location + if err := tui.validateCachePath(); err != nil { + tui.showErrorModal("Invalid cache location:\n"+err.Error(), func() { + tui.pages.SwitchToPage("page5") + }) + return + } + + // Check available cache size + if err := tui.getAvailableCacheSize(); err != nil { + tui.showErrorModal( + "Failed to check available cache size:\n"+err.Error(), + func() { + tui.pages.SwitchToPage("page5") + }, + ) + return + } + + cacheSizeText := fmt.Sprintf( + "Available Disk Space @ Cache Location: [darkred::b]%d GB[-::-]\n", + tui.config.availableCacheSizeGB, + ) + + fmt.Sprintf( + "Cache Size Currently Set to: [darkred::b]%.0f GB (%s%%)[-::-]\n\n", + float64(tui.config.currentCacheSizeGB), + tui.config.cacheSize, + ) + + "Would you like to proceed with this cache size?\n\n" + + "If not, hit [darkred::b]Return[-::-] to adjust cache size accordingly. Otherwise, hit [darkred::b]Finish[-::-] to complete the configuration." + + tui.showCacheConfirmationModal(cacheSizeText, + // Callback function if the user selects Finish + func() { + if err := tui.createYAMLConfig(); err != nil { + tui.showErrorModal( + "Failed to create YAML config:\n"+err.Error(), + func() { + tui.pages.SwitchToPage("page5") + }, + ) + return + } + tui.showExitModal(func() { + tui.app.Stop() + }) + }, + // Callback function if the user selects Return + func() { + tui.pages.SwitchToPage("page5") + }) + + } else { + // If caching is disabled, just finish the configuration + if err := tui.createYAMLConfig(); err != nil { + tui.showErrorModal("Failed to create YAML config:\n"+err.Error(), func() { + tui.pages.SwitchToPage("page5") + }) + return + } + tui.showExitModal(func() { + tui.app.Stop() + }) + } + }). + AddButton(tui.theme.navigationBackLabel, func() { + if tui.config.storageProtocol == "azstorage" { + tui.pages.SwitchToPage("page3") + } else { + page4 := tui.buildBucketSelectionPage() + tui.pages.AddPage("page4", page4, true, false) + tui.pages.SwitchToPage("page4") + } + }). + AddButton(tui.theme.navigationPreviewLabel, func() { + previewPage := tui.buildPreviewPage("page5") + tui.pages.AddPage("previewPage", previewPage, true, false) + tui.pages.SwitchToPage("previewPage") + }). + AddButton(tui.theme.navigationQuitLabel, func() { + tui.app.Stop() + }). + SetButtonBackgroundColor(tui.theme.navigationButtonColor). + SetButtonTextColor(colorBlack) + + // Widget to enable/disable caching + enableCachingDropdownWidget := tview.NewDropDown() + enableCachingDropdownWidget. + SetLabel("💾 Caching: "). + SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { + if option == " Enabled " { + tui.config.cacheMode = "file_cache" + tui.config.enableCaching = true + if !showCacheFields { + layout.RemoveItem(navigationButtonsWidget) + layout.RemoveItem(cacheFields) + layout.AddItem(cacheFields, 8, 0, false) + layout.AddItem( + navigationButtonsWidget, + tui.theme.navigationWidgetHeight, + 0, + false, + ) + showCacheFields = true + } + } else { + tui.config.cacheMode = "block_cache" + tui.config.enableCaching = false + if showCacheFields { + layout.RemoveItem(cacheFields) + showCacheFields = false + } + } + }). + SetCurrentOption(0). + SetLabelColor(tui.theme.widgetLabelColor). + SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). + SetFieldTextColor(tcell.ColorBlack) + + // Assemble page layout + layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) + layout.AddItem(enableCachingDropdownWidget, 2, 0, false) + + if showCacheFields { + layout.AddItem(cacheFields, 8, 0, false) + } + + layout.AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false) + layout.AddItem(nil, 1, 0, false) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) + + return layout +} + +// --- Summary Page --- +// +// Function to build the summary page that displays the configuration summary. +// This function creates a text view with the summary information and a return button. +// The preview page parameter allows switching back to the previous page when the user clicks "Return". +func (tui *appContext) buildPreviewPage(previewPage string) tview.Primitive { + summaryText := + "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n" + + "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]" + + fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", tui.config.storageProvider) + + fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", tui.config.endpointURL) + + fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", tui.config.bucketName) + + fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", tui.config.cacheMode) + + fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", tui.config.cacheLocation) + + fmt.Sprintf( + " Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", + tui.config.cacheSize, + tui.config.currentCacheSizeGB, + ) + + // Display cache retention duration in seconds and specified unit + if tui.config.cacheRetentionUnit == "Seconds" { + summaryText += fmt.Sprintf( + " Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", + tui.config.cacheRetentionDurationSec, + ) + } else { + summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", + tui.config.cacheRetentionDurationSec, tui.config.cacheRetentionDuration, tui.config.cacheRetentionUnit) + } + + // Set a dynamic width and height for the summary widget + summaryWidgetHeight := getTextHeight(summaryText) + summaryWidgetWidth := getTextWidth(summaryText) / 3 + + summaryWidget := tview.NewTextView(). + SetWrap(true). + SetDynamicColors(true). + SetText(summaryText). + SetScrollable(true) + + returnButton := tview.NewButton("[black]Return[-]"). + SetSelectedFunc(func() { + tui.pages.SwitchToPage(previewPage) + }) + returnButton.SetBackgroundColor(colorGreen) + returnButton.SetBorder(true) + returnButton.SetBorderColor(colorYellow) + returnButton.SetBackgroundColorActivated(colorGreen) + + buttons := tview.NewFlex(). + SetDirection(tview.FlexColumn). + AddItem(nil, 0, 1, false). // Left button spacer + AddItem(returnButton, 20, 0, true). + AddItem(nil, 0, 1, false) // Right button spacer + + modal := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(summaryWidget, summaryWidgetHeight, 0, false). + AddItem(nil, 1, 0, false). + AddItem(buttons, 3, 0, true) + + leftAlignedModal := tview.NewFlex(). + AddItem(modal, summaryWidgetWidth, 0, true) + + leftAlignedModal.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) + + return leftAlignedModal +} + +// Function to show a modal dialog with a message and an "OK" button. +// This function is used to display error messages or confirmations. +// May specify a callback function to execute when the modal is closed. +func (tui *appContext) showErrorModal(message string, onClose func()) { + modal := tview.NewModal(). + SetText(message). + AddButtons([]string{"OK"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + tui.pages.RemovePage("modal") + onClose() + }). + SetBackgroundColor(colorGreen). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(colorYellow) + modal.SetButtonBackgroundColor(colorYellow) + modal.SetButtonTextColor(tcell.ColorBlack) + tui.pages.AddPage("modal", modal, false, true) +} + +// Function to show a confirmation modal dialog with "Finish" and "Return" buttons. +// Used to confirm cache size before proceeding. Must specify two callback functions for the "Finish" and "Return" actions. +func (tui *appContext) showCacheConfirmationModal( + message string, + onFinish func(), + onReturn func(), +) { + modal := tview.NewModal(). + SetText(message). + AddButtons([]string{"Finish", "Return"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + tui.pages.RemovePage("modal") + if buttonLabel == "Finish" { + onFinish() + } else { + onReturn() + } + }). + SetBackgroundColor(colorGreen). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(colorYellow) + modal.SetButtonBackgroundColor(colorYellow) + modal.SetButtonTextColor(tcell.ColorBlack) + tui.pages.AddPage("modal", modal, true, true) +} + +// Function to show final exit modal when configuration is complete. +// Informs the user that the configuration is complete and they can exit. +// This function is called when the user clicks "Finish" on the caching page. +func (tui *appContext) showExitModal(onConfirm func()) { + + processingEmojis := []string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "✅"} + + modal := tview.NewModal(). + AddButtons([]string{"Exit"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + tui.pages.RemovePage("modal") + if buttonLabel == "Exit" { + onConfirm() + } + }). + SetBackgroundColor(colorGreen). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(colorYellow) + modal.SetButtonBackgroundColor(colorYellow) + modal.SetButtonTextColor(tcell.ColorBlack) + + tui.pages.AddPage("modal", modal, true, true) + + // Simulate processing with emoji animation + go func() { + // Show initial message with emoji animation + for i := 0; i < len(processingEmojis); i++ { + currentEmoji := processingEmojis[i] + time.Sleep(100 * time.Millisecond) + tui.app.QueueUpdateDraw(func() { + modal.SetText( + fmt.Sprintf( + "[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", + currentEmoji, + ), + ) + }) + } + + // After animation, show final message + tui.app.QueueUpdateDraw(func() { + modal.SetText(fmt.Sprintf("[#6EBE49::b]Configuration Complete![-::-]\n\n%s\n\n"+ + "Your CloudFuse configuration file has been created at:\n\n[blue:white:b]%s[-:-:-]\n\n"+ + "You can now exit the application.\n\n"+ + "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], tui.config.configFilePath)) + }) + }() +} + +// Helper function to center lines of text within a specified width. +// It is used to format text views and other UI elements in the TUI. +func centerText(text string, width int) string { + var centeredLines []string + lines := strings.Split(text, "\n") + for _, line := range lines { + visibleLen := tview.TaggedStringWidth(line) // handle color tags + if visibleLen >= width { + centeredLines = append(centeredLines, line) + } else { + padding := (width - visibleLen) / 2 + centeredLines = append(centeredLines, strings.Repeat(" ", padding)+line) + } + } + return strings.Join(centeredLines, "\n") +} + +// Helper function to get the length of the longest line in a string. +// It is used to determine the width of text views and other UI elements. +func getTextWidth(s string) int { + if s == "" { + return 0 + } + lines := strings.Split(s, "\n") + longest := 0 + for _, line := range lines { + if len(line) > longest { + longest = len(line) + } + } + return longest +} + +// Helper function to count the number of lines in a string. +// It is used to determine the height of text views and other UI elements. +func getTextHeight(s string) int { + if s == "" { + return 0 + } + return len(strings.Split(s, "\n")) +} + +// Helper function to get a fallback cache path if the home directory cannot be determined. +func getFallbackCachePath() string { + user := os.Getenv("USER") + if user == "" { + uid := os.Getuid() + user = fmt.Sprintf("uid_%d", uid) + } + return filepath.Join(os.TempDir(), "cloudfuse", user) +} + +// Helper function to get the default cache path. +// It retrieves the user's home directory and constructs a default cache path: +// +// `~/.cloudfuse/file_cache`. If it fails to retrieve the home directory or create the path, it returns a fallback path. +func getDefaultCachePath() string { + // TODO: Add logic to return OS-specific cache paths + home, err := os.UserHomeDir() + if err != nil { + fmt.Printf( + "[red::b]ERROR: Failed to get home directory: %v\nUsing fallback path for cache directory.\n", + err, + ) + return getFallbackCachePath() + } + cachePath := filepath.Join(home, ".cloudfuse", "file_cache") + // If the directory doesn't exist, create it + if _, err := os.Stat(cachePath); os.IsNotExist(err) { + if err := os.MkdirAll(cachePath, 0700); err != nil { + fmt.Printf( + "[red::b]ERROR: Failed to create cache directory: %v\nUsing fallback path for cache directory.\n", + err, + ) + return getFallbackCachePath() + } + } + // Return the full path to the cache directory + return cachePath +} + +// Helper function to validate the entered cache path. +func (tui *appContext) validateCachePath() error { + // Validate that the path is not empty + if strings.TrimSpace(tui.config.cacheLocation) == "" { + return fmt.Errorf("[red::b]ERROR: Cache location cannot be empty[-::-]") + } + // Make sure no invalid path characters are used + if strings.ContainsAny(tui.config.cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { + return fmt.Errorf("[red::b]ERROR: Cache location contains invalid characters[-::-]") + } + // Validate that the cache path exists + if tui.config.cacheLocation != getDefaultCachePath() && tui.config.cacheMode == "file_cache" { + if _, err := os.Stat(tui.config.cacheLocation); os.IsNotExist(err) { + return fmt.Errorf( + "[red::b]ERROR: '%s': No such file or directory[-::-]", + tui.config.cacheLocation, + ) + } + } + return nil +} + +// Helper function to get the available disk space at the cache location and calculates +// the cache size in GB based on the user-defined cache size percentage. +func (tui *appContext) getAvailableCacheSize() error { + availableBlocks, _, err := common.GetAvailFree(tui.config.cacheLocation) + if err != nil { + // If we fail to get the available cache size, we default to 80% of the available disk space + tui.config.cacheSize = "80" + returnMsg := fmt.Errorf( + "[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ + "Defaulting cache size to 80%% of available disk space.\n\n"+ + "Please manually verify you have enough disk space available for caching.[-::-]", + tui.config.cacheLocation, + err, + ) + return returnMsg + } + + const blockSize = 4096 + availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes + tui.config.availableCacheSizeGB = int( + availableCacheSizeBytes / (1024 * 1024 * 1024), + ) // Convert to GB + cacheSizeInt, _ := strconv.Atoi(tui.config.cacheSize) + tui.config.currentCacheSizeGB = int(tui.config.availableCacheSizeGB) * cacheSizeInt / 100 + + return nil +} + +// Helper function to normalize and validate the user-defined endpoint URL. +func (tui *appContext) validateEndpointURL(rawURL string) error { + rawURL = strings.TrimSpace(rawURL) + + // Check if the URL is empty + if strings.TrimSpace(rawURL) == "" { + return fmt.Errorf("[red::b]Endpoint URL cannot be empty[-::-]\nPlease try again.") + } + + // Normalize the URL by adding "https://" if it doesn't start with "http://" or "https://" + if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { + tui.config.endpointURL = "https://" + rawURL + return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n" + + "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") + } + + if _, err := url.ParseRequestURI(rawURL); err != nil { + return fmt.Errorf("[red::b]Invalid URL format[-::-]\n%s\nPlease try again.", err.Error()) + } + + return nil +} + +// Function to check the credentials entered by the user. +// Attempts to connect to the storage backend and fetch the bucket list. +// If successful, populates the `bucketList` variable with the list of available buckets (for s3 providers only). +// Called when the user clicks "Next" on the credentials page. +func (tui *appContext) checkCredentials() error { + // Create a temporary configOptions struct with only the storage component + tmpConfig := configOptions{ + Components: []string{tui.config.storageProtocol}, + } + + if tui.config.storageProtocol == "azstorage" { + tmpConfig.AzStorage = azstorage.AzStorageOptions{ + AccountType: "block", + AccountName: tui.config.accountName, + AccountKey: tui.config.accountKey, + AuthMode: "key", + Container: tui.config.containerName, + } + } else { + tmpConfig.S3Storage = s3StorageConfig{ + BucketName: tui.config.bucketName, + KeyID: tui.config.accessKey, + SecretKey: tui.config.secretKey, + Endpoint: tui.config.endpointURL, + EnableDirMarker: true, + } + } + + // Marshal the temporary struct to YAML format + tmpConfigData, _ := yaml.Marshal(&tmpConfig) + + // Write the temporary config data into the global options struct instead of a temporary file. + // This avoids the need to create and delete a temporary file on disk. + if err := config.ReadFromConfigBuffer(tmpConfigData); err != nil { + return fmt.Errorf("Failed to read config from buffer: %v", err) + } + + if err := config.Unmarshal(&options); err != nil { + return fmt.Errorf("Failed to unmarshal config: %v", err) + } + + // Try to fetch bucket list + var err error + if slices.Contains(options.Components, "azstorage") { + tui.config.bucketList, err = getContainerListAzure() + + } else if slices.Contains(options.Components, "s3storage") { + tui.config.bucketList, err = getBucketListS3() + + } else { + err = fmt.Errorf("Unsupported storage backend") + } + + if err != nil { + return fmt.Errorf("Failed to get bucket list: %v", err) + } + + return nil +} + +// Function to create the YAML configuration file based on user inputs once all forms are completed. +// Called when the user clicks "Finish" on the caching page. +func (tui *appContext) createYAMLConfig() error { + config := configOptions{ + Components: []string{ + "libfuse", + tui.config.cacheMode, + "attr_cache", + tui.config.storageProtocol, + }, + + Libfuse: libfuse.LibfuseOptions{ + NetworkShare: true, + }, + + AttrCache: attr_cache.AttrCacheOptions{ + Timeout: uint32(7200), + }, + } + + if tui.config.cacheMode == "file_cache" { + config.FileCache = file_cache.FileCacheOptions{ + TmpPath: tui.config.cacheLocation, + Timeout: uint32(tui.config.cacheRetentionDurationSec), + AllowNonEmpty: !tui.config.clearCacheOnStart, + SyncToFlush: true, + } + // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it + if tui.config.cacheSize != "80" { + config.FileCache.MaxSizeMB = float64( + tui.config.currentCacheSizeGB * 1024, + ) // Convert GB to MB + } + } + + if tui.config.storageProtocol == "s3storage" { + config.S3Storage = s3StorageConfig{ + BucketName: tui.config.bucketName, + KeyID: tui.config.accessKey, + SecretKey: tui.config.secretKey, + Endpoint: tui.config.endpointURL, + EnableDirMarker: true, + } + } else { + config.AzStorage = azstorage.AzStorageOptions{ + AccountType: "block", + AccountName: tui.config.accountName, + AccountKey: tui.config.accountKey, + AuthMode: "key", + Container: tui.config.containerName, + } + } + + // Marshal the struct to YAML format + configData, err := yaml.Marshal(&config) + if err != nil { + return fmt.Errorf("Failed to marshal configuration data to YAML: %v", err) + } + + // Encrypt the YAML config data using the user-provided passphrase + encryptedPassphrase := memguard.NewEnclave([]byte(tui.config.configEncryptionPassphrase)) + cipherText, err := common.EncryptData(configData, encryptedPassphrase) + if err != nil { + return fmt.Errorf("Failed to encrypt configuration data: %v", err) + } + + // Write the encrypted YAML config data to a file + if err := os.WriteFile("config.aes", cipherText, 0600); err != nil { + return fmt.Errorf("Failed to create encrypted config.aes file: %v", err) + } + + // Update configFilePath member to point to the created config file + currDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("Error: %v", err) + } + + tui.config.configFilePath = filepath.Join(currDir, "config.aes") + + return nil +} diff --git a/cmd/tui.go b/cmd/tui.go deleted file mode 100644 index b954f7c61..000000000 --- a/cmd/tui.go +++ /dev/null @@ -1,1462 +0,0 @@ -/* - Licensed under the MIT License . - - Copyright © 2023-2025 Seagate Technology LLC and/or its Affiliates - Copyright © 2020-2025 Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE -*/ - -package cmd - -import ( - "fmt" - "net/url" - "os" - "path/filepath" - "slices" - "strconv" - "strings" - "time" - - "github.com/Seagate/cloudfuse/common" - "github.com/Seagate/cloudfuse/common/config" - "github.com/Seagate/cloudfuse/component/attr_cache" - "github.com/Seagate/cloudfuse/component/azstorage" - "github.com/Seagate/cloudfuse/component/file_cache" - "github.com/Seagate/cloudfuse/component/libfuse" - "github.com/awnumar/memguard" - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" - "gopkg.in/yaml.v3" -) - -// Top-level struct to hold application context, including tview application instance, -// page stack, user configuration data, and UI theme settings. -type appContext struct { - app *tview.Application - pages *tview.Pages - config *userConfig - theme *uiTheme -} - -// Struct to hold user configuration data collected from the TUI session. -type userConfig struct { - configEncryptionPassphrase string // Sets config file encryption passphrase - configFilePath string // Sets file_cache.path - accountName string // Sets azstorage.account-name - accountKey string // Sets azstorage.account-key - accessKey string // Sets s3storage.key-id - secretKey string // Sets s3storage.secret-key - containerName string // Sets azstorage.container-name - bucketName string // Sets s3storage.bucket-name - endpointURL string // Sets s3storage.endpoint - bucketList []string // Holds list of available buckets retrieved from cloud provider (for s3 only). - storageProtocol string // Sets 's3storage' or 'azstorage' based on selected provider - storageProvider string // Options: 'LyveCloud', 'Microsoft', 'AWS', or 'Other (s3)'. Used to set certain UI elements. - cacheMode string // Sets 'components' to include 'file_cache' or 'block_cache' - enableCaching bool // If true, sets cacheMode to file_cache. If false, block_cache - cacheLocation string // Sets file_cache.path @ startup to default: $HOME/.cloudfuse/cache - cacheSize string // User-defined cache size as % - availableCacheSizeGB int // Total available cache size in GB @ the cache location - currentCacheSizeGB int // Current cache size in GB based on 'cacheSize' percentage - clearCacheOnStart bool // If false, sets 'allow-non-empty-temp' to true - cacheRetentionDuration int // User-defined cache retention duration. Default is '2' - cacheRetentionUnit string // User-defined cache retention unit (sec, min, hours, days). Default is 'days' - cacheRetentionDurationSec int // Sets 'file_cache.timeout-sec' from 'cacheRetentionDuration' -} - -// Struct to hold UI theme settings, including colors and labels for various widgets. -type uiTheme struct { - widgetLabelColor tcell.Color - widgetFieldBackgroundColor tcell.Color - navigationButtonColor tcell.Color - navigationButtonTextColor tcell.Color - navigationStartLabel string - navigationHomeLabel string - navigationNextLabel string - navigationBackLabel string - navigationPreviewLabel string - navigationQuitLabel string - navigationFinishLabel string - navigationWidgetHeight int -} - -// Global general purpose vars -var ( - colorYellow = tcell.GetColor("#FFD700") - colorGreen = tcell.GetColor("#6EBE49") - colorBlack = tcell.ColorBlack -) - -// Struct to hold the final configuration data to be written to the YAML config file. -type configOptions struct { - Components []string `yaml:"components,omitempty"` - Libfuse libfuse.LibfuseOptions `yaml:"libfuse,omitempty"` - FileCache file_cache.FileCacheOptions `yaml:"file_cache,omitempty"` - AttrCache attr_cache.AttrCacheOptions `yaml:"attr_cache,omitempty"` - S3Storage s3StorageConfig `yaml:"s3storage,omitempty"` - AzStorage azstorage.AzStorageOptions `yaml:"azstorage,omitempty"` -} - -// Struct to hold s3 storage configuration options for the YAML config file. -// TODO: change to using s3storage.Options from component/s3storage/config.go -type s3StorageConfig struct { - BucketName string `yaml:"bucket-name,omitempty"` - KeyID string `yaml:"key-id"` - SecretKey string `yaml:"secret-key"` - Endpoint string `yaml:"endpoint"` - EnableDirMarker bool `yaml:"enable-dir-marker"` -} - -// Constructor for appContext struct. Initializes default values for userConfig and uiTheme. -func newAppContext() *appContext { - return &appContext{ - app: tview.NewApplication(), - pages: tview.NewPages(), - config: &userConfig{ - enableCaching: true, - cacheLocation: getDefaultCachePath(), - cacheSize: "80", - cacheRetentionDuration: 2, - clearCacheOnStart: false, - }, - theme: &uiTheme{ - widgetLabelColor: colorYellow, - widgetFieldBackgroundColor: colorYellow, - navigationButtonColor: colorGreen, - navigationButtonTextColor: colorBlack, - navigationStartLabel: "[black]🚀 Start[-]", - navigationHomeLabel: "[black]🏠 Home[-]", - navigationNextLabel: "[black]🡲 Next[-]", - navigationBackLabel: "[black]🡰 Back[-]", - navigationPreviewLabel: "[black]📄 Preview[-]", - navigationQuitLabel: "[black]❌ Quit[-]", - navigationFinishLabel: "[black]✅ Finish[-]", - navigationWidgetHeight: 3, - }, - } -} - -// Main function to run the TUI application. -// Initializes the tview application, builds the TUI application, and runs it. -func (tui *appContext) runTUI() error { - tui.app.EnableMouse(true) - tui.app.EnablePaste(true) - - tui.buildTUI() - - // Run the application - if err := tui.app.Run(); err != nil { - panic(err) - } - - return nil -} - -// Function to build the TUI application. Initializes the pages and adds them to the page stack. -func (tui *appContext) buildTUI() { - - // Initialize the pages - homePage := tui.buildHomePage() // --- Home Page --- - page1 := tui.buildStorageProviderPage() // --- Page 1: Storage Provider Selection --- - page2 := tui.buildEndpointURLPage() // --- Page 2: Endpoint URL Entry --- - page3 := tui.buildCredentialsPage() // --- Page 3: Credentials Entry --- - page4 := tui.buildBucketSelectionPage() // --- Page 4: Bucket Selection --- - page5 := tui.buildCachingPage() // --- Page 5: Caching Settings --- - - // Add pages to the page stack - tui.pages.AddPage("home", homePage, true, true) - tui.pages.AddPage("page1", page1, true, false) - tui.pages.AddPage("page2", page2, true, false) - tui.pages.AddPage("page3", page3, true, false) - tui.pages.AddPage("page4", page4, true, false) - tui.pages.AddPage("page5", page5, true, false) - - tui.app.SetRoot(tui.pages, true) -} - -// --- Page 0: Home Page --- -// -// Function to build the home page of the TUI application. Displays a -// welcome banner, instructions, and buttons to start or quit the application. -func (tui *appContext) buildHomePage() tview.Primitive { - bannerText := "[#6EBE49::b]" + - " █▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + - "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + - "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + - "[white::b]Welcome to the CloudFuse Configuration Tool\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + - "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" - - // Banner text widget - bannerTextWidget := tview.NewTextView(). - SetText(centerText(bannerText, 75)). - SetDynamicColors(true). - SetWrap(true) - - instructionsText := "[#FFD700::b]Instructions:[::-]\n" + - "[#6EBE49::b]•[-::-] [::]Use your mouse or arrow keys to navigate.[-::-]\n" + - "[#6EBE49::b]•[-::-] [::]Press Enter or left-click to select items.[-::-]\n" + - "[#6EBE49::b]•[-::-] [::]For the best experience, expand terminal window to full size.[-::-]\n" - - // Instructions text widget - instructionsTextWidget := tview.NewTextView(). - SetText(instructionsText). - SetDynamicColors(true). - SetWrap(true) - - // Start/Quit buttons widget - startQuitButtonsWidget := tview.NewForm(). - AddButton(tui.theme.navigationStartLabel, func() { - tui.pages.SwitchToPage("page1") - }). - AddButton(tui.theme.navigationQuitLabel, func() { - tui.app.Stop() - }). - SetButtonBackgroundColor(tui.theme.navigationButtonColor). - SetButtonTextColor(tui.theme.navigationButtonTextColor) - - aboutText := "[#FFD700::b]ABOUT[-::-]\n" + - "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + - "[grey::i]CloudFuse TUI Configuration Tool\n" + - "Seagate Technology, LLC\n" + - "cloudfuse@seagate.com\n" + - fmt.Sprintf("Version: %s", common.CloudfuseVersion) - - // About text widget - aboutTextWidget := tview.NewTextView(). - SetText(centerText(aboutText, 75)). - SetDynamicColors(true). - SetWrap(true) - - // Assemble page layout - layout := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(bannerTextWidget, getTextHeight(bannerText), 0, false). // Banner Widget - AddItem(nil, 1, 0, false). // Padding - AddItem(startQuitButtonsWidget, 3, 0, false). // Start/Quit buttons widget - AddItem(nil, 1, 0, false). // Padding - AddItem(instructionsTextWidget, 4, 0, false). // Instructions widget - AddItem(nil, 2, 0, false). // Padding - AddItem(aboutTextWidget, 9, 0, false). // About widget - AddItem(nil, 1, 0, false) // Bottom padding - - layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) - - return layout -} - -// --- Page 1: Storage Provider Selection --- -// -// Function to build the storage provider selection page. Allows users to select their cloud storage provider -// from a dropdown list. The options are: LyveCloud, Microsoft, AWS, and Other S3. -func (tui *appContext) buildStorageProviderPage() tview.Primitive { - instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + - "[grey::i] If your provider is not listed, choose [darkmagenta::b]Other (s3)[-::-][grey::i]. You’ll be\n" + - " prompted to enter the endpoint URL and region manually.[-::-]\n" - - // Instructions text widget - instructionsTextWidget := tview.NewTextView(). - SetText(instructionsText). - SetDynamicColors(true). - SetWrap(true) - - // Dropdown widget for selecting storage provider - storageProviderDropdownWidget := tview.NewDropDown(). - SetLabel("📦 Storage Provider: "). - SetOptions([]string{" LyveCloud ⬇️", " Microsoft ", " AWS ", " Other (s3) "}, func(option string, index int) { - tui.config.storageProvider = option - switch option { - case " LyveCloud ⬇️": - tui.config.storageProtocol = "s3storage" - tui.config.storageProvider = "LyveCloud" - case " Microsoft ": - tui.config.storageProtocol = "azstorage" - tui.config.storageProvider = "Microsoft" - case " AWS ": - tui.config.storageProtocol = "s3storage" - tui.config.storageProvider = "AWS" - case " Other (s3) ": - tui.config.storageProtocol = "s3storage" - tui.config.storageProvider = "Other" - tui.config.endpointURL = "" - default: - tui.config.storageProtocol = "s3storage" - tui.config.storageProvider = "LyveCloud" - } - }). - SetCurrentOption(0). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack). - SetFieldWidth(14) - - // Navigation buttons widget - navigationButtonsWidget := tview.NewForm(). - AddButton(tui.theme.navigationHomeLabel, func() { - tui.pages.SwitchToPage("home") - }). - AddButton(tui.theme.navigationNextLabel, func() { - // If Microsoft is selected, switch to page 3 and skip endpoint entry, handled internally by Azure SDK. - if tui.config.storageProvider == "Microsoft" { - page3 := tui.buildCredentialsPage() - tui.pages.AddPage("page3", page3, true, false) - tui.pages.SwitchToPage("page3") - } else { - page2 := tui.buildEndpointURLPage() - tui.pages.AddPage("page2", page2, true, false) - tui.pages.SwitchToPage("page2") - } - }). - AddButton(tui.theme.navigationPreviewLabel, func() { - previewPage := tui.buildPreviewPage("page1") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") - }). - AddButton(tui.theme.navigationQuitLabel, func() { - tui.app.Stop() - }). - SetButtonBackgroundColor(tui.theme.navigationButtonColor). - SetButtonTextColor(tui.theme.navigationButtonTextColor) - - // Assemble page layout - layout := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). - AddItem(nil, 1, 0, false). - AddItem(storageProviderDropdownWidget, 2, 0, false). - AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). - AddItem(nil, 1, 0, false) - - layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) - - return layout -} - -// --- Page 2: Endpoint URL Entry Page --- -// -// Function to build the endpoint URL page. Allows users to enter the endpoint URL for their cloud storage provider. -// It validates the endpoint URL format and provides help text based on the selected provider. -func (tui *appContext) buildEndpointURLPage() tview.Primitive { - var urlRegionHelpText string - - // Determine URL help text based on selected provider - switch tui.config.storageProvider { - case "LyveCloud": - urlRegionHelpText = "[::b]You selected LyveCloud as your storage provider.[::-]\n\n" + - "For LyveCloud, the endpoint URL format is generally:\n" + - "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + - "Example:\n[darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + - "[grey::i]Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown.[-::-]" - urlRegionHelpText = centerText(urlRegionHelpText, 65) - - case "AWS": - urlRegionHelpText = "[::b]You selected AWS as your storage provider.[::-]\n\n" + - "The endpoint URL format is generally:\n" + - "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-]\n\n" + - "Example:\n[darkmagenta::b]https://s3.us-east-1.amazonaws.com[-]\n\n" + - "[grey::i]Refer to AWS documentation for valid formats and available regions.[-::-]" - urlRegionHelpText = centerText(urlRegionHelpText, 65) - - case "Other": - urlRegionHelpText = "[::b]You selected a custom s3 provider.[::-]\n\n" + - "Enter the endpoint URL.\n" + - "[grey::i]Refer to your provider’s documentation for valid formats.[-::-]" - urlRegionHelpText = centerText(urlRegionHelpText, 65) - } - - instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n"+ - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ - "[white]\n %s", tui.config.storageProvider, urlRegionHelpText) - - instructionsTextWidget := tview.NewTextView(). - SetText(instructionsText). - SetWrap(true). - SetDynamicColors(true) - - endpointURLFieldWidget := tview.NewInputField(). - SetLabel("🔗 Endpoint URL: "). - SetText(tui.config.endpointURL). - SetFieldWidth(50). - SetChangedFunc(func(url string) { - tui.config.endpointURL = url - }). - SetPlaceholder("\t\t\t\t"). - SetPlaceholderTextColor(tcell.ColorGray). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Navigation buttons widget - navigationButtonsWidget := tview.NewForm(). - AddButton(tui.theme.navigationHomeLabel, func() { - tui.pages.SwitchToPage("home") - }). - AddButton(tui.theme.navigationNextLabel, func() { - if err := tui.validateEndpointURL(tui.config.endpointURL); err != nil { - tui.showErrorModal( - fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), - func() { - tui.pages.RemovePage("page2") - page2 := tui.buildEndpointURLPage() - tui.pages.AddPage("page2", page2, true, false) - tui.pages.SwitchToPage("page2") - }, - ) - return - } - tui.pages.RemovePage("page3") - page3 := tui.buildCredentialsPage() - tui.pages.AddPage("page3", page3, true, false) - tui.pages.SwitchToPage("page3") - }). - AddButton(tui.theme.navigationBackLabel, func() { - tui.pages.SwitchToPage("page1") - }). - AddButton(tui.theme.navigationPreviewLabel, func() { - previewPage := tui.buildPreviewPage("page2") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") - }). - AddButton(tui.theme.navigationQuitLabel, func() { - tui.app.Stop() - }). - SetButtonBackgroundColor(tui.theme.navigationButtonColor). - SetLabelColor(tui.theme.widgetLabelColor). - SetButtonTextColor(tui.theme.navigationButtonTextColor) - - // Assemble page layout - layout := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). - AddItem(nil, 2, 0, false). - AddItem(endpointURLFieldWidget, 2, 0, false). - AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). - AddItem(nil, 1, 0, false) - - layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) - - return layout -} - -// --- Page 3: Credentials Page --- -// -// Function to build the credentials page. Allows users to enter their cloud storage credentials. -// If the storage protocol is "s3", it provides input fields for access key, secret key. -// If the storage protocol is "azure", it provides input fields for account name, account key, and container name. -func (tui *appContext) buildCredentialsPage() tview.Primitive { - layout := tview.NewFlex() - layout.Clear() - - // Determine labels for input fields based on storage protocol. - accessLabel := "" - secretLabel := "" - if tui.config.storageProtocol == "azstorage" { - accessLabel = "🔑 Account Name: " - secretLabel = "🔑 Account Key: " - } else { - accessLabel = "🔑 Access Key: " - secretLabel = "🔑 Secret Key: " - } - - instructionsText := fmt.Sprintf( - "[%s::b] Enter Your Cloud Storage Credentials[-]\n", - colorGreen, - ) + - fmt.Sprintf( - "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n", - colorYellow, - ) + - fmt.Sprintf( - "[%s::b] -%s[-::-] This is your unique identifier for accessing your cloud storage.\n", - colorYellow, - strings.Trim(accessLabel, "🔑 "), - ) + - fmt.Sprintf( - "[%s::b] -%s[-::-] This is your secret password for accessing your cloud storage.\n", - colorYellow, - strings.Trim(secretLabel, "🔑 "), - ) + - fmt.Sprintf( - "[%s::b] -Passphrase:[-::-] This is used to encrypt your configuration file.\n", - colorYellow, - ) - - if tui.config.storageProtocol == "azstorage" { - instructionsText += fmt.Sprintf( - "[%s::b] -Container Name:[-::-] This is the name of your Azure Blob Storage container.\n", - colorYellow, - ) - } - - instructionsText += "\n[darkmagenta::i]\t\t\t*Keep these credentials secure. Do not share.[-]" - - // Instructions text widget - instructionsTextWidget := tview.NewTextView(). - SetWrap(true). - SetDynamicColors(true). - SetText(instructionsText) - - // Access key field widget - accessKeyFieldWidget := tview.NewInputField(). - SetLabel(accessLabel). - SetText(tui.config.accessKey). - SetFieldWidth(50). - SetChangedFunc(func(key string) { - tui.config.accessKey = key - tui.config.accountName = key - }). - SetPlaceholder("\t\t\t\t"). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Secret key field widget with masked input - secretKeyFieldWidget := tview.NewInputField(). - SetLabel(secretLabel). - SetText(string(tui.config.secretKey)). - SetFieldWidth(50). - SetChangedFunc(func(key string) { - tui.config.secretKey = key - tui.config.accountKey = key - }). - SetPlaceholder("\t\t\t\t"). - SetMaskCharacter('*'). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Container name field widget for Azure storage - containerNameFieldWidget := tview.NewInputField(). - SetLabel("🪣 Container Name: "). - SetText(tui.config.containerName). - SetPlaceholder("\t\t\t\t"). - SetChangedFunc(func(name string) { - tui.config.containerName = name - tui.config.bucketName = name - }). - SetFieldWidth(50). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Passphrase field widget for config file encryption - passphraseFieldWidget := tview.NewInputField(). - SetLabel("🔒 Passphrase: "). - SetText(tui.config.configEncryptionPassphrase). - SetFieldWidth(50). - SetChangedFunc(func(passphrase string) { - tui.config.configEncryptionPassphrase = strings.TrimSpace(passphrase) - }). - SetPlaceholder("\t\t\t "). - SetMaskCharacter('*'). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Navigation buttons widget - navigationButtonsWidget := tview.NewForm(). - AddButton(tui.theme.navigationHomeLabel, func() { - tui.pages.SwitchToPage("home") - }). - AddButton(tui.theme.navigationNextLabel, func() { - // TODO: Add validation for access key and secret key HERE - // For now, just check that they are not empty - if (tui.config.storageProtocol == "s3storage" && (len(tui.config.accessKey) == 0 || len(tui.config.secretKey) == 0)) || - (tui.config.storageProtocol == "azstorage" && (len(tui.config.accountName) == 0 || len(tui.config.accountKey) == 0 || len(tui.config.containerName) == 0)) || - len(tui.config.configEncryptionPassphrase) == 0 { - tui.showErrorModal( - "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", - func() { - tui.pages.SwitchToPage("page3") - }, - ) - return - } - // TODO: Fix bug here where calling listBuckets() in the checkCredentials() function - // causes the layout to shift upwards and the widgets to be misaligned if the user incorrectly - // enters credentials. - if err := tui.checkCredentials(); err != nil { - tui.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { - tui.pages.RemovePage("page3") // Remove the current page - page3 := tui.buildCredentialsPage() // Rebuild the page - tui.pages.AddPage("page3", page3, true, false) // Add the new page - tui.pages.SwitchToPage("page3") - }) - return - } - - if tui.config.storageProtocol == "azstorage" { - tui.pages.RemovePage("page4") // Remove previous page if it exists - tui.pages.SwitchToPage("page5") - } else { - page4 := tui.buildBucketSelectionPage() - tui.pages.AddPage("page4", page4, true, false) - tui.pages.SwitchToPage("page4") - } - }). - AddButton(tui.theme.navigationBackLabel, func() { - if tui.config.storageProvider == "Microsoft" || tui.config.storageProvider == "AWS" { - tui.pages.RemovePage("page2") - tui.pages.SwitchToPage("page1") - } else { - page2 := tui.buildEndpointURLPage() - tui.pages.AddPage("page2", page2, true, false) - tui.pages.SwitchToPage("page2") - } - }). - AddButton(tui.theme.navigationPreviewLabel, func() { - previewPage := tui.buildPreviewPage("page3") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") - }). - AddButton(tui.theme.navigationQuitLabel, func() { - tui.app.Stop() - }). - SetLabelColor(tui.theme.widgetLabelColor). - SetButtonBackgroundColor(tui.theme.navigationButtonColor). - SetButtonTextColor(tui.theme.navigationButtonTextColor) - - // Combine all credential widgets into a single form - credentialsWidget := tview.NewForm(). - AddFormItem(accessKeyFieldWidget). - AddFormItem(secretKeyFieldWidget). - AddFormItem(passphraseFieldWidget). - SetFieldTextColor(tcell.ColorBlack). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor) - - // If Azure is selected, add the container name field - if tui.config.storageProvider == "Microsoft" { - credentialsWidget.AddFormItem(containerNameFieldWidget) - } - - // Assemble page layout - layout.SetDirection(tview.FlexRow) - layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) - layout.AddItem(nil, 1, 0, false) - layout.AddItem(credentialsWidget, credentialsWidget.GetFormItemCount()*2+1, 0, false) - layout.AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false) - layout.AddItem(nil, 1, 0, false) - layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) - - return layout -} - -// --- Page 4: Bucket Name Selection --- -// -// Function to build the bucket selection page. Allows users to select a bucket from a dropdown list -// of retrieved buckets based on provided s3 credentials. For s3 storage users only. Azure storage users will skip this page. -func (tui *appContext) buildBucketSelectionPage() tview.Primitive { - instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + - "[grey::i] The list of available buckets is retrieved from your cloud storage provider\n " + - "based on the credentials provided in the previous step.[-::-]" - - // Instructions text widget - instructionsTextWidget := tview.NewTextView(). - SetWrap(true). - SetDynamicColors(true). - SetText(instructionsText) - - // Dropdown widget for selecting bucket name - bucketSelectionWidget := tview.NewDropDown(). - SetLabel(" 🪣 Bucket Name: "). - SetOptions(tui.config.bucketList, func(name string, index int) { - tui.config.bucketName = name - }). - SetCurrentOption(0). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack). - SetFieldWidth(25) - - // Navigation buttons widget - navigationButtonsWidget := tview.NewForm(). - AddButton(tui.theme.navigationHomeLabel, func() { - tui.pages.SwitchToPage("home") - }). - AddButton(tui.theme.navigationNextLabel, func() { - tui.pages.SwitchToPage("page5") - }). - AddButton(tui.theme.navigationBackLabel, func() { - tui.pages.SwitchToPage("page3") - }). - AddButton(tui.theme.navigationPreviewLabel, func() { - previewPage := tui.buildPreviewPage("page4") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") - }). - AddButton(tui.theme.navigationQuitLabel, func() { - tui.app.Stop() - }). - SetButtonBackgroundColor(tui.theme.navigationButtonColor). - SetButtonTextColor(tui.theme.navigationButtonTextColor) - - // Assemble page layout - layout := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). - AddItem(nil, 2, 0, false). - AddItem(bucketSelectionWidget, 2, 0, false). - AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). - AddItem(nil, 1, 0, false) - - layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) - - return layout -} - -// --- Page 5: Caching Settings --- -// -// Function to build the caching page that allows users to configure caching settings. -// Includes options for enabling/disabling caching, specifying cache location, size, and retention settings. -func (tui *appContext) buildCachingPage() tview.Primitive { - // Main layout container. Must be instantiated first to allow nested items. - layout := tview.NewFlex().SetDirection(tview.FlexRow) - - instructionsText := "[#6EBE49::b] Configure Caching Settings[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + - "[white::b] CloudFuse can cache data locally. You control the location, size, and duration.[-::-]\n\n" + - "[#FFD700::b] -[-::-] [#6EBE49::b]Enable[-::-] caching if you frequently re-read data and have ample disk space.\n" + - "[#FFD700::b] -[-::-] [red::b]Disable[-::-] caching if you prefer faster initial access or have limited disk space.\n\n" - - // Instructions text widget - instructionsTextWidget := tview.NewTextView(). - SetWrap(true). - SetDynamicColors(true). - SetText(instructionsText) - - // Dropdown widget for enabling/disabling caching - cacheLocationFieldWidget := tview.NewInputField(). - SetLabel("📁 Cache Location: "). - SetText(tui.config.cacheLocation). - SetFieldWidth(40). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack). - SetChangedFunc(func(text string) { - tui.config.cacheLocation = text - }) - - // Input field widget for cache size percentage - cacheSizeFieldWidget := tview.NewInputField(). - SetLabel("📊 Cache Size (%): "). - SetText(tui.config.cacheSize). // Default to 80% - SetFieldWidth(4). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack). - SetChangedFunc(func(size string) { - if size, err := strconv.Atoi(size); err != nil || size < 1 || size > 100 { - tui.showErrorModal( - "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", - func() { - tui.pages.SwitchToPage("page5") - }, - ) - return - } - tui.config.cacheSize = size - }) - - // Input field widget for cache retention duration - cacheRetentionDurationFieldWidget := tview.NewInputField(). - SetLabel("⌛ Cache Retention Duration: "). - SetText(fmt.Sprintf("%d", tui.config.cacheRetentionDuration)). - SetFieldWidth(5). - SetChangedFunc(func(text string) { - if val, err := strconv.Atoi(text); err == nil { - tui.config.cacheRetentionDuration = val - } else { - // TODO: Handle invalid input - tui.config.cacheRetentionDuration = 0 - } - }). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Dropdown widget for cache retention unit - cacheRetentionUnitDropdownWidget := tview.NewDropDown(). - SetOptions([]string{"Seconds", "Minutes", "Hours", "Days"}, func(option string, index int) { - tui.config.cacheRetentionUnit = option - // Convert cache retention duration to seconds - switch tui.config.cacheRetentionUnit { - case "Seconds": - tui.config.cacheRetentionDurationSec = tui.config.cacheRetentionDuration - case "Minutes": - minutes := tui.config.cacheRetentionDuration - tui.config.cacheRetentionDurationSec = minutes * 60 - case "Hours": - hours := tui.config.cacheRetentionDuration - tui.config.cacheRetentionDurationSec = hours * 3600 - case "Days": - days := tui.config.cacheRetentionDuration - tui.config.cacheRetentionDurationSec = days * 86400 - } - }). - SetCurrentOption(3). // Default to Days - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Dropdown widget for enabling/disabling cache cleanup on restart - // If enabled --> allow-non-empty-temp: false - // if disabled --> allow-non-empty-temp: true - clearCacheOnStartDropdownWidget := tview.NewDropDown(). - SetLabel("🧹 Clear Cache On Start: "). - SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { - if option == " Enabled " { - tui.config.clearCacheOnStart = true - } else { - tui.config.clearCacheOnStart = false - } - }).SetCurrentOption(0). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(colorBlack) - - // Horizontal container to place retention duration and unit side by side - cacheRetentionRow := tview.NewFlex(). - SetDirection(tview.FlexColumn). - AddItem(cacheRetentionDurationFieldWidget, 35, 0, false). - AddItem(cacheRetentionUnitDropdownWidget, 7, 0, false) - - // Group cache field widgets in a container - cacheFields := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(cacheLocationFieldWidget, 2, 0, false). - AddItem(cacheSizeFieldWidget, 2, 0, false). - AddItem(cacheRetentionRow, 2, 0, false). - AddItem(clearCacheOnStartDropdownWidget, 2, 0, false) - - // Tracks whether or not cache fields are currently shown - showCacheFields := true - - // Navigation buttons widget - navigationButtonsWidget := tview.NewForm() - navigationButtonsWidget. - AddButton(tui.theme.navigationHomeLabel, func() { - tui.pages.SwitchToPage("home") - }). - AddButton(tui.theme.navigationFinishLabel, func() { - // Check if caching is enabled and validate cache settings - if tui.config.enableCaching { - // Validate the cache location - if err := tui.validateCachePath(); err != nil { - tui.showErrorModal("Invalid cache location:\n"+err.Error(), func() { - tui.pages.SwitchToPage("page5") - }) - return - } - - // Check available cache size - if err := tui.getAvailableCacheSize(); err != nil { - tui.showErrorModal( - "Failed to check available cache size:\n"+err.Error(), - func() { - tui.pages.SwitchToPage("page5") - }, - ) - return - } - - cacheSizeText := fmt.Sprintf( - "Available Disk Space @ Cache Location: [darkred::b]%d GB[-::-]\n", - tui.config.availableCacheSizeGB, - ) + - fmt.Sprintf( - "Cache Size Currently Set to: [darkred::b]%.0f GB (%s%%)[-::-]\n\n", - float64(tui.config.currentCacheSizeGB), - tui.config.cacheSize, - ) + - "Would you like to proceed with this cache size?\n\n" + - "If not, hit [darkred::b]Return[-::-] to adjust cache size accordingly. Otherwise, hit [darkred::b]Finish[-::-] to complete the configuration." - - tui.showCacheConfirmationModal(cacheSizeText, - // Callback function if the user selects Finish - func() { - if err := tui.createYAMLConfig(); err != nil { - tui.showErrorModal( - "Failed to create YAML config:\n"+err.Error(), - func() { - tui.pages.SwitchToPage("page5") - }, - ) - return - } - tui.showExitModal(func() { - tui.app.Stop() - }) - }, - // Callback function if the user selects Return - func() { - tui.pages.SwitchToPage("page5") - }) - - } else { - // If caching is disabled, just finish the configuration - if err := tui.createYAMLConfig(); err != nil { - tui.showErrorModal("Failed to create YAML config:\n"+err.Error(), func() { - tui.pages.SwitchToPage("page5") - }) - return - } - tui.showExitModal(func() { - tui.app.Stop() - }) - } - }). - AddButton(tui.theme.navigationBackLabel, func() { - if tui.config.storageProtocol == "azstorage" { - tui.pages.SwitchToPage("page3") - } else { - page4 := tui.buildBucketSelectionPage() - tui.pages.AddPage("page4", page4, true, false) - tui.pages.SwitchToPage("page4") - } - }). - AddButton(tui.theme.navigationPreviewLabel, func() { - previewPage := tui.buildPreviewPage("page5") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") - }). - AddButton(tui.theme.navigationQuitLabel, func() { - tui.app.Stop() - }). - SetButtonBackgroundColor(tui.theme.navigationButtonColor). - SetButtonTextColor(colorBlack) - - // Widget to enable/disable caching - enableCachingDropdownWidget := tview.NewDropDown() - enableCachingDropdownWidget. - SetLabel("💾 Caching: "). - SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { - if option == " Enabled " { - tui.config.cacheMode = "file_cache" - tui.config.enableCaching = true - if !showCacheFields { - layout.RemoveItem(navigationButtonsWidget) - layout.RemoveItem(cacheFields) - layout.AddItem(cacheFields, 8, 0, false) - layout.AddItem( - navigationButtonsWidget, - tui.theme.navigationWidgetHeight, - 0, - false, - ) - showCacheFields = true - } - } else { - tui.config.cacheMode = "block_cache" - tui.config.enableCaching = false - if showCacheFields { - layout.RemoveItem(cacheFields) - showCacheFields = false - } - } - }). - SetCurrentOption(0). - SetLabelColor(tui.theme.widgetLabelColor). - SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). - SetFieldTextColor(tcell.ColorBlack) - - // Assemble page layout - layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) - layout.AddItem(enableCachingDropdownWidget, 2, 0, false) - - if showCacheFields { - layout.AddItem(cacheFields, 8, 0, false) - } - - layout.AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false) - layout.AddItem(nil, 1, 0, false) - layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) - - return layout -} - -// --- Summary Page --- -// -// Function to build the summary page that displays the configuration summary. -// This function creates a text view with the summary information and a return button. -// The preview page parameter allows switching back to the previous page when the user clicks "Return". -func (tui *appContext) buildPreviewPage(previewPage string) tview.Primitive { - summaryText := - "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]" + - fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", tui.config.storageProvider) + - fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", tui.config.endpointURL) + - fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", tui.config.bucketName) + - fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", tui.config.cacheMode) + - fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", tui.config.cacheLocation) + - fmt.Sprintf( - " Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", - tui.config.cacheSize, - tui.config.currentCacheSizeGB, - ) - - // Display cache retention duration in seconds and specified unit - if tui.config.cacheRetentionUnit == "Seconds" { - summaryText += fmt.Sprintf( - " Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", - tui.config.cacheRetentionDurationSec, - ) - } else { - summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", - tui.config.cacheRetentionDurationSec, tui.config.cacheRetentionDuration, tui.config.cacheRetentionUnit) - } - - // Set a dynamic width and height for the summary widget - summaryWidgetHeight := getTextHeight(summaryText) - summaryWidgetWidth := getTextWidth(summaryText) / 3 - - summaryWidget := tview.NewTextView(). - SetWrap(true). - SetDynamicColors(true). - SetText(summaryText). - SetScrollable(true) - - returnButton := tview.NewButton("[black]Return[-]"). - SetSelectedFunc(func() { - tui.pages.SwitchToPage(previewPage) - }) - returnButton.SetBackgroundColor(colorGreen) - returnButton.SetBorder(true) - returnButton.SetBorderColor(colorYellow) - returnButton.SetBackgroundColorActivated(colorGreen) - - buttons := tview.NewFlex(). - SetDirection(tview.FlexColumn). - AddItem(nil, 0, 1, false). // Left button spacer - AddItem(returnButton, 20, 0, true). - AddItem(nil, 0, 1, false) // Right button spacer - - modal := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(summaryWidget, summaryWidgetHeight, 0, false). - AddItem(nil, 1, 0, false). - AddItem(buttons, 3, 0, true) - - leftAlignedModal := tview.NewFlex(). - AddItem(modal, summaryWidgetWidth, 0, true) - - leftAlignedModal.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) - - return leftAlignedModal -} - -// Function to show a modal dialog with a message and an "OK" button. -// This function is used to display error messages or confirmations. -// May specify a callback function to execute when the modal is closed. -func (tui *appContext) showErrorModal(message string, onClose func()) { - modal := tview.NewModal(). - SetText(message). - AddButtons([]string{"OK"}). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - tui.pages.RemovePage("modal") - onClose() - }). - SetBackgroundColor(colorGreen). - SetTextColor(tcell.ColorBlack) - modal.SetBorder(true) - modal.SetBorderColor(colorYellow) - modal.SetButtonBackgroundColor(colorYellow) - modal.SetButtonTextColor(tcell.ColorBlack) - tui.pages.AddPage("modal", modal, false, true) -} - -// Function to show a confirmation modal dialog with "Finish" and "Return" buttons. -// Used to confirm cache size before proceeding. Must specify two callback functions for the "Finish" and "Return" actions. -func (tui *appContext) showCacheConfirmationModal( - message string, - onFinish func(), - onReturn func(), -) { - modal := tview.NewModal(). - SetText(message). - AddButtons([]string{"Finish", "Return"}). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - tui.pages.RemovePage("modal") - if buttonLabel == "Finish" { - onFinish() - } else { - onReturn() - } - }). - SetBackgroundColor(colorGreen). - SetTextColor(tcell.ColorBlack) - modal.SetBorder(true) - modal.SetBorderColor(colorYellow) - modal.SetButtonBackgroundColor(colorYellow) - modal.SetButtonTextColor(tcell.ColorBlack) - tui.pages.AddPage("modal", modal, true, true) -} - -// Function to show final exit modal when configuration is complete. -// Informs the user that the configuration is complete and they can exit. -// This function is called when the user clicks "Finish" on the caching page. -func (tui *appContext) showExitModal(onConfirm func()) { - - processingEmojis := []string{"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "✅"} - - modal := tview.NewModal(). - AddButtons([]string{"Exit"}). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - tui.pages.RemovePage("modal") - if buttonLabel == "Exit" { - onConfirm() - } - }). - SetBackgroundColor(colorGreen). - SetTextColor(tcell.ColorBlack) - modal.SetBorder(true) - modal.SetBorderColor(colorYellow) - modal.SetButtonBackgroundColor(colorYellow) - modal.SetButtonTextColor(tcell.ColorBlack) - - tui.pages.AddPage("modal", modal, true, true) - - // Simulate processing with emoji animation - go func() { - // Show initial message with emoji animation - for i := 0; i < len(processingEmojis); i++ { - currentEmoji := processingEmojis[i] - time.Sleep(100 * time.Millisecond) - tui.app.QueueUpdateDraw(func() { - modal.SetText( - fmt.Sprintf( - "[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", - currentEmoji, - ), - ) - }) - } - - // After animation, show final message - tui.app.QueueUpdateDraw(func() { - modal.SetText(fmt.Sprintf("[#6EBE49::b]Configuration Complete![-::-]\n\n%s\n\n"+ - "Your CloudFuse configuration file has been created at:\n\n[blue:white:b]%s[-:-:-]\n\n"+ - "You can now exit the application.\n\n"+ - "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], tui.config.configFilePath)) - }) - }() -} - -// Helper function to center lines of text within a specified width. -// It is used to format text views and other UI elements in the TUI. -func centerText(text string, width int) string { - var centeredLines []string - lines := strings.Split(text, "\n") - for _, line := range lines { - visibleLen := tview.TaggedStringWidth(line) // handle color tags - if visibleLen >= width { - centeredLines = append(centeredLines, line) - } else { - padding := (width - visibleLen) / 2 - centeredLines = append(centeredLines, strings.Repeat(" ", padding)+line) - } - } - return strings.Join(centeredLines, "\n") -} - -// Helper function to get the length of the longest line in a string. -// It is used to determine the width of text views and other UI elements. -func getTextWidth(s string) int { - if s == "" { - return 0 - } - lines := strings.Split(s, "\n") - longest := 0 - for _, line := range lines { - if len(line) > longest { - longest = len(line) - } - } - return longest -} - -// Helper function to count the number of lines in a string. -// It is used to determine the height of text views and other UI elements. -func getTextHeight(s string) int { - if s == "" { - return 0 - } - return len(strings.Split(s, "\n")) -} - -// Helper function to get a fallback cache path if the home directory cannot be determined. -func getFallbackCachePath() string { - user := os.Getenv("USER") - if user == "" { - uid := os.Getuid() - user = fmt.Sprintf("uid_%d", uid) - } - return filepath.Join(os.TempDir(), "cloudfuse", user) -} - -// Helper function to get the default cache path. -// It retrieves the user's home directory and constructs a default cache path: -// -// `~/.cloudfuse/file_cache`. If it fails to retrieve the home directory or create the path, it returns a fallback path. -func getDefaultCachePath() string { - // TODO: Add logic to return OS-specific cache paths - home, err := os.UserHomeDir() - if err != nil { - fmt.Printf( - "[red::b]ERROR: Failed to get home directory: %v\nUsing fallback path for cache directory.\n", - err, - ) - return getFallbackCachePath() - } - cachePath := filepath.Join(home, ".cloudfuse", "file_cache") - // If the directory doesn't exist, create it - if _, err := os.Stat(cachePath); os.IsNotExist(err) { - if err := os.MkdirAll(cachePath, 0700); err != nil { - fmt.Printf( - "[red::b]ERROR: Failed to create cache directory: %v\nUsing fallback path for cache directory.\n", - err, - ) - return getFallbackCachePath() - } - } - // Return the full path to the cache directory - return cachePath -} - -// Helper function to validate the entered cache path. -func (tui *appContext) validateCachePath() error { - // Validate that the path is not empty - if strings.TrimSpace(tui.config.cacheLocation) == "" { - return fmt.Errorf("[red::b]ERROR: Cache location cannot be empty[-::-]") - } - // Make sure no invalid path characters are used - if strings.ContainsAny(tui.config.cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { - return fmt.Errorf("[red::b]ERROR: Cache location contains invalid characters[-::-]") - } - // Validate that the cache path exists - if tui.config.cacheLocation != getDefaultCachePath() && tui.config.cacheMode == "file_cache" { - if _, err := os.Stat(tui.config.cacheLocation); os.IsNotExist(err) { - return fmt.Errorf( - "[red::b]ERROR: '%s': No such file or directory[-::-]", - tui.config.cacheLocation, - ) - } - } - return nil -} - -// Helper function to get the available disk space at the cache location and calculates -// the cache size in GB based on the user-defined cache size percentage. -func (tui *appContext) getAvailableCacheSize() error { - availableBlocks, _, err := common.GetAvailFree(tui.config.cacheLocation) - if err != nil { - // If we fail to get the available cache size, we default to 80% of the available disk space - tui.config.cacheSize = "80" - returnMsg := fmt.Errorf( - "[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ - "Defaulting cache size to 80%% of available disk space.\n\n"+ - "Please manually verify you have enough disk space available for caching.[-::-]", - tui.config.cacheLocation, - err, - ) - return returnMsg - } - - const blockSize = 4096 - availableCacheSizeBytes := availableBlocks * blockSize // Convert blocks to bytes - tui.config.availableCacheSizeGB = int( - availableCacheSizeBytes / (1024 * 1024 * 1024), - ) // Convert to GB - cacheSizeInt, _ := strconv.Atoi(tui.config.cacheSize) - tui.config.currentCacheSizeGB = int(tui.config.availableCacheSizeGB) * cacheSizeInt / 100 - - return nil -} - -// Helper function to normalize and validate the user-defined endpoint URL. -func (tui *appContext) validateEndpointURL(rawURL string) error { - rawURL = strings.TrimSpace(rawURL) - - // Check if the URL is empty - if strings.TrimSpace(rawURL) == "" { - return fmt.Errorf("[red::b]Endpoint URL cannot be empty[-::-]\nPlease try again.") - } - - // Normalize the URL by adding "https://" if it doesn't start with "http://" or "https://" - if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { - tui.config.endpointURL = "https://" + rawURL - return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n" + - "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") - } - - if _, err := url.ParseRequestURI(rawURL); err != nil { - return fmt.Errorf("[red::b]Invalid URL format[-::-]\n%s\nPlease try again.", err.Error()) - } - - return nil -} - -// Function to check the credentials entered by the user. -// Attempts to connect to the storage backend and fetch the bucket list. -// If successful, populates the `bucketList` variable with the list of available buckets (for s3 providers only). -// Called when the user clicks "Next" on the credentials page. -func (tui *appContext) checkCredentials() error { - // Create a temporary configOptions struct with only the storage component - tmpConfig := configOptions{ - Components: []string{tui.config.storageProtocol}, - } - - if tui.config.storageProtocol == "azstorage" { - tmpConfig.AzStorage = azstorage.AzStorageOptions{ - AccountType: "block", - AccountName: tui.config.accountName, - AccountKey: tui.config.accountKey, - AuthMode: "key", - Container: tui.config.containerName, - } - } else { - tmpConfig.S3Storage = s3StorageConfig{ - BucketName: tui.config.bucketName, - KeyID: tui.config.accessKey, - SecretKey: tui.config.secretKey, - Endpoint: tui.config.endpointURL, - EnableDirMarker: true, - } - } - - // Marshal the temporary struct to YAML format - tmpConfigData, _ := yaml.Marshal(&tmpConfig) - - // Write the temporary config data into the global options struct instead of a temporary file. - // This avoids the need to create and delete a temporary file on disk. - if err := config.ReadFromConfigBuffer(tmpConfigData); err != nil { - return fmt.Errorf("Failed to read config from buffer: %v", err) - } - - if err := config.Unmarshal(&options); err != nil { - return fmt.Errorf("Failed to unmarshal config: %v", err) - } - - // Try to fetch bucket list - var err error - if slices.Contains(options.Components, "azstorage") { - tui.config.bucketList, err = getContainerListAzure() - - } else if slices.Contains(options.Components, "s3storage") { - tui.config.bucketList, err = getBucketListS3() - - } else { - err = fmt.Errorf("Unsupported storage backend") - } - - if err != nil { - return fmt.Errorf("Failed to get bucket list: %v", err) - } - - return nil -} - -// Function to create the YAML configuration file based on user inputs once all forms are completed. -// Called when the user clicks "Finish" on the caching page. -func (tui *appContext) createYAMLConfig() error { - config := configOptions{ - Components: []string{ - "libfuse", - tui.config.cacheMode, - "attr_cache", - tui.config.storageProtocol, - }, - - Libfuse: libfuse.LibfuseOptions{ - NetworkShare: true, - }, - - AttrCache: attr_cache.AttrCacheOptions{ - Timeout: uint32(7200), - }, - } - - if tui.config.cacheMode == "file_cache" { - config.FileCache = file_cache.FileCacheOptions{ - TmpPath: tui.config.cacheLocation, - Timeout: uint32(tui.config.cacheRetentionDurationSec), - AllowNonEmpty: !tui.config.clearCacheOnStart, - SyncToFlush: true, - } - // If cache size is not set to 80%, convert currentCacheSizeGB to MB and set file_cache.max-size-mb to it - if tui.config.cacheSize != "80" { - config.FileCache.MaxSizeMB = float64( - tui.config.currentCacheSizeGB * 1024, - ) // Convert GB to MB - } - } - - if tui.config.storageProtocol == "s3storage" { - config.S3Storage = s3StorageConfig{ - BucketName: tui.config.bucketName, - KeyID: tui.config.accessKey, - SecretKey: tui.config.secretKey, - Endpoint: tui.config.endpointURL, - EnableDirMarker: true, - } - } else { - config.AzStorage = azstorage.AzStorageOptions{ - AccountType: "block", - AccountName: tui.config.accountName, - AccountKey: tui.config.accountKey, - AuthMode: "key", - Container: tui.config.containerName, - } - } - - // Marshal the struct to YAML format - configData, err := yaml.Marshal(&config) - if err != nil { - return fmt.Errorf("Failed to marshal configuration data to YAML: %v", err) - } - - // Encrypt the YAML config data using the user-provided passphrase - encryptedPassphrase := memguard.NewEnclave([]byte(tui.config.configEncryptionPassphrase)) - cipherText, err := common.EncryptData(configData, encryptedPassphrase) - if err != nil { - return fmt.Errorf("Failed to encrypt configuration data: %v", err) - } - - // Write the encrypted YAML config data to a file - if err := os.WriteFile("config.aes", cipherText, 0600); err != nil { - return fmt.Errorf("Failed to create encrypted config.aes file: %v", err) - } - - // Update configFilePath member to point to the created config file - currDir, err := os.Getwd() - if err != nil { - return fmt.Errorf("Error: %v", err) - } - - tui.config.configFilePath = filepath.Join(currDir, "config.aes") - - return nil -} From 433abfea546944bc94ebf44181b11d02c31ecdbd Mon Sep 17 00:00:00 2001 From: brayan Date: Wed, 27 Aug 2025 10:14:07 -0600 Subject: [PATCH 17/21] fix view bug in checkCredentials(), add loading message, minor fixes --- cmd/config.go | 111 ++++++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index aca44e1b6..82984bcd2 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -37,6 +37,7 @@ import ( "github.com/Seagate/cloudfuse/common" "github.com/Seagate/cloudfuse/common/config" + "github.com/Seagate/cloudfuse/common/log" "github.com/Seagate/cloudfuse/component/attr_cache" "github.com/Seagate/cloudfuse/component/azstorage" "github.com/Seagate/cloudfuse/component/file_cache" @@ -175,6 +176,9 @@ func init() { // Main function to run the TUI application. // Initializes the tview application, builds the TUI application, and runs it. func (tui *appContext) runTUI() error { + // Disable cloudfuse logging during TUI session to prevent log messages from interfering with the UI. + log.SetLogLevel(1) + tui.app.EnableMouse(true) tui.app.EnablePaste(true) @@ -381,31 +385,28 @@ func (tui *appContext) buildEndpointURLPage() tview.Primitive { // Determine URL help text based on selected provider switch tui.config.storageProvider { case "LyveCloud": - urlRegionHelpText = "[::b]You selected LyveCloud as your storage provider.[::-]\n\n" + - "For LyveCloud, the endpoint URL format is generally:\n" + - "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + - "Example:\n[darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + - "[grey::i]Find more info in your LyveCloud portal.\nAvailable regions are listed below in the dropdown.[-::-]" - urlRegionHelpText = centerText(urlRegionHelpText, 65) + urlRegionHelpText = "[::b] You selected LyveCloud as your storage provider.[::-]\n\n" + + " For LyveCloud, the endpoint URL format is generally:\n" + + "[darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.<[darkcyan::b]identifier[darkmagenta::b]>.lyve.seagate.com[-]\n\n" + + "\t\t\t\t Example:\n [darkmagenta::b]https://s3.us-east-1.sv15.lyve.seagate.com[-]\n\n" + + "[grey::i] *Refer to your LyveCloud portal for valid formats.[-::-]" case "AWS": - urlRegionHelpText = "[::b]You selected AWS as your storage provider.[::-]\n\n" + - "The endpoint URL format is generally:\n" + - "[darkmagenta::b]https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-]\n\n" + - "Example:\n[darkmagenta::b]https://s3.us-east-1.amazonaws.com[-]\n\n" + - "[grey::i]Refer to AWS documentation for valid formats and available regions.[-::-]" - urlRegionHelpText = centerText(urlRegionHelpText, 65) + urlRegionHelpText = "[::b] You selected AWS as your storage provider.[::-]\n\n" + + " The endpoint URL format is generally:\n" + + "[darkmagenta::b] https://s3.<[darkcyan::b]region[darkmagenta::b]>.amazonaws.com[-]\n\n" + + "\t\t\t Example:\n[darkmagenta::b] https://s3.us-east-1.amazonaws.com[-]\n\n" + + "[grey::i] *Refer to your AWS portal for valid formats.[-::-]" case "Other": - urlRegionHelpText = "[::b]You selected a custom s3 provider.[::-]\n\n" + - "Enter the endpoint URL.\n" + - "[grey::i]Refer to your provider’s documentation for valid formats.[-::-]" - urlRegionHelpText = centerText(urlRegionHelpText, 65) + urlRegionHelpText = "[::b] You selected a custom s3 provider.[::-]\n\n" + + " Enter the endpoint URL.\n\n" + + "[grey::i] *Refer to your provider’s documentation for valid formats.[-::-]" } instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n"+ "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ - "[white]\n %s", tui.config.storageProvider, urlRegionHelpText) + "[white]\n%s", tui.config.storageProvider, urlRegionHelpText) instructionsTextWidget := tview.NewTextView(). SetText(instructionsText). @@ -417,7 +418,7 @@ func (tui *appContext) buildEndpointURLPage() tview.Primitive { SetText(tui.config.endpointURL). SetFieldWidth(50). SetChangedFunc(func(url string) { - tui.config.endpointURL = url + tui.config.endpointURL = strings.TrimSpace(url) }). SetPlaceholder("\t\t\t\t"). SetPlaceholderTextColor(tcell.ColorGray). @@ -541,8 +542,8 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { SetText(tui.config.accessKey). SetFieldWidth(50). SetChangedFunc(func(key string) { - tui.config.accessKey = key - tui.config.accountName = key + tui.config.accessKey = strings.TrimSpace(key) + tui.config.accountName = strings.TrimSpace(key) }). SetPlaceholder("\t\t\t\t"). SetLabelColor(tui.theme.widgetLabelColor). @@ -555,8 +556,8 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { SetText(string(tui.config.secretKey)). SetFieldWidth(50). SetChangedFunc(func(key string) { - tui.config.secretKey = key - tui.config.accountKey = key + tui.config.secretKey = strings.TrimSpace(key) + tui.config.accountKey = strings.TrimSpace(key) }). SetPlaceholder("\t\t\t\t"). SetMaskCharacter('*'). @@ -570,8 +571,8 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { SetText(tui.config.containerName). SetPlaceholder("\t\t\t\t"). SetChangedFunc(func(name string) { - tui.config.containerName = name - tui.config.bucketName = name + tui.config.containerName = strings.TrimSpace(name) + tui.config.bucketName = strings.TrimSpace(name) }). SetFieldWidth(50). SetLabelColor(tui.theme.widgetLabelColor). @@ -611,42 +612,43 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { ) return } - // TODO: Fix bug here where calling listBuckets() in the checkCredentials() function - // causes the layout to shift upwards and the widgets to be misaligned if the user incorrectly - // enters credentials. - if err := tui.checkCredentials(); err != nil { - tui.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { - tui.pages.RemovePage("page3") // Remove the current page - page3 := tui.buildCredentialsPage() // Rebuild the page - tui.pages.AddPage("page3", page3, true, false) // Add the new page + // Show a quick loading modal while validating credentials by attempting to fetch list of buckets/containers + tui.showLoadingModal("Validating credentials...") + go func() { + err := tui.checkCredentials() + + tui.app.QueueUpdateDraw(func() { tui.pages.SwitchToPage("page3") - }) - return - } + tui.pages.RemovePage("loading") - if tui.config.storageProtocol == "azstorage" { - tui.pages.RemovePage("page4") // Remove previous page if it exists - tui.pages.SwitchToPage("page5") - } else { - page4 := tui.buildBucketSelectionPage() - tui.pages.AddPage("page4", page4, true, false) - tui.pages.SwitchToPage("page4") - } + if err != nil { + tui.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { + tui.pages.SwitchToPage("page3") + }) + return + } + if tui.config.storageProtocol == "azstorage" { + tui.pages.RemovePage("page4") + tui.pages.SwitchToPage("page5") + } else { + page4 := tui.buildBucketSelectionPage() + tui.pages.AddAndSwitchToPage("page4", page4, true) + } + }) + }() }). AddButton(tui.theme.navigationBackLabel, func() { - if tui.config.storageProvider == "Microsoft" || tui.config.storageProvider == "AWS" { + if tui.config.storageProvider == "Microsoft" { tui.pages.RemovePage("page2") tui.pages.SwitchToPage("page1") } else { page2 := tui.buildEndpointURLPage() - tui.pages.AddPage("page2", page2, true, false) - tui.pages.SwitchToPage("page2") + tui.pages.AddAndSwitchToPage("page2", page2, true) } }). AddButton(tui.theme.navigationPreviewLabel, func() { previewPage := tui.buildPreviewPage("page3") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") + tui.pages.AddAndSwitchToPage("previewPage", previewPage, true) }). AddButton(tui.theme.navigationQuitLabel, func() { tui.app.Stop() @@ -1106,6 +1108,19 @@ func (tui *appContext) showErrorModal(message string, onClose func()) { tui.pages.AddPage("modal", modal, false, true) } +// Function to show a loading modal dialog with a message. +func (tui *appContext) showLoadingModal(loadingMessage string) { + modal := tview.NewModal(). + SetText(loadingMessage). + SetBackgroundColor(colorGreen). + SetTextColor(tcell.ColorBlack) + modal.SetBorder(true) + modal.SetBorderColor(colorYellow) + modal.SetButtonBackgroundColor(colorYellow) + modal.SetButtonTextColor(tcell.ColorBlack) + tui.pages.AddPage("loading", modal, true, true) +} + // Function to show a confirmation modal dialog with "Finish" and "Return" buttons. // Used to confirm cache size before proceeding. Must specify two callback functions for the "Finish" and "Return" actions. func (tui *appContext) showCacheConfirmationModal( From 9ae802e9dfb178ef6dc65aae017b0e49ab7f27a6 Mon Sep 17 00:00:00 2001 From: brayan Date: Wed, 27 Aug 2025 14:02:08 -0600 Subject: [PATCH 18/21] improve code consistency and readability --- cmd/config.go | 290 +++++++++++++++++++++++++------------------------- 1 file changed, 146 insertions(+), 144 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 82984bcd2..34a7c919f 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -102,9 +102,9 @@ type uiTheme struct { // Global general purpose vars var ( - colorYellow = tcell.GetColor("#FFD700") - colorGreen = tcell.GetColor("#6EBE49") - colorBlack = tcell.ColorBlack + colorYellow tcell.Color = tcell.GetColor("#FFD700") + colorGreen tcell.Color = tcell.GetColor("#6EBE49") + colorBlack tcell.Color = tcell.ColorBlack ) // Struct to hold the final configuration data to be written to the YAML config file. @@ -219,14 +219,16 @@ func (tui *appContext) buildTUI() { // Function to build the home page of the TUI application. Displays a // welcome banner, instructions, and buttons to start or quit the application. func (tui *appContext) buildHomePage() tview.Primitive { - bannerText := "[#6EBE49::b]" + - " █▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n" + - "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n" + - "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n" + - "[white::b]Welcome to the CloudFuse Configuration Tool\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[#6EBE49::b]Cloud storage configuration made easy via terminal.[-]\n\n" + - "[::b]Press [#FFD700]Start[-] to begin or [red]Quit[-] to exit.\n" + bannerText := fmt.Sprintf( + "[%s::b]"+ + " █▀▀░█░░░█▀█░█░█░█▀▄░█▀▀░█░█░█▀▀░█▀▀\n"+ + "░█░░░█░░░█░█░█░█░█░█░█▀▀░█░█░▀▀█░█▀▀\n"+ + "░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀▀░░▀░░░▀▀▀░▀▀▀░▀▀▀[-]\n\n"+ + "[white::b]Welcome to the CloudFuse Configuration Tool\n"+ + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n"+ + "[%s::b]Cloud storage configuration made easy via terminal.[-]\n\n"+ + "[::b]Press [%s]Start[-] to begin or [red]Quit[-] to exit.\n", + colorGreen, colorYellow, colorGreen, colorYellow) // Banner text widget bannerTextWidget := tview.NewTextView(). @@ -234,10 +236,12 @@ func (tui *appContext) buildHomePage() tview.Primitive { SetDynamicColors(true). SetWrap(true) - instructionsText := "[#FFD700::b]Instructions:[::-]\n" + - "[#6EBE49::b]•[-::-] [::]Use your mouse or arrow keys to navigate.[-::-]\n" + - "[#6EBE49::b]•[-::-] [::]Press Enter or left-click to select items.[-::-]\n" + - "[#6EBE49::b]•[-::-] [::]For the best experience, expand terminal window to full size.[-::-]\n" + instructionsText := fmt.Sprintf( + "[%s::b]Instructions:[::-]\n"+ + "[%s::b] •[-::-] Use your mouse or arrow keys to navigate.\n"+ + "[%s::b] •[-::-] Press Enter or left-click to select items.\n"+ + "[%s::b] •[-::-] [::]For the best experience, expand terminal window to full size.\n", + colorYellow, colorGreen, colorGreen, colorGreen) // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -256,12 +260,14 @@ func (tui *appContext) buildHomePage() tview.Primitive { SetButtonBackgroundColor(tui.theme.navigationButtonColor). SetButtonTextColor(tui.theme.navigationButtonTextColor) - aboutText := "[#FFD700::b]ABOUT[-::-]\n" + - "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n" + - "[grey::i]CloudFuse TUI Configuration Tool\n" + - "Seagate Technology, LLC\n" + - "cloudfuse@seagate.com\n" + - fmt.Sprintf("Version: %s", common.CloudfuseVersion) + aboutText := fmt.Sprintf( + "[%s::b]ABOUT[-::-]\n"+ + "[white]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n"+ + "[grey::i]CloudFuse TUI Configuration Tool\n"+ + "Seagate Technology, LLC\n"+ + "cloudfuse@seagate.com\n"+ + "Version: %s", + colorYellow, common.CloudfuseVersion) // About text widget aboutTextWidget := tview.NewTextView(). @@ -291,11 +297,13 @@ func (tui *appContext) buildHomePage() tview.Primitive { // Function to build the storage provider selection page. Allows users to select their cloud storage provider // from a dropdown list. The options are: LyveCloud, Microsoft, AWS, and Other S3. func (tui *appContext) buildStorageProviderPage() tview.Primitive { - instructionsText := "[#6EBE49::b] Select Your Cloud Storage Provider[-::-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n" + - "[grey::i] If your provider is not listed, choose [darkmagenta::b]Other (s3)[-::-][grey::i]. You’ll be\n" + - " prompted to enter the endpoint URL and region manually.[-::-]\n" + instructionsText := fmt.Sprintf( + "[%s::b] Select Your Cloud Storage Provider[-::-]\n"+ + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n"+ + "[white::b] Choose your cloud storage provider from the dropdown below.[-::-]\n"+ + "[grey::i] If your provider is not listed, choose [darkmagenta::b]Other (s3)[-::-][grey::i]. You’ll be\n"+ + " prompted to enter the endpoint URL and region manually.[-::-]\n", + colorGreen, colorYellow) // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -352,8 +360,7 @@ func (tui *appContext) buildStorageProviderPage() tview.Primitive { }). AddButton(tui.theme.navigationPreviewLabel, func() { previewPage := tui.buildPreviewPage("page1") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") + tui.pages.AddAndSwitchToPage("previewPage", previewPage, true) }). AddButton(tui.theme.navigationQuitLabel, func() { tui.app.Stop() @@ -404,9 +411,10 @@ func (tui *appContext) buildEndpointURLPage() tview.Primitive { "[grey::i] *Refer to your provider’s documentation for valid formats.[-::-]" } - instructionsText := fmt.Sprintf("[#6EBE49::b] Enter Endpoint URL for %s[-]\n"+ - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ - "[white]\n%s", tui.config.storageProvider, urlRegionHelpText) + instructionsText := fmt.Sprintf( + "[%s::b] Enter Endpoint URL for %s[-]\n"+ + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"+ + "[white]\n%s", colorGreen, tui.config.storageProvider, colorYellow, urlRegionHelpText) instructionsTextWidget := tview.NewTextView(). SetText(instructionsText). @@ -434,28 +442,23 @@ func (tui *appContext) buildEndpointURLPage() tview.Primitive { AddButton(tui.theme.navigationNextLabel, func() { if err := tui.validateEndpointURL(tui.config.endpointURL); err != nil { tui.showErrorModal( - fmt.Sprintf("[red::b]ERROR: %s[-::-]", err.Error()), + fmt.Sprintf("[red::b]ERROR:[-::-] %s", err.Error()), func() { - tui.pages.RemovePage("page2") page2 := tui.buildEndpointURLPage() - tui.pages.AddPage("page2", page2, true, false) - tui.pages.SwitchToPage("page2") + tui.pages.AddAndSwitchToPage("page2", page2, true) }, ) return } - tui.pages.RemovePage("page3") page3 := tui.buildCredentialsPage() - tui.pages.AddPage("page3", page3, true, false) - tui.pages.SwitchToPage("page3") + tui.pages.AddAndSwitchToPage("page3", page3, true) }). AddButton(tui.theme.navigationBackLabel, func() { tui.pages.SwitchToPage("page1") }). AddButton(tui.theme.navigationPreviewLabel, func() { previewPage := tui.buildPreviewPage("page2") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") + tui.pages.AddAndSwitchToPage("previewPage", previewPage, true) }). AddButton(tui.theme.navigationQuitLabel, func() { tui.app.Stop() @@ -484,8 +487,6 @@ func (tui *appContext) buildEndpointURLPage() tview.Primitive { // If the storage protocol is "s3", it provides input fields for access key, secret key. // If the storage protocol is "azure", it provides input fields for account name, account key, and container name. func (tui *appContext) buildCredentialsPage() tview.Primitive { - layout := tview.NewFlex() - layout.Clear() // Determine labels for input fields based on storage protocol. accessLabel := "" @@ -499,27 +500,13 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { } instructionsText := fmt.Sprintf( - "[%s::b] Enter Your Cloud Storage Credentials[-]\n", - colorGreen, - ) + - fmt.Sprintf( - "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n", - colorYellow, - ) + - fmt.Sprintf( - "[%s::b] -%s[-::-] This is your unique identifier for accessing your cloud storage.\n", - colorYellow, - strings.Trim(accessLabel, "🔑 "), - ) + - fmt.Sprintf( + "[%s::b] Enter Your Cloud Storage Credentials[-]\n"+ + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[::-]\n\n"+ + "[%s::b] -%s[-::-] This is your unique identifier for accessing your cloud storage.\n"+ "[%s::b] -%s[-::-] This is your secret password for accessing your cloud storage.\n", - colorYellow, - strings.Trim(secretLabel, "🔑 "), - ) + - fmt.Sprintf( - "[%s::b] -Passphrase:[-::-] This is used to encrypt your configuration file.\n", - colorYellow, - ) + colorGreen, colorYellow, colorYellow, strings.Trim(accessLabel, "🔑 "), + colorYellow, strings.Trim(secretLabel, "🔑 "), + ) if tui.config.storageProtocol == "azstorage" { instructionsText += fmt.Sprintf( @@ -528,7 +515,10 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { ) } - instructionsText += "\n[darkmagenta::i]\t\t\t*Keep these credentials secure. Do not share.[-]" + instructionsText += fmt.Sprintf( + "[%s::b] -Passphrase:[-::-] This is used to encrypt your configuration file.\n"+ + "\n[darkmagenta::i]\t\t\t*Keep these credentials secure. Do not share.[-]", + colorYellow) // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -605,7 +595,7 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { (tui.config.storageProtocol == "azstorage" && (len(tui.config.accountName) == 0 || len(tui.config.accountKey) == 0 || len(tui.config.containerName) == 0)) || len(tui.config.configEncryptionPassphrase) == 0 { tui.showErrorModal( - "[red::b]ERROR: Credential fields cannot be empty.\nPlease try again.[-::-]", + "[red::b]ERROR:[-::-] Credential fields cannot be empty.\nPlease try again.", func() { tui.pages.SwitchToPage("page3") }, @@ -622,9 +612,11 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { tui.pages.RemovePage("loading") if err != nil { - tui.showErrorModal(fmt.Sprintf("[red::b]ERROR: %s", err.Error()), func() { - tui.pages.SwitchToPage("page3") - }) + tui.showErrorModal(fmt.Sprintf("[red::b]ERROR:[-::-] %s", err.Error()), + func() { + tui.pages.SwitchToPage("page3") + }, + ) return } if tui.config.storageProtocol == "azstorage" { @@ -661,7 +653,6 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { credentialsWidget := tview.NewForm(). AddFormItem(accessKeyFieldWidget). AddFormItem(secretKeyFieldWidget). - AddFormItem(passphraseFieldWidget). SetFieldTextColor(tcell.ColorBlack). SetLabelColor(tui.theme.widgetLabelColor). SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor) @@ -671,13 +662,17 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { credentialsWidget.AddFormItem(containerNameFieldWidget) } + credentialsWidget.AddFormItem(passphraseFieldWidget) + // Assemble page layout - layout.SetDirection(tview.FlexRow) - layout.AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false) - layout.AddItem(nil, 1, 0, false) - layout.AddItem(credentialsWidget, credentialsWidget.GetFormItemCount()*2+1, 0, false) - layout.AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false) - layout.AddItem(nil, 1, 0, false) + layout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(instructionsTextWidget, getTextHeight(instructionsText), 0, false). + AddItem(nil, 1, 0, false). + AddItem(credentialsWidget, credentialsWidget.GetFormItemCount()*2+1, 0, false). + AddItem(navigationButtonsWidget, tui.theme.navigationWidgetHeight, 0, false). + AddItem(nil, 1, 0, false) + layout.SetBorder(true).SetBorderColor(colorGreen).SetBorderPadding(1, 1, 1, 1) return layout @@ -688,11 +683,13 @@ func (tui *appContext) buildCredentialsPage() tview.Primitive { // Function to build the bucket selection page. Allows users to select a bucket from a dropdown list // of retrieved buckets based on provided s3 credentials. For s3 storage users only. Azure storage users will skip this page. func (tui *appContext) buildBucketSelectionPage() tview.Primitive { - instructionsText := "[#6EBE49::b] Select Your Bucket Name[-::-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n" + - "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n" + - "[grey::i] The list of available buckets is retrieved from your cloud storage provider\n " + - "based on the credentials provided in the previous step.[-::-]" + instructionsText := fmt.Sprintf( + "[%s::b] Select Your Bucket Name[-::-]\n"+ + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[-]\n\n"+ + "[white::b] Select the name of your storage bucket from the dropdown below.[-::-]\n\n"+ + "[grey::i] The list of available buckets is retrieved from your cloud storage provider\n "+ + "based on the credentials provided in the previous step.[-::-]", + colorGreen, colorYellow) // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -725,8 +722,7 @@ func (tui *appContext) buildBucketSelectionPage() tview.Primitive { }). AddButton(tui.theme.navigationPreviewLabel, func() { previewPage := tui.buildPreviewPage("page4") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") + tui.pages.AddAndSwitchToPage("previewPage", previewPage, true) }). AddButton(tui.theme.navigationQuitLabel, func() { tui.app.Stop() @@ -756,11 +752,13 @@ func (tui *appContext) buildCachingPage() tview.Primitive { // Main layout container. Must be instantiated first to allow nested items. layout := tview.NewFlex().SetDirection(tview.FlexRow) - instructionsText := "[#6EBE49::b] Configure Caching Settings[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + - "[white::b] CloudFuse can cache data locally. You control the location, size, and duration.[-::-]\n\n" + - "[#FFD700::b] -[-::-] [#6EBE49::b]Enable[-::-] caching if you frequently re-read data and have ample disk space.\n" + - "[#FFD700::b] -[-::-] [red::b]Disable[-::-] caching if you prefer faster initial access or have limited disk space.\n\n" + instructionsText := fmt.Sprintf( + "[%s::b] Configure Caching Settings[-]\n"+ + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"+ + "[white::b] CloudFuse can cache data locally. You control the location, size, and duration.[-::-]\n\n"+ + "[%s::b] -[-::-] [%s::b]Enable[-::-] caching if you frequently re-read data and have ample disk space.\n"+ + "[%s::b] -[-::-] [red::b]Disable[-::-] caching if you prefer faster initial access or have limited disk space.\n\n", + colorGreen, colorYellow, colorYellow, colorGreen, colorYellow) // Instructions text widget instructionsTextWidget := tview.NewTextView(). @@ -776,8 +774,8 @@ func (tui *appContext) buildCachingPage() tview.Primitive { SetLabelColor(tui.theme.widgetLabelColor). SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack). - SetChangedFunc(func(text string) { - tui.config.cacheLocation = text + SetChangedFunc(func(location string) { + tui.config.cacheLocation = location }) // Input field widget for cache size percentage @@ -791,7 +789,7 @@ func (tui *appContext) buildCachingPage() tview.Primitive { SetChangedFunc(func(size string) { if size, err := strconv.Atoi(size); err != nil || size < 1 || size > 100 { tui.showErrorModal( - "[red::b]ERROR: Cache size must be between 1 and 100.\nPlease try again.[-::-]", + "[red::b]ERROR:[-::-] Cache size must be between 1 and 100.\nPlease try again.", func() { tui.pages.SwitchToPage("page5") }, @@ -806,8 +804,8 @@ func (tui *appContext) buildCachingPage() tview.Primitive { SetLabel("⌛ Cache Retention Duration: "). SetText(fmt.Sprintf("%d", tui.config.cacheRetentionDuration)). SetFieldWidth(5). - SetChangedFunc(func(text string) { - if val, err := strconv.Atoi(text); err == nil { + SetChangedFunc(func(duration string) { + if val, err := strconv.Atoi(duration); err == nil { tui.config.cacheRetentionDuration = val } else { // TODO: Handle invalid input @@ -842,9 +840,9 @@ func (tui *appContext) buildCachingPage() tview.Primitive { SetFieldBackgroundColor(tui.theme.widgetFieldBackgroundColor). SetFieldTextColor(colorBlack) - // Dropdown widget for enabling/disabling cache cleanup on restart - // If enabled --> allow-non-empty-temp: false - // if disabled --> allow-non-empty-temp: true + // Dropdown widget for enabling/disabling cache cleanup on restart + // If enabled --> allow-non-empty-temp: false + // if disabled --> allow-non-empty-temp: true clearCacheOnStartDropdownWidget := tview.NewDropDown(). SetLabel("🧹 Clear Cache On Start: "). SetOptions([]string{" Enabled ", " Disabled "}, func(option string, index int) { @@ -886,16 +884,19 @@ func (tui *appContext) buildCachingPage() tview.Primitive { if tui.config.enableCaching { // Validate the cache location if err := tui.validateCachePath(); err != nil { - tui.showErrorModal("Invalid cache location:\n"+err.Error(), func() { - tui.pages.SwitchToPage("page5") - }) + tui.showErrorModal( + "[red::b]ERROR:[-::-] Invalid cache location:\n"+err.Error(), + func() { + tui.pages.SwitchToPage("page5") + }, + ) return } // Check available cache size if err := tui.getAvailableCacheSize(); err != nil { tui.showErrorModal( - "Failed to check available cache size:\n"+err.Error(), + "[red::b]ERROR:[-::-] Failed to fetch available cache size:\n"+err.Error(), func() { tui.pages.SwitchToPage("page5") }, @@ -920,7 +921,7 @@ func (tui *appContext) buildCachingPage() tview.Primitive { func() { if err := tui.createYAMLConfig(); err != nil { tui.showErrorModal( - "Failed to create YAML config:\n"+err.Error(), + "[red::b]ERROR:[-::-] Failed to create YAML config:\n"+err.Error(), func() { tui.pages.SwitchToPage("page5") }, @@ -939,7 +940,7 @@ func (tui *appContext) buildCachingPage() tview.Primitive { } else { // If caching is disabled, just finish the configuration if err := tui.createYAMLConfig(); err != nil { - tui.showErrorModal("Failed to create YAML config:\n"+err.Error(), func() { + tui.showErrorModal("[red::b]ERROR:[-::-] Failed to create YAML config:\n"+err.Error(), func() { tui.pages.SwitchToPage("page5") }) return @@ -954,14 +955,12 @@ func (tui *appContext) buildCachingPage() tview.Primitive { tui.pages.SwitchToPage("page3") } else { page4 := tui.buildBucketSelectionPage() - tui.pages.AddPage("page4", page4, true, false) - tui.pages.SwitchToPage("page4") + tui.pages.AddAndSwitchToPage("page4", page4, true) } }). AddButton(tui.theme.navigationPreviewLabel, func() { previewPage := tui.buildPreviewPage("page5") - tui.pages.AddPage("previewPage", previewPage, true, false) - tui.pages.SwitchToPage("previewPage") + tui.pages.AddAndSwitchToPage("previewPage", previewPage, true) }). AddButton(tui.theme.navigationQuitLabel, func() { tui.app.Stop() @@ -1024,29 +1023,33 @@ func (tui *appContext) buildCachingPage() tview.Primitive { // This function creates a text view with the summary information and a return button. // The preview page parameter allows switching back to the previous page when the user clicks "Return". func (tui *appContext) buildPreviewPage(previewPage string) tview.Primitive { - summaryText := - "[#6EBE49::b] CloudFuse Summary Configuration:[-]\n" + - "[#FFD700]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]" + - fmt.Sprintf(" Storage Provider: [#FFD700::b]%s[-]\n", tui.config.storageProvider) + - fmt.Sprintf(" Endpoint URL: [#FFD700::b]%s[-]\n", tui.config.endpointURL) + - fmt.Sprintf(" Bucket Name: [#FFD700::b]%s[-]\n", tui.config.bucketName) + - fmt.Sprintf(" Cache Mode: [#FFD700::b]%s[-]\n", tui.config.cacheMode) + - fmt.Sprintf(" Cache Location: [#FFD700::b]%s[-]\n", tui.config.cacheLocation) + - fmt.Sprintf( - " Cache Size: [#FFD700::b]%s%% (%d GB)[-]\n", - tui.config.cacheSize, - tui.config.currentCacheSizeGB, - ) + summaryText := fmt.Sprintf( + "[%s::b] CloudFuse Summary Configuration:[-]\n"+ + "[%s]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n[-]"+ + " Storage Provider: [%s::b]%s[-]\n"+ + " Endpoint URL: [%s::b]%s[-]\n"+ + " Bucket Name: [%s::b]%s[-]\n"+ + " Cache Mode: [%s::b]%s[-]\n"+ + " Cache Location: [%s::b]%s[-]\n"+ + " Cache Size: [%s::b]%s%% (%d GB)[-]\n", + colorGreen, colorYellow, + colorYellow, tui.config.storageProvider, + colorYellow, tui.config.endpointURL, + colorYellow, tui.config.bucketName, + colorYellow, tui.config.cacheMode, + colorYellow, tui.config.cacheLocation, + colorYellow, tui.config.cacheSize, tui.config.currentCacheSizeGB, + ) // Display cache retention duration in seconds and specified unit if tui.config.cacheRetentionUnit == "Seconds" { summaryText += fmt.Sprintf( - " Cache Retention: [#FFD700::b]%d Seconds[-]\n\n", - tui.config.cacheRetentionDurationSec, + " Cache Retention: [%s::b]%d Seconds[-]\n\n", + colorYellow, tui.config.cacheRetentionDurationSec, ) } else { - summaryText += fmt.Sprintf(" Cache Retention: [#FFD700::b]%d sec (%d %s)[-]\n\n", - tui.config.cacheRetentionDurationSec, tui.config.cacheRetentionDuration, tui.config.cacheRetentionUnit) + summaryText += fmt.Sprintf(" Cache Retention: [%s::b]%d sec (%d %s)[-]\n\n", + colorYellow, tui.config.cacheRetentionDurationSec, tui.config.cacheRetentionDuration, tui.config.cacheRetentionUnit) } // Set a dynamic width and height for the summary widget @@ -1181,7 +1184,7 @@ func (tui *appContext) showExitModal(onConfirm func()) { tui.app.QueueUpdateDraw(func() { modal.SetText( fmt.Sprintf( - "[#6EBE49::b]Creating configuration file...[-::-]\n\n%s", + "[black::b]Creating configuration file...[-::-]\n\n%s", currentEmoji, ), ) @@ -1190,8 +1193,8 @@ func (tui *appContext) showExitModal(onConfirm func()) { // After animation, show final message tui.app.QueueUpdateDraw(func() { - modal.SetText(fmt.Sprintf("[#6EBE49::b]Configuration Complete![-::-]\n\n%s\n\n"+ - "Your CloudFuse configuration file has been created at:\n\n[blue:white:b]%s[-:-:-]\n\n"+ + modal.SetText(fmt.Sprintf("[black::b]Configuration Complete![-::-]\n\n%s\n\n"+ + "Your CloudFuse configuration file has been created at:\n\n[blue:white:b] %s [-:-:-]\n\n"+ "You can now exit the application.\n\n"+ "[black::i]Thank you for using CloudFuse Config![-::-]", processingEmojis[len(processingEmojis)-1], tui.config.configFilePath)) }) @@ -1259,7 +1262,7 @@ func getDefaultCachePath() string { home, err := os.UserHomeDir() if err != nil { fmt.Printf( - "[red::b]ERROR: Failed to get home directory: %v\nUsing fallback path for cache directory.\n", + "Failed to get home directory: %v\nUsing fallback path for cache directory.\n", err, ) return getFallbackCachePath() @@ -1269,7 +1272,7 @@ func getDefaultCachePath() string { if _, err := os.Stat(cachePath); os.IsNotExist(err) { if err := os.MkdirAll(cachePath, 0700); err != nil { fmt.Printf( - "[red::b]ERROR: Failed to create cache directory: %v\nUsing fallback path for cache directory.\n", + "Failed to create cache directory: %v\nUsing fallback path for cache directory.\n", err, ) return getFallbackCachePath() @@ -1283,19 +1286,16 @@ func getDefaultCachePath() string { func (tui *appContext) validateCachePath() error { // Validate that the path is not empty if strings.TrimSpace(tui.config.cacheLocation) == "" { - return fmt.Errorf("[red::b]ERROR: Cache location cannot be empty[-::-]") + return fmt.Errorf("Cache location cannot be empty.") } // Make sure no invalid path characters are used if strings.ContainsAny(tui.config.cacheLocation, `<>:"|?*#%^&;'"`+"`"+`{}[]`) { - return fmt.Errorf("[red::b]ERROR: Cache location contains invalid characters[-::-]") + return fmt.Errorf("Cache location contains invalid characters.") } // Validate that the cache path exists if tui.config.cacheLocation != getDefaultCachePath() && tui.config.cacheMode == "file_cache" { if _, err := os.Stat(tui.config.cacheLocation); os.IsNotExist(err) { - return fmt.Errorf( - "[red::b]ERROR: '%s': No such file or directory[-::-]", - tui.config.cacheLocation, - ) + return fmt.Errorf("'%s': No such file or directory.", tui.config.cacheLocation) } } return nil @@ -1309,12 +1309,10 @@ func (tui *appContext) getAvailableCacheSize() error { // If we fail to get the available cache size, we default to 80% of the available disk space tui.config.cacheSize = "80" returnMsg := fmt.Errorf( - "[red::b]WARNING: Failed to get available cache size at '%s': %v\n\n"+ + "Failed to get available cache size at '%s': %v\n\n"+ "Defaulting cache size to 80%% of available disk space.\n\n"+ - "Please manually verify you have enough disk space available for caching.[-::-]", - tui.config.cacheLocation, - err, - ) + "Please manually verify you have enough disk space available for caching.", + tui.config.cacheLocation, err) return returnMsg } @@ -1335,18 +1333,21 @@ func (tui *appContext) validateEndpointURL(rawURL string) error { // Check if the URL is empty if strings.TrimSpace(rawURL) == "" { - return fmt.Errorf("[red::b]Endpoint URL cannot be empty[-::-]\nPlease try again.") + return fmt.Errorf("Endpoint URL cannot be empty.\nPlease try again.") } // Normalize the URL by adding "https://" if it doesn't start with "http://" or "https://" if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { tui.config.endpointURL = "https://" + rawURL - return fmt.Errorf("[red::b]Endpoint URL should start with 'http://' or 'https://'.\n" + - "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.") + return fmt.Errorf( + "Endpoint URL should start with 'http://' or 'https://'.\n" + + "Appending 'https://' to the URL...\n\nPlease verify the URL and try again.", + ) } if _, err := url.ParseRequestURI(rawURL); err != nil { - return fmt.Errorf("[red::b]Invalid URL format[-::-]\n%s\nPlease try again.", err.Error()) + return fmt.Errorf( + "Invalid URL format.\n%s\nPlease try again.", err.Error()) } return nil @@ -1406,7 +1407,7 @@ func (tui *appContext) checkCredentials() error { } if err != nil { - return fmt.Errorf("Failed to get bucket list: %v", err) + return fmt.Errorf("Failed to validate credentials: %v", err) } return nil @@ -1468,7 +1469,8 @@ func (tui *appContext) createYAMLConfig() error { // Marshal the struct to YAML format configData, err := yaml.Marshal(&config) if err != nil { - return fmt.Errorf("Failed to marshal configuration data to YAML: %v", err) + return fmt.Errorf( + "Failed to marshal configuration data to YAML: %v", err) } // Encrypt the YAML config data using the user-provided passphrase @@ -1486,7 +1488,7 @@ func (tui *appContext) createYAMLConfig() error { // Update configFilePath member to point to the created config file currDir, err := os.Getwd() if err != nil { - return fmt.Errorf("Error: %v", err) + return fmt.Errorf("Failed to get current working directory: %v", err) } tui.config.configFilePath = filepath.Join(currDir, "config.aes") From ac4e75bd3d8b089e3df4d34803ffc44d2eacbf6b Mon Sep 17 00:00:00 2001 From: brayan Date: Wed, 27 Aug 2025 14:06:51 -0600 Subject: [PATCH 19/21] fix bug in bucketName selection (s3 only) --- cmd/config.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 34a7c919f..6eabae3fa 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -954,8 +954,7 @@ func (tui *appContext) buildCachingPage() tview.Primitive { if tui.config.storageProtocol == "azstorage" { tui.pages.SwitchToPage("page3") } else { - page4 := tui.buildBucketSelectionPage() - tui.pages.AddAndSwitchToPage("page4", page4, true) + tui.pages.SwitchToPage("page4") } }). AddButton(tui.theme.navigationPreviewLabel, func() { From ba62e58eaf853b12048f303189a581da7065bf44 Mon Sep 17 00:00:00 2001 From: brayan Date: Thu, 28 Aug 2025 10:19:30 -0600 Subject: [PATCH 20/21] disable logging quietly, remove from and method names --- cmd/config.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 6eabae3fa..4958a874a 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -162,7 +162,7 @@ var configCmd = &cobra.Command{ Long: "Starts an interactive terminal-based UI to generate your Cloudfuse configuration file.", RunE: func(cmd *cobra.Command, args []string) error { tui := newAppContext() - if err := tui.runTUI(); err != nil { + if err := tui.run(); err != nil { return fmt.Errorf("Failed to run TUI: %v", err) } return nil @@ -175,14 +175,14 @@ func init() { // Main function to run the TUI application. // Initializes the tview application, builds the TUI application, and runs it. -func (tui *appContext) runTUI() error { +func (tui *appContext) run() error { // Disable cloudfuse logging during TUI session to prevent log messages from interfering with the UI. - log.SetLogLevel(1) + log.SetDefaultLogger("silent", common.LogConfig{Level: common.ELogLevel.LOG_OFF()}) tui.app.EnableMouse(true) tui.app.EnablePaste(true) - tui.buildTUI() + tui.build() // Run the application if err := tui.app.Run(); err != nil { @@ -193,7 +193,7 @@ func (tui *appContext) runTUI() error { } // Function to build the TUI application. Initializes the pages and adds them to the page stack. -func (tui *appContext) buildTUI() { +func (tui *appContext) build() { // Initialize the pages homePage := tui.buildHomePage() // --- Home Page --- From 3c194012501a8f28cf136c4e90f9967916e88b28 Mon Sep 17 00:00:00 2001 From: brayan Date: Thu, 28 Aug 2025 11:01:49 -0600 Subject: [PATCH 21/21] fix linting issues by handling log.SetDefaultLogger() --- cmd/config.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 4958a874a..d7a37a186 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -177,11 +177,13 @@ func init() { // Initializes the tview application, builds the TUI application, and runs it. func (tui *appContext) run() error { // Disable cloudfuse logging during TUI session to prevent log messages from interfering with the UI. - log.SetDefaultLogger("silent", common.LogConfig{Level: common.ELogLevel.LOG_OFF()}) + if err := log.SetDefaultLogger("silent", common.LogConfig{Level: common.ELogLevel.LOG_OFF()}); err != nil { + // If setting silent logger fails, this fallback is sufficient. + log.SetLogLevel(1) + } tui.app.EnableMouse(true) tui.app.EnablePaste(true) - tui.build() // Run the application