Skip to content

Commit eae118f

Browse files
committed
🚧 Downloading all content seems to work
1 parent 39fa7c6 commit eae118f

File tree

9 files changed

+242
-61
lines changed

9 files changed

+242
-61
lines changed

‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88
### Added
9+
- **Downloading pages and any content** (#38)
910
- Link and heading lines are wrapped just like regular text lines
1011
- Wrapped list items are indented to stay behind the bullet (#35)
1112
- Certificate expiry date is stored when the cert IDs match (#39)

‎NOTES.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Notes
22

3-
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
4-
53
## Issues
4+
- URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL
65
- Can't go back or do other things while page is loading - need a way to stop `handleURL`
6+
- dlChoiceModal doesn't go away when portal is selected, and freezes on Cancel
77

88
## Upstream Bugs
99
- Wrapping messes up on brackets

‎README.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ curl -sSL https://raw.githubusercontent.com/makeworld-the-better-one/amfora/mast
3535
update-desktop-database ~/.local/share/applications
3636
```
3737

38+
### For developers
39+
This section is for programmers who want to install from source.
40+
41+
Install latest release:
42+
```
43+
GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora
44+
```
45+
46+
Install latest commit:
47+
```
48+
GO111MODULE=on go get -u github.com/makeworld-the-better-one/amfora@master
49+
```
50+
3851
## Usage
3952

4053
Just call `amfora` or `amfora <url>` on the terminal. On Windows it might be `amfora.exe` instead.
@@ -59,8 +72,8 @@ Features in *italics* are in the master branch, but not in the latest release.
5972
- [x] Multiple charset support (over 55)
6073
- [x] Built-in search (uses GUS by default)
6174
- [x] Bookmarks
75+
- [x] *Download pages and arbitrary data*
6276
- [ ] Search in pages with <kbd>Ctrl-F</kbd>
63-
- [ ] Download pages and arbitrary data
6477
- [ ] Emoji favicons
6578
- See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details
6679
- [ ] Stream support

‎display/display.go

+5
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ func Init() {
225225
// An InputField is in focus, nothing should interrupt
226226
return event
227227
}
228+
_, ok = App.GetFocus().(*cview.Modal)
229+
if ok {
230+
// It's focused on a modal right now, nothing should interrupt
231+
return event
232+
}
228233

229234
if tabs[curTab].mode == tabModeDone {
230235
// All the keys and operations that can only work while NOT loading

‎display/download.go

+208-42
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,226 @@
11
package display
22

33
import (
4+
"fmt"
5+
"io"
46
"io/ioutil"
57
"net/url"
68
"os"
79
"path"
810
"path/filepath"
911
"strconv"
1012
"strings"
13+
"time"
1114

15+
"github.com/gdamore/tcell"
1216
"github.com/makeworld-the-better-one/amfora/config"
1317
"github.com/makeworld-the-better-one/amfora/structs"
18+
"github.com/makeworld-the-better-one/go-gemini"
19+
"github.com/makeworld-the-better-one/progressbar/v3"
20+
"github.com/spf13/viper"
21+
"gitlab.com/tslocum/cview"
1422
)
1523

24+
// For choosing between download and the portal - copy of YesNo basically
25+
var dlChoiceModal = cview.NewModal().
26+
SetTextColor(tcell.ColorWhite).
27+
SetText("That file could not be displayed. What would you like to do?").
28+
AddButtons([]string{"Download", "Open in portal", "Cancel"})
29+
30+
// Channel to indicate what choice they made using the button text
31+
var dlChoiceCh = make(chan string)
32+
33+
var dlModal = cview.NewModal().
34+
SetTextColor(tcell.ColorWhite)
35+
36+
func dlInit() {
37+
if viper.GetBool("a-general.color") {
38+
dlChoiceModal.SetButtonBackgroundColor(tcell.ColorNavy).
39+
SetButtonTextColor(tcell.ColorWhite).
40+
SetBackgroundColor(tcell.ColorPurple)
41+
dlModal.SetButtonBackgroundColor(tcell.ColorNavy).
42+
SetButtonTextColor(tcell.ColorWhite).
43+
SetBackgroundColor(tcell.Color130) // DarkOrange3, #af5f00
44+
} else {
45+
dlChoiceModal.SetButtonBackgroundColor(tcell.ColorWhite).
46+
SetButtonTextColor(tcell.ColorBlack).
47+
SetBackgroundColor(tcell.ColorBlack)
48+
dlModal.SetButtonBackgroundColor(tcell.ColorWhite).
49+
SetButtonTextColor(tcell.ColorBlack).
50+
SetBackgroundColor(tcell.ColorBlack)
51+
}
52+
53+
dlChoiceModal.SetBorder(true)
54+
dlChoiceModal.SetBorderColor(tcell.ColorWhite)
55+
dlChoiceModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
56+
dlChoiceCh <- buttonLabel
57+
})
58+
dlChoiceModal.GetFrame().SetTitleColor(tcell.ColorWhite)
59+
dlChoiceModal.GetFrame().SetTitleAlign(cview.AlignCenter)
60+
61+
dlModal.SetBorder(true)
62+
dlModal.SetBorderColor(tcell.ColorWhite)
63+
dlModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
64+
if buttonLabel == "Ok" {
65+
tabPages.SwitchToPage(strconv.Itoa(curTab))
66+
}
67+
})
68+
dlModal.GetFrame().SetTitleColor(tcell.ColorWhite)
69+
dlModal.GetFrame().SetTitleAlign(cview.AlignCenter)
70+
dlModal.GetFrame().SetTitle(" Download ")
71+
}
72+
73+
// dlChoice displays the download choice modal and acts on the user's choice.
74+
// It should run in a goroutine.
75+
func dlChoice(u string, resp *gemini.Response) {
76+
defer resp.Body.Close()
77+
78+
parsed, err := url.Parse(u)
79+
if err != nil {
80+
Error("URL Error", err.Error())
81+
return
82+
}
83+
84+
tabPages.ShowPage("dlChoice")
85+
tabPages.SendToFront("dlChoice")
86+
App.SetFocus(dlChoiceModal)
87+
App.Draw()
88+
89+
choice := <-dlChoiceCh
90+
if choice == "Download" {
91+
tabPages.HidePage("dlChoice")
92+
App.Draw()
93+
downloadURL(u, resp)
94+
return
95+
}
96+
if choice == "Open in portal" {
97+
// Open in mozz's proxy
98+
portalURL := u
99+
if parsed.RawQuery != "" {
100+
// Remove query and add encoded version on the end
101+
query := parsed.RawQuery
102+
parsed.RawQuery = ""
103+
portalURL = parsed.String() + "%3F" + query
104+
}
105+
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
106+
handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
107+
tabPages.SwitchToPage(strconv.Itoa(curTab))
108+
App.Draw()
109+
return
110+
}
111+
tabPages.SwitchToPage(strconv.Itoa(curTab))
112+
App.Draw()
113+
}
114+
115+
// downloadURL pulls up a modal to show download progress and saves the URL content.
116+
// downloadPage should be used for Page content.
117+
func downloadURL(u string, resp *gemini.Response) {
118+
_, _, width, _ := dlModal.GetInnerRect()
119+
// Copy of progressbar.DefaultBytesSilent with custom width
120+
bar := progressbar.NewOptions64(
121+
-1,
122+
progressbar.OptionSetWidth(width),
123+
progressbar.OptionSetWriter(ioutil.Discard),
124+
progressbar.OptionShowBytes(true),
125+
progressbar.OptionThrottle(65*time.Millisecond),
126+
progressbar.OptionShowCount(),
127+
progressbar.OptionSpinnerType(14),
128+
)
129+
bar.RenderBlank()
130+
131+
savePath, err := downloadNameFromURL(u, "")
132+
if err != nil {
133+
Error("Download Error", "Error deciding on file name: "+err.Error())
134+
return
135+
}
136+
f, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
137+
if err != nil {
138+
Error("Download Error", "Error creating download file: "+err.Error())
139+
return
140+
}
141+
defer f.Close()
142+
143+
done := false
144+
145+
go func(isDone *bool) {
146+
// Update the bar display
147+
for !*isDone {
148+
dlModal.SetText(bar.String())
149+
App.Draw()
150+
time.Sleep(100 * time.Millisecond)
151+
}
152+
}(&done)
153+
154+
// Display
155+
dlModal.ClearButtons()
156+
dlModal.AddButtons([]string{"Downloading..."})
157+
tabPages.ShowPage("dl")
158+
tabPages.SendToFront("dl")
159+
App.SetFocus(dlModal)
160+
App.Draw()
161+
162+
io.Copy(io.MultiWriter(f, bar), resp.Body)
163+
done = true
164+
dlModal.SetText(fmt.Sprintf("Download complete! File saved to %s.", savePath))
165+
dlModal.ClearButtons()
166+
dlModal.AddButtons([]string{"Ok"})
167+
dlModal.GetForm().SetFocus(100)
168+
App.SetFocus(dlModal)
169+
App.Draw()
170+
}
171+
172+
// downloadPage saves the passed Page to a file.
173+
// It returns the saved path and an error.
174+
// It always cleans up, so if an error is returned there is no file saved
175+
func downloadPage(p *structs.Page) (string, error) {
176+
var savePath string
177+
var err error
178+
179+
if p.Mediatype == structs.TextGemini {
180+
savePath, err = downloadNameFromURL(p.Url, ".gmi")
181+
} else {
182+
savePath, err = downloadNameFromURL(p.Url, ".txt")
183+
}
184+
if err != nil {
185+
return "", err
186+
}
187+
err = ioutil.WriteFile(savePath, []byte(p.Raw), 0644)
188+
if err != nil {
189+
// Just in case
190+
os.Remove(savePath)
191+
return "", err
192+
}
193+
return savePath, err
194+
}
195+
196+
// downloadNameFromURL takes a URl and returns a safe download path that will not overwrite any existing file.
197+
// ext is an extension that will be added if the file has no extension, and for domain only URLs.
198+
// It should include the dot.
199+
func downloadNameFromURL(u string, ext string) (string, error) {
200+
var name string
201+
var err error
202+
parsed, _ := url.Parse(u)
203+
if parsed.Path == "" || path.Base(parsed.Path) == "/" {
204+
// No file, just the root domain
205+
name, err = getSafeDownloadName(parsed.Hostname()+ext, true, 0)
206+
if err != nil {
207+
return "", err
208+
}
209+
} else {
210+
// There's a specific file
211+
name = path.Base(parsed.Path)
212+
if !strings.Contains(name, ".") {
213+
// No extension
214+
name += ext
215+
}
216+
name, err = getSafeDownloadName(name, false, 0)
217+
if err != nil {
218+
return "", err
219+
}
220+
}
221+
return filepath.Join(config.DownloadsDir, name), nil
222+
}
223+
16224
// getSafeDownloadName is used by downloads.go only.
17225
// It returns a modified name that is unique for the downloads folder.
18226
// This way duplicate saved files will not overwrite each other.
@@ -59,45 +267,3 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
59267
d.Close()
60268
return nn, nil // Name doesn't exist already
61269
}
62-
63-
// downloadPage saves the passed Page to a file.
64-
// It returns the saved path and an error.
65-
// It always cleans up, so if an error is returned there is no file saved
66-
func downloadPage(p *structs.Page) (string, error) {
67-
// Figure out file name
68-
var name string
69-
var err error
70-
parsed, _ := url.Parse(p.Url)
71-
if parsed.Path == "" || path.Base(parsed.Path) == "/" {
72-
// No file, just the root domain
73-
if p.Mediatype == structs.TextGemini {
74-
name, err = getSafeDownloadName(parsed.Hostname()+".gmi", true, 0)
75-
if err != nil {
76-
return "", err
77-
}
78-
} else {
79-
name, err = getSafeDownloadName(parsed.Hostname()+".txt", true, 0)
80-
if err != nil {
81-
return "", err
82-
}
83-
}
84-
} else {
85-
// There's a specific file
86-
name = path.Base(parsed.Path)
87-
if p.Mediatype == structs.TextGemini && !strings.HasSuffix(name, ".gmi") && !strings.HasSuffix(name, ".gemini") {
88-
name += ".gmi"
89-
}
90-
name, err = getSafeDownloadName(name, false, 0)
91-
if err != nil {
92-
return "", err
93-
}
94-
}
95-
savePath := filepath.Join(config.DownloadsDir, name)
96-
err = ioutil.WriteFile(savePath, []byte(p.Raw), 0644)
97-
if err != nil {
98-
// Just in case
99-
os.Remove(savePath)
100-
return "", err
101-
}
102-
return savePath, err
103-
}

‎display/modals.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ func modalInit() {
4040
AddPage("error", errorModal, false, false).
4141
AddPage("input", inputModal, false, false).
4242
AddPage("yesno", yesNoModal, false, false).
43-
AddPage("bkmk", bkmkModal, false, false)
43+
AddPage("bkmk", bkmkModal, false, false).
44+
AddPage("dlChoice", dlChoiceModal, false, false).
45+
AddPage("dl", dlModal, false, false)
4446

4547
// Color setup
4648
if viper.GetBool("a-general.color") {
@@ -125,6 +127,7 @@ func modalInit() {
125127
yesNoModal.GetFrame().SetTitleAlign(cview.AlignCenter)
126128

127129
bkmkInit()
130+
dlInit()
128131
}
129132

130133
// Error displays an error on the screen in a modal.

‎display/private.go

+2-15
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func setPage(t *tab, p *structs.Page) {
166166
// Setup display
167167
App.SetFocus(t.view)
168168

169-
// Save bottom bar for the tab - TODO: other funcs will apply/display it
169+
// Save bottom bar for the tab - other funcs will apply/display it
170170
t.barLabel = ""
171171
t.barText = p.Url
172172
}
@@ -359,20 +359,7 @@ func handleURL(t *tab, u string) (string, bool) {
359359
return ret("", false)
360360
}
361361
// Status code 20, but not a document that can be displayed
362-
yes := YesNo("This type of file can't be displayed. Downloading will be implemented soon. Would like to open the file in a HTTPS proxy for now?")
363-
if yes {
364-
// Open in mozz's proxy
365-
portalURL := u
366-
if parsed.RawQuery != "" {
367-
// Remove query and add encoded version on the end
368-
query := parsed.RawQuery
369-
parsed.RawQuery = ""
370-
portalURL = parsed.String() + "%3F" + query
371-
}
372-
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
373-
374-
handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
375-
}
362+
go dlChoice(u, res)
376363
return ret("", false)
377364
}
378365

‎go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/fsnotify/fsnotify v1.4.9 // indirect
77
github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606
88
github.com/makeworld-the-better-one/go-gemini v0.6.0
9+
github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f
910
github.com/mitchellh/go-homedir v1.1.0
1011
github.com/mitchellh/mapstructure v1.3.1 // indirect
1112
github.com/pelletier/go-toml v1.8.0 // indirect

0 commit comments

Comments
 (0)