Bombabillo is a non-web client for the terminal, supporting Gopher, Gemini and much more. https://bombadillo.colorfield.space
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
6.3 KiB

  1. package main
  2. // Bombadillo is an internet client for the terminal of unix or
  3. // unix-like systems.
  4. //
  5. // Copyright (C) 2019 Brian Evans
  6. //
  7. // This program is free software: you can redistribute it and/or modify
  8. // it under the terms of the GNU General Public License as published by
  9. // the Free Software Foundation, either version 3 of the License, or
  10. // (at your option) any later version.
  11. //
  12. // This program is distributed in the hope that it will be useful,
  13. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. // GNU General Public License for more details.
  16. //
  17. // You should have received a copy of the GNU General Public License
  18. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. import (
  20. "flag"
  21. "fmt"
  22. "io/ioutil"
  23. "os"
  24. "os/signal"
  25. "path/filepath"
  26. "strconv"
  27. "strings"
  28. "syscall"
  29. "time"
  30. "tildegit.org/sloum/bombadillo/config"
  31. "tildegit.org/sloum/bombadillo/cui"
  32. "tildegit.org/sloum/bombadillo/gemini"
  33. )
  34. var version string = "2.3.3"
  35. var bombadillo *client
  36. var helplocation string = "gopher://bombadillo.colorfield.space:70/1/user-guide.map"
  37. var settings config.Config
  38. func saveConfig() error {
  39. var opts strings.Builder
  40. bkmrks := bombadillo.BookMarks.IniDump()
  41. certs := bombadillo.Certs.IniDump()
  42. opts.WriteString("\n[SETTINGS]\n")
  43. for k, v := range bombadillo.Options {
  44. opts.WriteString(k)
  45. opts.WriteRune('=')
  46. opts.WriteString(v)
  47. opts.WriteRune('\n')
  48. }
  49. opts.WriteString(bkmrks)
  50. opts.WriteString(certs)
  51. return ioutil.WriteFile(filepath.Join(bombadillo.Options["configlocation"], ".bombadillo.ini"), []byte(opts.String()), 0644)
  52. }
  53. func validateOpt(opt, val string) bool {
  54. var validOpts = map[string][]string{
  55. "webmode": []string{"none", "gui", "lynx", "w3m", "elinks"},
  56. "theme": []string{"normal", "inverse", "color"},
  57. "defaultscheme": []string{"gopher", "gemini", "http", "https"},
  58. "showimages": []string{"true", "false"},
  59. "geminiblocks": []string{"block", "neither", "alt", "both"},
  60. }
  61. opt = strings.ToLower(opt)
  62. val = strings.ToLower(val)
  63. if _, ok := validOpts[opt]; ok {
  64. for _, item := range validOpts[opt] {
  65. if item == val {
  66. return true
  67. }
  68. }
  69. return false
  70. }
  71. if opt == "timeout" {
  72. _, err := strconv.Atoi(val)
  73. if err != nil {
  74. return false
  75. }
  76. }
  77. return true
  78. }
  79. func lowerCaseOpt(opt, val string) string {
  80. switch opt {
  81. case "webmode", "theme", "defaultscheme", "showimages", "geminiblocks":
  82. return strings.ToLower(val)
  83. default:
  84. return val
  85. }
  86. }
  87. func loadConfig() {
  88. err := os.MkdirAll(bombadillo.Options["configlocation"], 0755)
  89. if err != nil {
  90. exitMsg := fmt.Sprintf("Error creating 'configlocation' directory: %s", err.Error())
  91. cui.Exit(3, exitMsg)
  92. }
  93. fp := filepath.Join(bombadillo.Options["configlocation"], ".bombadillo.ini")
  94. file, err := os.Open(fp)
  95. if err != nil {
  96. err = saveConfig()
  97. if err != nil {
  98. exitMsg := fmt.Sprintf("Error writing config file during bootup: %s", err.Error())
  99. cui.Exit(4, exitMsg)
  100. }
  101. }
  102. confparser := config.NewParser(file)
  103. settings, _ = confparser.Parse()
  104. _ = file.Close()
  105. for _, v := range settings.Settings {
  106. lowerkey := strings.ToLower(v.Key)
  107. if lowerkey == "configlocation" {
  108. // Read only
  109. continue
  110. }
  111. if _, ok := bombadillo.Options[lowerkey]; ok {
  112. if validateOpt(lowerkey, v.Value) {
  113. bombadillo.Options[lowerkey] = v.Value
  114. if lowerkey == "geminiblocks" {
  115. gemini.BlockBehavior = v.Value
  116. } else if lowerkey == "timeout" {
  117. updateTimeouts(v.Value)
  118. }
  119. } else {
  120. bombadillo.Options[lowerkey] = defaultOptions[lowerkey]
  121. }
  122. }
  123. }
  124. for i, v := range settings.Bookmarks.Titles {
  125. _, _ = bombadillo.BookMarks.Add([]string{v, settings.Bookmarks.Links[i]})
  126. }
  127. for _, v := range settings.Certs {
  128. // Remove expired certs
  129. vals := strings.SplitN(v.Value, "|", -1)
  130. if len(vals) < 2 {
  131. continue
  132. }
  133. now := time.Now()
  134. ts, err := strconv.ParseInt(vals[1], 10, 64)
  135. if err != nil || now.Unix() > ts {
  136. continue
  137. }
  138. // Satisfied that the cert is not expired
  139. // or malformed: add to the current client
  140. // instance
  141. bombadillo.Certs.Add(v.Key, vals[0], ts)
  142. }
  143. }
  144. func initClient() {
  145. bombadillo = MakeClient(" ((( Bombadillo ))) ")
  146. loadConfig()
  147. }
  148. // In the event of specific signals, ensure the display is shown correctly.
  149. // Accepts a signal, blocking until it is received. Once not blocked, corrects
  150. // terminal display settings as appropriate for that signal. Loops
  151. // indefinitely, does not return.
  152. func handleSignals(c <-chan os.Signal) {
  153. for {
  154. switch <-c {
  155. case syscall.SIGTSTP:
  156. cui.CleanupTerm()
  157. _ = syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
  158. case syscall.SIGCONT:
  159. cui.InitTerm()
  160. bombadillo.Draw()
  161. case syscall.SIGINT:
  162. cui.Exit(130, "")
  163. }
  164. }
  165. }
  166. //printHelp produces a nice display message when the --help flag is used
  167. func printHelp() {
  168. art := `Bombadillo - a non-web browser
  169. Syntax: bombadillo [options] [url]
  170. Examples: bombadillo gopher://bombadillo.colorfield.space
  171. bombadillo -t
  172. bombadillo -v
  173. Options:
  174. `
  175. _, _ = fmt.Fprint(os.Stdout, art)
  176. flag.PrintDefaults()
  177. }
  178. func main() {
  179. getVersion := flag.Bool("v", false, "Display version information and exit")
  180. addTitleToXWindow := flag.Bool("t", false, "Set the window title to 'Bombadillo'. Can be used in a GUI environment, however not all terminals support this feature.")
  181. flag.Usage = printHelp
  182. flag.Parse()
  183. if *getVersion {
  184. fmt.Printf("Bombadillo %s\n", version)
  185. os.Exit(0)
  186. }
  187. args := flag.Args()
  188. cui.InitTerm()
  189. if *addTitleToXWindow {
  190. fmt.Print("\033[22;0t") // Store window title on terminal stack
  191. fmt.Print("\033]0;Bombadillo\007") // Update window title
  192. }
  193. defer cui.Exit(0, "")
  194. initClient()
  195. // watch for signals, send them to be handled
  196. c := make(chan os.Signal, 1)
  197. signal.Notify(c, syscall.SIGTSTP, syscall.SIGCONT, syscall.SIGINT)
  198. go handleSignals(c)
  199. // Start polling for terminal size changes
  200. go bombadillo.GetSize()
  201. if len(args) > 0 {
  202. // If a url was passed, move it down the line
  203. // Goroutine so keypresses can be made during
  204. // page load
  205. bombadillo.Visit(args[0])
  206. } else {
  207. // Otherwise, load the homeurl
  208. // Goroutine so keypresses can be made during
  209. // page load
  210. bombadillo.Visit(bombadillo.Options["homeurl"])
  211. }
  212. // Loop indefinitely on user input
  213. for {
  214. bombadillo.TakeControlInput()
  215. }
  216. }