diff --git a/amfora.go b/amfora.go index d0b7d1a..6ffee79 100644 --- a/amfora.go +++ b/amfora.go @@ -18,22 +18,26 @@ func main() { if len(os.Args) > 1 { if os.Args[1] == "--version" || os.Args[1] == "-v" { - fmt.Print(version + "\r\n") + fmt.Println(version) return } if os.Args[1] == "--help" || os.Args[1] == "-h" { - fmt.Print("Amfora is a fancy terminal browser for the Gemini protocol.\r\n\r\n") - fmt.Print("Usage:\r\namfora [URL]\r\namfora --version, -v\r\n") + fmt.Println("Amfora is a fancy terminal browser for the Gemini protocol.") + fmt.Println() + fmt.Println("Usage:") + fmt.Println("amfora [URL]") + fmt.Println("amfora --version, -v") return } } err := config.Init() if err != nil { - panic(err) + fmt.Printf("Config error: %v\n", err) + os.Exit(1) } - display.Init() + display.Init() display.NewTab() display.NewTab() // Open extra tab and close it to fully initialize the app and wrapping display.CloseTab() diff --git a/config/config.go b/config/config.go index b0983d4..8386183 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path/filepath" "runtime" @@ -24,10 +25,13 @@ var BkmkStore = viper.New() var bkmkDir string var bkmkPath string +// For other pkgs to use +var DownloadsDir string + func Init() error { home, err := homedir.Dir() if err != nil { - panic(err) + return err } // Store AppData path if runtime.GOOS == "windows" { @@ -144,6 +148,7 @@ func Init() error { viper.SetDefault("a-general.bullets", true) viper.SetDefault("a-general.left_margin", 0.15) viper.SetDefault("a-general.max_width", 100) + viper.SetDefault("a-general.downloads", "") viper.SetDefault("cache.max_size", 0) viper.SetDefault("cache.max_pages", 20) @@ -154,6 +159,37 @@ func Init() error { return err } + // Setup downloads dir + if viper.GetString("a-general.downloads") == "" { + // Find default Downloads dir + // This seems to work for all OSes? + DownloadsDir = filepath.Join(home, "Downloads") + // Create it just in case + err = os.MkdirAll(DownloadsDir, 0755) + if err != nil { + return fmt.Errorf("downloads path could not be created: %s", DownloadsDir) + } + } else { + // Validate path + dDir := viper.GetString("a-general.downloads") + di, err := os.Stat(dDir) + if err == nil { + if !di.IsDir() { + return fmt.Errorf("downloads path specified is not a directory: %s", dDir) + } + } else if os.IsNotExist(err) { + // Try to create path + err = os.MkdirAll(dDir, 0755) + if err != nil { + return fmt.Errorf("downloads path could not be created: %s", dDir) + } + } else { + // Some other error + return fmt.Errorf("couldn't access downloads directory: %s", dDir) + } + DownloadsDir = dDir + } + // Setup cache from config cache.SetMaxSize(viper.GetInt("cache.max_size")) cache.SetMaxPages(viper.GetInt("cache.max_pages")) diff --git a/config/default.go b/config/default.go index 26a0912..9a7a63d 100644 --- a/config/default.go +++ b/config/default.go @@ -27,6 +27,7 @@ left_margin = 0.15 max_width = 100 # The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped. # 'downloads' is the path to a downloads folder. # An empty value means the code will find the default downloads folder for your system. +# If the path does not exist it will be created. downloads = "" # Options for page cache - which is only for text/gemini pages # Increase the cache size to speed up browsing at the expense of memory diff --git a/default-config.toml b/default-config.toml index 5475dd0..e1a56e7 100644 --- a/default-config.toml +++ b/default-config.toml @@ -24,6 +24,7 @@ left_margin = 0.15 max_width = 100 # The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped. # 'downloads' is the path to a downloads folder. # An empty value means the code will find the default downloads folder for your system. +# If the path does not exist it will be created. downloads = "" # Options for page cache - which is only for text/gemini pages # Increase the cache size to speed up browsing at the expense of memory diff --git a/display/display.go b/display/display.go index db41fe9..4e1f03a 100644 --- a/display/display.go +++ b/display/display.go @@ -274,6 +274,18 @@ func Init() { case tcell.KeyPgDn: tabs[curTab].pageDown() return nil + case tcell.KeyCtrlS: + if tabs[curTab].hasContent() { + savePath, err := downloadPage(tabs[curTab].page) + if err != nil { + Error("Download Error", fmt.Sprintf("Error saving page content: %v", err)) + } else { + Info(fmt.Sprintf("Page content saved to %s. ", savePath)) + } + } else { + Info("The current page has no content, so it couldn't be downloaded.") + } + return nil case tcell.KeyRune: // Regular key was sent switch string(event.Rune()) { diff --git a/display/download.go b/display/download.go new file mode 100644 index 0000000..893089f --- /dev/null +++ b/display/download.go @@ -0,0 +1,103 @@ +package display + +import ( + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "strings" + + "github.com/makeworld-the-better-one/amfora/config" + "github.com/makeworld-the-better-one/amfora/structs" +) + +// getSafeDownloadName is used by downloads.go only. +// It returns a modified name that is unique for the downloads folder. +// This way duplicate saved files will not overwrite each other. +// +// lastDot should be set to true if the number added to the name should come before +// the last dot in the filename instead of the first. +// +// n should be set to 0, it is used for recursiveness. +func getSafeDownloadName(name string, lastDot bool, n int) (string, error) { + // newName("test.txt", 3) -> "test(3).txt" + newName := func() string { + if n <= 0 { + return name + } + if lastDot { + ext := filepath.Ext(name) + return strings.TrimSuffix(name, ext) + "(" + strconv.Itoa(n) + ")" + ext + } else { + idx := strings.Index(name, ".") + if idx == -1 { + return name + "(" + strconv.Itoa(n) + ")" + } + return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:] + } + } + + d, err := os.Open(config.DownloadsDir) + if err != nil { + return "", err + } + files, err := d.Readdirnames(-1) + if err != nil { + d.Close() + return "", err + } + + nn := newName() + for i := range files { + if nn == files[i] { + d.Close() + return getSafeDownloadName(name, lastDot, n+1) + } + } + d.Close() + return nn, nil // Name doesn't exist already +} + +// downloadPage saves the passed Page to a file. +// It returns the saved path and an error. +// It always cleans up, so if an error is returned there is no file saved +func downloadPage(p *structs.Page) (string, error) { + // Figure out file name + var name string + var err error + parsed, _ := url.Parse(p.Url) + if parsed.Path == "" || path.Base(parsed.Path) == "/" { + // No file, just the root domain + if p.Mediatype == structs.TextGemini { + name, err = getSafeDownloadName(parsed.Hostname()+".gmi", true, 0) + if err != nil { + return "", err + } + } else { + name, err = getSafeDownloadName(parsed.Hostname()+".txt", true, 0) + if err != nil { + return "", err + } + } + } else { + // There's a specific file + name = path.Base(parsed.Path) + if p.Mediatype == structs.TextGemini && !strings.HasSuffix(name, ".gmi") && !strings.HasSuffix(name, ".gemini") { + name += ".gmi" + } + name, err = getSafeDownloadName(name, false, 0) + if err != nil { + return "", err + } + } + savePath := filepath.Join(config.DownloadsDir, name) + err = ioutil.WriteFile(savePath, []byte(p.Raw), 0644) + if err != nil { + // Just in case + os.Remove(savePath) + return "", err + } + return savePath, err +}