download_with_progress.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package tui
  3. import (
  4. "fmt"
  5. "os"
  6. "strings"
  7. "sync"
  8. "time"
  9. "kitty/tools/tui/loop"
  10. "kitty/tools/utils"
  11. "kitty/tools/utils/humanize"
  12. )
  13. var _ = fmt.Print
  14. type dl_data struct {
  15. mutex sync.Mutex
  16. canceled_by_user bool
  17. error_from_download error
  18. done, total uint64
  19. download_started bool
  20. download_finished bool
  21. temp_file_path string
  22. }
  23. type render_data struct {
  24. done, total uint64
  25. screen_width int
  26. spinner *Spinner
  27. started_at time.Time
  28. }
  29. func render_without_total(rd *render_data) string {
  30. return fmt.Sprint(rd.spinner.Tick(), humanize.Bytes(rd.done), " downloaded so far. Started %s", humanize.Time(rd.started_at))
  31. }
  32. func format_time(d time.Duration) string {
  33. d = d.Round(time.Second)
  34. ans := ""
  35. if d.Hours() > 1 {
  36. h := d / time.Hour
  37. d -= h * time.Hour
  38. ans += fmt.Sprintf("%02d:", h)
  39. }
  40. m := d / time.Minute
  41. d -= m * time.Minute
  42. s := d / time.Second
  43. return fmt.Sprintf("%s%02d:%02d", ans, m, s)
  44. }
  45. func render_progress(rd *render_data) string {
  46. if rd.total == 0 {
  47. return render_without_total(rd)
  48. }
  49. now := time.Now()
  50. duration := now.Sub(rd.started_at)
  51. rate := float64(rd.done) / float64(duration)
  52. frac := float64(rd.done) / float64(rd.total)
  53. bytes_left := rd.total - rd.done
  54. time_left := time.Duration(float64(bytes_left) / rate)
  55. speed := rate * float64(time.Second)
  56. before := rd.spinner.Tick()
  57. after := fmt.Sprintf(" %d%% %s/s %s", int(frac*100), strings.ReplaceAll(humanize.Bytes(uint64(speed)), " ", ""), format_time(time_left))
  58. available_width := rd.screen_width - len("T 100% 1000 MB/s 11:11:11")
  59. // fmt.Println("\r\n", frac, available_width)
  60. progress_bar := ""
  61. if available_width > 10 {
  62. progress_bar = " " + RenderProgressBar(frac, available_width)
  63. }
  64. return before + progress_bar + after
  65. }
  66. func DownloadFileWithProgress(destpath, url string, kill_if_signaled bool) (err error) {
  67. lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
  68. if err != nil {
  69. return
  70. }
  71. dl_data := dl_data{}
  72. rd := render_data{spinner: NewSpinner("dots"), started_at: time.Now()}
  73. register_temp_file_path := func(path string) {
  74. dl_data.mutex.Lock()
  75. dl_data.temp_file_path = path
  76. dl_data.mutex.Unlock()
  77. }
  78. report_progress := func(done, total uint64) error {
  79. dl_data.mutex.Lock()
  80. dl_data.done = done
  81. dl_data.total = total
  82. canceled := dl_data.canceled_by_user
  83. dl_data.mutex.Unlock()
  84. if canceled {
  85. return Canceled
  86. }
  87. lp.WakeupMainThread()
  88. return nil
  89. }
  90. do_download := func() {
  91. dl_data.mutex.Lock()
  92. dl_data.download_started = true
  93. dl_data.mutex.Unlock()
  94. err := utils.DownloadToFile(destpath, url, report_progress, register_temp_file_path)
  95. dl_data.mutex.Lock()
  96. dl_data.download_finished = true
  97. if err != Canceled && err != nil {
  98. dl_data.error_from_download = err
  99. }
  100. dl_data.mutex.Unlock()
  101. lp.WakeupMainThread()
  102. }
  103. redraw := func() {
  104. lp.StartAtomicUpdate()
  105. lp.AllowLineWrapping(false)
  106. defer func() {
  107. lp.AllowLineWrapping(true)
  108. lp.EndAtomicUpdate()
  109. }()
  110. lp.QueueWriteString("\r")
  111. lp.ClearToEndOfLine()
  112. dl_data.mutex.Lock()
  113. rd.done, rd.total = dl_data.done, dl_data.total
  114. dl_data.mutex.Unlock()
  115. if rd.done+rd.total == 0 {
  116. lp.QueueWriteString("Waiting for download to start...")
  117. } else {
  118. sz, err := lp.ScreenSize()
  119. w := sz.WidthCells
  120. if err != nil {
  121. w = 80
  122. }
  123. rd.screen_width = int(w)
  124. lp.QueueWriteString(render_progress(&rd))
  125. }
  126. }
  127. on_timer_tick := func(timer_id loop.IdType) error {
  128. return lp.OnWakeup()
  129. }
  130. lp.OnInitialize = func() (string, error) {
  131. if _, err = lp.AddTimer(rd.spinner.interval, true, on_timer_tick); err != nil {
  132. return "", err
  133. }
  134. go do_download()
  135. lp.QueueWriteString("Downloading: " + url + "\r\n")
  136. return "\r\n", nil
  137. }
  138. lp.OnResumeFromStop = func() error {
  139. redraw()
  140. return nil
  141. }
  142. lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
  143. redraw()
  144. return nil
  145. }
  146. lp.OnWakeup = func() error {
  147. dl_data.mutex.Lock()
  148. err := dl_data.error_from_download
  149. finished := dl_data.download_finished
  150. dl_data.mutex.Unlock()
  151. if err != nil {
  152. return dl_data.error_from_download
  153. }
  154. if finished {
  155. lp.Quit(0)
  156. return nil
  157. }
  158. redraw()
  159. return nil
  160. }
  161. lp.OnKeyEvent = func(event *loop.KeyEvent) error {
  162. if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") {
  163. event.Handled = true
  164. dl_data.mutex.Lock()
  165. dl_data.canceled_by_user = true
  166. dl_data.mutex.Unlock()
  167. return Canceled
  168. }
  169. return nil
  170. }
  171. err = lp.Run()
  172. dl_data.mutex.Lock()
  173. if dl_data.temp_file_path != "" && !dl_data.download_finished {
  174. os.Remove(dl_data.temp_file_path)
  175. }
  176. dl_data.mutex.Unlock()
  177. if err != nil {
  178. return
  179. }
  180. ds := lp.DeathSignalName()
  181. if ds != "" {
  182. if kill_if_signaled {
  183. lp.KillIfSignalled()
  184. return
  185. }
  186. return &KilledBySignal{Msg: fmt.Sprint("Killed by signal: ", ds), SignalName: ds}
  187. }
  188. return
  189. }