Personal emacs config
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.

1016 lines
44 KiB

  1. ;;; ein-notebooklist.el --- Notebook list buffer -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2018- John M. Miller
  3. ;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
  4. ;; John M. Miller <millejoh at mac.com>
  5. ;; This file is NOT part of GNU Emacs.
  6. ;; ein-notebooklist.el is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; ein-notebooklist.el is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with ein-notebooklist.el. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;;; Code:
  18. (require 'widget)
  19. (require 'cus-edit)
  20. (require 'ein-core)
  21. (require 'ein-notebook)
  22. (require 'ein-connect)
  23. (require 'ein-file)
  24. (require 'ein-contents-api)
  25. (require 'ein-subpackages)
  26. (require 'ein-ac)
  27. (require 'ein-company)
  28. (require 'deferred)
  29. (require 'dash)
  30. (require 'ido)
  31. (autoload 'ein:jupyterhub-connect "ein-jupyterhub")
  32. (defcustom ein:notebooklist-login-timeout (truncate (* 6.3 1000))
  33. "Timeout in milliseconds for logging into server"
  34. :group 'ein
  35. :type 'integer
  36. )
  37. (defcustom ein:notebooklist-render-order
  38. '(render-header
  39. render-opened-notebooks
  40. render-directory)
  41. "Order of notebook list sections.
  42. Must contain render-header, render-opened-notebooks, and render-directory."
  43. :group 'ein
  44. :type 'list
  45. )
  46. (defcustom ein:notebooklist-first-open-hook nil
  47. "Hooks to run when the notebook list is opened at first time.
  48. Example to open a notebook named _scratch_ when the notebook list
  49. is opened at first time.::
  50. (add-hook
  51. 'ein:notebooklist-first-open-hook
  52. (lambda () (ein:notebook-open (ein:$notebooklist-url-or-port ein:%notebooklist%) \"main.ipynb\")))
  53. "
  54. :type 'hook
  55. :group 'ein)
  56. (cl-defstruct ein:$notebooklist
  57. "Hold notebooklist variables.
  58. `ein:$notebooklist-url-or-port'
  59. URL or port of IPython server.
  60. `ein:$notebooklist-path'
  61. The path for the notebooklist.
  62. `ein:$notebooklist-data'
  63. JSON data sent from the server.
  64. `ein:$notebooklist-api-version'
  65. Major version of the IPython notebook server we are talking to."
  66. url-or-port
  67. path
  68. data
  69. api-version)
  70. (ein:deflocal ein:%notebooklist% nil
  71. "Buffer local variable to store an instance of `ein:$notebooklist'.")
  72. (ein:deflocal ein:%notebooklist-new-kernel% nil
  73. "Buffer local variable to store kernel type for newly created notebooks.")
  74. (defcustom ein:notebooklist-sort-field :name
  75. "The notebook list sort field."
  76. :type '(choice (const :tag "Name" :name)
  77. (const :tag "Last modified" :last_modified))
  78. :group 'ein)
  79. (make-variable-buffer-local 'ein:notebooklist-sort-field)
  80. (put 'ein:notebooklist-sort-field 'permanent-local t)
  81. (defcustom ein:notebooklist-sort-order :ascending
  82. "The notebook list sort order."
  83. :type '(choice (const :tag "Ascending" :ascending)
  84. (const :tag "Descending" :descending))
  85. :group 'ein)
  86. (make-variable-buffer-local 'ein:notebooklist-sort-order)
  87. (put 'ein:notebooklist-sort-order 'permanent-local t)
  88. (defmacro ein:make-sorting-widget (tag custom-var)
  89. "Create the sorting widget."
  90. ;; assume that custom-var has type `choice' of `const's.
  91. `(widget-create
  92. 'menu-choice :tag ,tag
  93. :value ,custom-var
  94. :notify (lambda (widget &rest _ignore)
  95. (run-at-time 1 nil #'ein:notebooklist-reload)
  96. (setq ,custom-var (widget-value widget)))
  97. ,@(mapcar (lambda (const)
  98. `'(item :tag ,(cl-third const) :value ,(cl-fourth const)))
  99. (cl-rest (custom-variable-type custom-var)))))
  100. (define-obsolete-variable-alias 'ein:notebooklist 'ein:%notebooklist% "0.1.2")
  101. (defvar ein:notebooklist-buffer-name-template "*ein:notebooklist %s*")
  102. (defvar ein:notebooklist-map (make-hash-table :test 'equal)
  103. "Data store for `ein:notebooklist-list'.
  104. Mapping from URL-OR-PORT to an instance of `ein:$notebooklist'.")
  105. (defun ein:notebooklist-keys ()
  106. "Get a list of registered server urls."
  107. (hash-table-keys ein:notebooklist-map))
  108. (defun ein:notebooklist-list ()
  109. "Get a list of opened `ein:$notebooklist'."
  110. (hash-table-values ein:notebooklist-map))
  111. (defun ein:notebooklist-list-remove (url-or-port)
  112. (remhash url-or-port ein:notebooklist-map))
  113. (defun ein:notebooklist-list-add (nblist)
  114. "Register notebook list instance NBLIST for global lookup.
  115. This function adds NBLIST to `ein:notebooklist-map'."
  116. (puthash (ein:$notebooklist-url-or-port nblist)
  117. nblist
  118. ein:notebooklist-map))
  119. (defun ein:notebooklist-list-get (url-or-port)
  120. "Get an instance of `ein:$notebooklist' by URL-OR-PORT as a key."
  121. (gethash url-or-port ein:notebooklist-map))
  122. (defun ein:notebooklist-url (url-or-port version &optional path)
  123. (let ((base-path (cond ((= version 2) "api/notebooks")
  124. ((>= version 3) "api/contents"))))
  125. (ein:url url-or-port base-path path)))
  126. (defun ein:notebooklist-sentinel (url-or-port process event)
  127. "Remove URL-OR-PORT from ein:notebooklist-map when PROCESS dies"
  128. (when (not (string= "open" (substring event 0 4)))
  129. (ein:log 'info "Process %s %s %s"
  130. (car (process-command process))
  131. (replace-regexp-in-string "\n$" "" event)
  132. url-or-port)
  133. (ein:notebooklist-list-remove url-or-port)))
  134. (defun ein:notebooklist-get-buffer (url-or-port)
  135. (get-buffer-create
  136. (format ein:notebooklist-buffer-name-template url-or-port)))
  137. (defvar ein:jupyter-default-server-command)
  138. (defun ein:jupyter-notebook-list (caller)
  139. "Return list of local notebooks as JSON."
  140. (condition-case err
  141. (mapcar #'ein:json-read-from-string
  142. (process-lines ein:jupyter-default-server-command
  143. "notebook" "list" "--json"))
  144. ;; often there is no local jupyter installation
  145. (error (ein:log 'info "ein:jupyter-notebook-list(%s): %s" caller err)
  146. nil)))
  147. (defun ein:crib-token (url-or-port)
  148. "Shell out to jupyter for its credentials knowledge. Return list of (PASSWORD TOKEN)."
  149. (ein:aif (cl-loop for json in (ein:jupyter-notebook-list 'ein:crib-token)
  150. with token0
  151. with password0
  152. when (cl-destructuring-bind (&key password url token &allow-other-keys) json
  153. (prog1 (equal (ein:url url) url-or-port)
  154. (setq password0 password) ;; t or :json-false
  155. (setq token0 token)))
  156. return (list password0 token0))
  157. it (list nil nil)))
  158. (defun ein:crib-running-servers ()
  159. "Shell out to jupyter for running servers."
  160. (cl-loop for json in (ein:jupyter-notebook-list 'ein:crib-running-servers)
  161. collecting (cl-destructuring-bind (&key url &allow-other-keys) json
  162. (ein:url url))))
  163. (defun ein:notebooklist-token-or-password (url-or-port)
  164. "Return token or password (jupyter requires one or the other but not both) for URL-OR-PORT. Empty string token means all authentication disabled. Nil means don't know."
  165. (cl-multiple-value-bind (password-p token) (ein:crib-token url-or-port)
  166. (autoload 'ein:jupyter-server-conn-info "ein-jupyter")
  167. (cl-multiple-value-bind (my-url-or-port my-token) (ein:jupyter-server-conn-info)
  168. (cond ((eq password-p t) (read-passwd (format "Password for %s: " url-or-port)))
  169. ((and (stringp token) (eql password-p :json-false)) token)
  170. ((equal url-or-port my-url-or-port) my-token)
  171. (t nil)))))
  172. (defun ein:notebooklist-ask-url-or-port ()
  173. (let* ((default (ein:url (ein:aif (ein:get-notebook)
  174. (ein:$notebook-url-or-port it)
  175. (ein:aif ein:%notebooklist%
  176. (ein:$notebooklist-url-or-port it)
  177. (ein:default-url-or-port)))))
  178. (url-or-port-list
  179. (-distinct (mapcar #'ein:url
  180. (append (list default)
  181. ein:url-or-port
  182. (ein:crib-running-servers)))))
  183. (url-or-port (let ((ido-report-no-match nil)
  184. (ido-use-faces nil))
  185. (ein:completing-read "URL or port: "
  186. url-or-port-list
  187. nil nil nil nil
  188. default))))
  189. (ein:url url-or-port)))
  190. (defun ein:notebooklist-open* (url-or-port &optional path resync restore-point-p callback errback)
  191. "The main entry to server at URL-OR-PORT. Users should not directly call this, but instead `ein:notebooklist-login'.
  192. A \"notebooklist\" can be opened from any PATH within the server root hierarchy. PATH is empty at the root. RESYNC is requery server attributes such as ipython version and kernelspecs. CALLBACK takes two arguments, the resulting buffer and URL-OR-PORT. ERRBACK takes one argument, the resulting buffer.
  193. TODO: going to maintain jupyterhub hooks here
  194. "
  195. (unless path (setq path ""))
  196. (setq url-or-port (ein:url url-or-port)) ;; should work towards not needing this
  197. (let* ((url-or-port url-or-port)
  198. (path path)
  199. (success (apply-partially #'ein:notebooklist-open--finish
  200. url-or-port restore-point-p callback))
  201. (failure errback))
  202. (if (and (not resync) (ein:notebooklist-list-get url-or-port))
  203. (ein:content-query-contents url-or-port path success failure)
  204. (ein:query-notebook-version
  205. url-or-port
  206. (lambda ()
  207. (ein:query-kernelspecs
  208. url-or-port
  209. (lambda ()
  210. (deferred:$
  211. (deferred:next
  212. (lambda ()
  213. (ein:content-query-hierarchy url-or-port #'ignore))))
  214. (ein:content-query-contents url-or-port path success failure))))))))
  215. (defcustom ein:notebooklist-keepalive-refresh-time 1
  216. "When the notebook keepalive is enabled, the frequency, IN
  217. HOURS, with which to make calls to the jupyter content API to
  218. refresh the notebook connection."
  219. :type 'float
  220. :group 'ein)
  221. (defcustom ein:enable-keepalive nil
  222. "When non-nil, will cause EIN to automatically call
  223. `ein:notebooklist-enable-keepalive' after any call to
  224. `ein:notebooklist-open'."
  225. :type 'boolean
  226. :group 'ein)
  227. (defcustom ein:notebooklist-date-format "%x"
  228. "The format spec for date in notebooklist mode.
  229. See `ein:format-time-string'."
  230. :type '(or string function)
  231. :group 'ein)
  232. (defvar ein:notebooklist--keepalive-timer nil)
  233. ;;;###autoload
  234. (defun ein:notebooklist-enable-keepalive (&optional url-or-port)
  235. "Enable periodic calls to the notebook server to keep long running sessions from expiring.
  236. By long running we mean sessions to last days, or weeks. The
  237. frequency of the refresh (which is very similar to a call to
  238. `ein:notebooklist-open`) is controlled by
  239. `ein:notebooklist-keepalive-refresh-time`, and is measured in
  240. terms of hours. If `ein:enable-keepalive' is non-nil this will
  241. automatically be called during calls to `ein:notebooklist-open`."
  242. (interactive (list (ein:notebooklist-ask-url-or-port)))
  243. (unless ein:notebooklist--keepalive-timer
  244. (message "Enabling notebooklist keepalive...")
  245. (let ((success
  246. (lambda (_content)
  247. (ein:log 'info "Refreshing notebooklist connection.")))
  248. (refresh-time (* ein:notebooklist-keepalive-refresh-time 60 60)))
  249. (setq ein:notebooklist--keepalive-timer
  250. (run-at-time 0.1 refresh-time #'ein:content-query-contents url-or-port "" success nil)))))
  251. ;;;###autoload
  252. (defun ein:notebooklist-disable-keepalive ()
  253. "Disable the notebooklist keepalive calls to the jupyter notebook server."
  254. (interactive)
  255. (message "Disabling notebooklist keepalive...")
  256. (cancel-timer ein:notebooklist--keepalive-timer)
  257. (setq ein:notebooklist--keepalive-timer nil))
  258. (defun ein:notebooklist-open--finish (url-or-port restore-point-p callback content)
  259. "Called via `ein:notebooklist-open*'."
  260. (let ((path (ein:$content-path content))
  261. (nb-version (ein:$content-notebook-version content))
  262. (data (ein:$content-raw-content content)))
  263. (with-current-buffer (ein:notebooklist-get-buffer url-or-port)
  264. (let ((already-opened-p (ein:notebooklist-list-get url-or-port))
  265. (orig-point (if restore-point-p
  266. (point)
  267. (point-min))))
  268. (setq ein:%notebooklist%
  269. (make-ein:$notebooklist :url-or-port url-or-port
  270. :path path
  271. :data data
  272. :api-version nb-version))
  273. (ein:notebooklist-list-add ein:%notebooklist%)
  274. (ein:notebooklist-render nb-version orig-point)
  275. (ein:log 'verbose "Opened notebooklist at %s" (ein:url url-or-port path))
  276. (unless already-opened-p
  277. (run-hooks 'ein:notebooklist-first-open-hook))
  278. (when ein:enable-keepalive
  279. (ein:notebooklist-enable-keepalive url-or-port))
  280. (when callback
  281. (funcall callback (current-buffer) url-or-port)))
  282. (current-buffer))))
  283. (cl-defun ein:notebooklist-open-error (url-or-port path
  284. &key error-thrown &allow-other-keys)
  285. (ein:log 'error
  286. "ein:notebooklist-open-error %s: ERROR %s DATA %s" (concat (file-name-as-directory url-or-port) path) (car error-thrown) (cdr error-thrown)))
  287. ;;;###autoload
  288. (defun ein:notebooklist-reload (&optional nblist resync callback)
  289. "Reload current Notebook list."
  290. (interactive)
  291. (unless nblist
  292. (setq nblist ein:%notebooklist%))
  293. (when nblist
  294. (ein:notebooklist-open* (ein:$notebooklist-url-or-port nblist)
  295. (ein:$notebooklist-path nblist) resync t
  296. callback)))
  297. (defun ein:notebooklist-refresh-related ()
  298. "Reload notebook list in which current notebook locates.
  299. This function is called via `ein:notebook-after-rename-hook'."
  300. (ein:notebooklist-open* (ein:$notebook-url-or-port ein:%notebook%)
  301. (ein:$notebook-notebook-path ein:%notebook%)))
  302. (add-hook 'ein:notebook-after-rename-hook 'ein:notebooklist-refresh-related)
  303. ;;;###autoload
  304. (defun ein:notebooklist-upload-file (upload-path)
  305. (interactive "fSelect file to upload:")
  306. (unless ein:%notebooklist%
  307. (error "Only works when called from an ein:notebooklist buffer."))
  308. (let ((nb-path (ein:$notebooklist-path ein:%notebooklist%)))
  309. (ein:content-upload nb-path upload-path)))
  310. ;;;###autoload
  311. (defun ein:notebooklist-new-notebook (url-or-port kernelspec &optional callback no-pop retry)
  312. (interactive (list (ein:notebooklist-ask-url-or-port)
  313. (ein:completing-read
  314. "Select kernel: "
  315. (ein:list-available-kernels
  316. (ein:$notebooklist-url-or-port ein:%notebooklist%))
  317. nil t nil nil "default" nil)))
  318. (let* ((notebooklist (ein:notebooklist-list-get url-or-port))
  319. (path (ein:$notebooklist-path notebooklist))
  320. (version (ein:$notebooklist-api-version notebooklist))
  321. (url (ein:notebooklist-url url-or-port version path)))
  322. (ein:query-singleton-ajax
  323. (list 'notebooklist-new-notebook url-or-port path)
  324. url
  325. :type "POST"
  326. :data (json-encode '((:type . "notebook")))
  327. :headers (list (cons "Content-Type" "application/json"))
  328. :parser #'ein:json-read
  329. :error (apply-partially #'ein:notebooklist-new-notebook-error
  330. url-or-port kernelspec path callback no-pop retry)
  331. :success (apply-partially #'ein:notebooklist-new-notebook-success
  332. url-or-port kernelspec path callback no-pop))))
  333. (cl-defun ein:notebooklist-new-notebook-success (url-or-port
  334. kernelspec
  335. path
  336. callback
  337. no-pop
  338. &key data &allow-other-keys)
  339. (let ((nbname (plist-get data :name))
  340. (nbpath (plist-get data :path)))
  341. (when (< (ein:notebook-version-numeric url-or-port) 3)
  342. (if (string= nbpath "")
  343. (setq nbpath nbname)
  344. (setq nbpath (format "%s/%s" nbpath nbname))))
  345. (ein:notebook-open url-or-port nbpath kernelspec callback nil no-pop)
  346. (ein:notebooklist-open* url-or-port path nil t)))
  347. (cl-defun ein:notebooklist-new-notebook-error
  348. (url-or-port kernelspec _path callback no-pop retry
  349. &key symbol-status error-thrown &allow-other-keys)
  350. (let ((notice (format "ein:notebooklist-new-notebook-error: %s %s"
  351. symbol-status error-thrown)))
  352. (if retry
  353. (ein:log 'error notice)
  354. (ein:log 'info notice)
  355. (sleep-for 0 1500)
  356. (ein:notebooklist-new-notebook url-or-port kernelspec callback no-pop t))))
  357. ;;;###autoload
  358. (defun ein:notebooklist-new-notebook-with-name
  359. (url-or-port kernelspec name &optional callback no-pop)
  360. "Upon notebook-open, rename the notebook, then funcall CALLBACK."
  361. (interactive
  362. (let* ((url-or-port (or (ein:get-url-or-port)
  363. (ein:default-url-or-port)))
  364. (kernelspec (ein:completing-read
  365. "Select kernel: "
  366. (ein:list-available-kernels url-or-port)
  367. nil t nil nil "default" nil))
  368. (name (read-from-minibuffer
  369. (format "Notebook name (at %s): " url-or-port))))
  370. (list url-or-port kernelspec name)))
  371. (unless callback
  372. (setq callback #'ignore))
  373. (add-function :before (var callback)
  374. (apply-partially
  375. (lambda (name* notebook _created)
  376. (with-current-buffer (ein:notebook-buffer notebook)
  377. (ein:notebook-rename-command name*)))
  378. name))
  379. (ein:notebooklist-new-notebook url-or-port kernelspec callback no-pop))
  380. (defun ein:notebooklist-delete-notebook-ask (path)
  381. (when (y-or-n-p (format "Delete notebook %s?" path))
  382. (ein:notebooklist-delete-notebook path)))
  383. (defun ein:notebooklist-delete-notebook (path &optional callback)
  384. "CALLBACK with no arguments, e.g., semaphore"
  385. (let* ((path path)
  386. (notebooklist ein:%notebooklist%)
  387. (callback callback)
  388. (url-or-port (ein:$notebooklist-url-or-port notebooklist)))
  389. (unless callback (setq callback (lambda () (ein:notebooklist-reload notebooklist))))
  390. (ein:query-singleton-ajax
  391. (list 'notebooklist-delete-notebook (ein:url url-or-port path))
  392. (ein:notebook-url-from-url-and-id
  393. url-or-port (ein:$notebooklist-api-version notebooklist) path)
  394. :type "DELETE"
  395. :complete (apply-partially #'ein:notebooklist-delete-notebook--complete (ein:url url-or-port path) callback))))
  396. (cl-defun ein:notebooklist-delete-notebook--complete (_url callback
  397. &key data response _symbol-status &allow-other-keys
  398. &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
  399. (ein:log 'debug "ein:notebooklist-delete-notebook--complete %s" resp-string)
  400. (when callback (funcall callback)))
  401. (defun generate-breadcrumbs (path)
  402. "Given notebooklist path, generate alist of breadcrumps of form (name . path)."
  403. (let* ((paths (split-string path "/" t))
  404. (current-path "/")
  405. (pairs (list (cons "Home" ""))))
  406. (dolist (p paths pairs)
  407. (setf current-path (concat current-path "/" p)
  408. pairs (append pairs (list (cons p current-path)))))))
  409. (cl-defun ein:nblist--sort-group (group by-param order)
  410. (sort group #'(lambda (x y)
  411. (cond ((eql order :ascending)
  412. (string-lessp (plist-get x by-param)
  413. (plist-get y by-param)))
  414. ((eql order :descending)
  415. (string-greaterp (plist-get x by-param)
  416. (plist-get y by-param)))))))
  417. (defun ein:notebooklist--order-data (nblist-data sort-param sort-order)
  418. "Try to sanely sort the notebooklist data for the current path."
  419. (let* ((groups (-group-by #'(lambda (x) (plist-get x :type))
  420. nblist-data))
  421. (dirs (ein:nblist--sort-group (cdr (assoc "directory" groups))
  422. sort-param
  423. sort-order))
  424. (nbs (ein:nblist--sort-group (cdr (assoc "notebook" groups))
  425. sort-param
  426. sort-order))
  427. (files (ein:nblist--sort-group (-flatten-n 1 (-map #'cdr (-group-by
  428. #'(lambda (x) (car (last (s-split "\\." (plist-get x :name)))))
  429. (cdr (assoc "file" groups)))))
  430. sort-param
  431. sort-order)))
  432. (-concat dirs nbs files)))
  433. (defun render-header-ipy2 (&rest _args)
  434. "Render the header (for ipython2)."
  435. ;; Create notebook list
  436. (widget-insert (format "IPython %s Notebook list\n\n" (ein:$notebooklist-api-version ein:%notebooklist%)))
  437. (let ((breadcrumbs (generate-breadcrumbs (ein:$notebooklist-path ein:%notebooklist%))))
  438. (dolist (p breadcrumbs)
  439. (let ((name (car p))
  440. (path (cdr p)))
  441. (widget-insert " | ")
  442. (widget-create
  443. 'link
  444. :notify (lambda (&rest _ignore)
  445. (ein:notebooklist-login
  446. (ein:$notebooklist-url-or-port ein:%notebooklist%) path))
  447. name)))
  448. (widget-insert " |\n\n"))
  449. (widget-create
  450. 'link
  451. :notify (lambda (&rest _ignore) (ein:notebooklist-new-notebook
  452. (ein:$notebooklist-url-or-port ein:%notebooklist%)
  453. nil))
  454. "New Notebook")
  455. (widget-insert " ")
  456. (widget-create
  457. 'link
  458. :notify (lambda (&rest _ignore) (ein:notebooklist-reload nil t))
  459. "Reload List")
  460. (widget-insert " ")
  461. (widget-create
  462. 'link
  463. :notify (lambda (&rest _ignore)
  464. (browse-url
  465. (ein:url (ein:$notebooklist-url-or-port ein:%notebooklist%))))
  466. "Open In Browser")
  467. (widget-insert "\n"))
  468. (defvar ein:jupyter-default-kernel)
  469. (defun render-header* (url-or-port &rest _args)
  470. "Render the header (for ipython>=3)."
  471. (with-current-buffer (ein:notebooklist-get-buffer url-or-port)
  472. (widget-insert
  473. (format "Contents API %s (%s)\n\n" (ein:need-notebook-version url-or-port) url-or-port))
  474. (let ((breadcrumbs (generate-breadcrumbs (ein:$notebooklist-path ein:%notebooklist%))))
  475. (dolist (p breadcrumbs)
  476. (let ((url-or-port url-or-port)
  477. (name (car p))
  478. (path (cdr p)))
  479. (widget-insert " | ")
  480. (widget-create
  481. 'link
  482. :notify (lambda (&rest _ignore)
  483. (ein:notebooklist-open* url-or-port path nil nil
  484. (lambda (buffer _url-or-port)
  485. (pop-to-buffer buffer))))
  486. name)))
  487. (widget-insert " |\n\n"))
  488. (let* ((url-or-port url-or-port)
  489. (kernels (ein:list-available-kernels url-or-port)))
  490. (if (null ein:%notebooklist-new-kernel%)
  491. (setq ein:%notebooklist-new-kernel% (ein:get-kernelspec url-or-port (caar kernels))))
  492. (widget-create
  493. 'link
  494. :notify (lambda (&rest _ignore)
  495. (ein:notebooklist-new-notebook url-or-port
  496. ein:%notebooklist-new-kernel%))
  497. "New Notebook")
  498. (widget-insert " ")
  499. (widget-create
  500. 'link
  501. :notify (lambda (&rest _ignore) (ein:notebooklist-reload nil t))
  502. "Resync")
  503. (widget-insert " ")
  504. (widget-create
  505. 'link
  506. :notify (lambda (&rest _ignore)
  507. (browse-url (ein:url url-or-port)))
  508. "Open In Browser")
  509. (widget-insert "\n\nCreate New Notebooks Using Kernel:\n")
  510. (let* ((radio-widget (widget-create 'radio-button-choice
  511. :value (and ein:%notebooklist-new-kernel% (ein:$kernelspec-name ein:%notebooklist-new-kernel%))
  512. :notify (lambda (widget &rest _ignore)
  513. (setq ein:%notebooklist-new-kernel%
  514. (ein:get-kernelspec url-or-port (widget-value widget)))
  515. (message "New notebooks will be started using the %s kernel."
  516. (ein:$kernelspec-display-name ein:%notebooklist-new-kernel%))))))
  517. (if (null kernels)
  518. (widget-insert "\n No kernels found.")
  519. (dolist (k kernels)
  520. (widget-radio-add-item radio-widget (list 'item
  521. :value (car k)
  522. :format (format "%s\n" (cdr k)))))
  523. (unless (eq ein:jupyter-default-kernel 'first-alphabetically)
  524. (widget-radio-value-set
  525. radio-widget
  526. (if (stringp ein:jupyter-default-kernel)
  527. ein:jupyter-default-kernel
  528. (symbol-name ein:jupyter-default-kernel))))
  529. (widget-insert "\n"))))))
  530. (defun render-opened-notebooks (url-or-port &rest _args)
  531. "Render the opened notebooks section (for ipython>=3)."
  532. ;; Opened Notebooks Section
  533. (with-current-buffer (ein:notebooklist-get-buffer url-or-port)
  534. (widget-insert "\n---------- All Opened Notebooks ----------\n\n")
  535. (cl-loop for buffer in (ein:notebook-opened-buffers)
  536. do (progn (widget-create
  537. 'link
  538. :notify (let ((buffer buffer))
  539. (lambda (&rest _ignore)
  540. (condition-case err
  541. (switch-to-buffer buffer)
  542. (error
  543. (message "%S" err)
  544. (ein:notebooklist-reload)))))
  545. "Open")
  546. (widget-create
  547. 'link
  548. :notify (let ((buffer buffer))
  549. (lambda (&rest _ignore)
  550. (if (buffer-live-p buffer)
  551. (kill-buffer buffer))
  552. (run-at-time 1 nil #'ein:notebooklist-reload)))
  553. "Close")
  554. (widget-insert " : " (buffer-name buffer))
  555. (widget-insert "\n")))))
  556. (defun ein:format-nbitem-data (name last-modified)
  557. (let ((dt (date-to-time last-modified)))
  558. (format "%-40s%+20s" name
  559. (ein:format-time-string ein:notebooklist-date-format dt))))
  560. (defun render-directory (url-or-port sessions)
  561. ;; SESSIONS is a hashtable of path to (session-id . kernel-id) pairs
  562. (with-current-buffer (ein:notebooklist-get-buffer url-or-port)
  563. (widget-insert "\n------------------------------------------\n\n")
  564. (ein:make-sorting-widget "Sort by" ein:notebooklist-sort-field)
  565. (ein:make-sorting-widget "In Order" ein:notebooklist-sort-order)
  566. (widget-insert "\n")
  567. (cl-loop for note in (ein:notebooklist--order-data (ein:$notebooklist-data ein:%notebooklist%)
  568. ein:notebooklist-sort-field
  569. ein:notebooklist-sort-order)
  570. for name = (plist-get note :name)
  571. for path = (plist-get note :path)
  572. for last-modified = (plist-get note :last_modified)
  573. for type = (plist-get note :type)
  574. ;; for opened-notebook-maybe = (ein:notebook-get-opened-notebook url-or-port path)
  575. do (widget-insert " ")
  576. if (string= type "directory")
  577. do (progn (widget-create
  578. 'link
  579. :notify (let ((url-or-port url-or-port)
  580. (name name))
  581. (lambda (&rest _ignore)
  582. ;; each directory creates a whole new notebooklist
  583. (ein:notebooklist-open* url-or-port
  584. (concat (file-name-as-directory
  585. (ein:$notebooklist-path ein:%notebooklist%))
  586. name)
  587. nil nil
  588. (lambda (buffer _url-or-port) (pop-to-buffer buffer)))))
  589. "Dir")
  590. (widget-insert " : " name)
  591. (widget-insert "\n"))
  592. if (and (string= type "file") (> (ein:notebook-version-numeric url-or-port) 2))
  593. do (progn (widget-create
  594. 'link
  595. :notify (let ((url-or-port url-or-port)
  596. (path path))
  597. (lambda (&rest _ignore)
  598. (ein:file-open url-or-port path)))
  599. "Open")
  600. (widget-insert " ------ ")
  601. (widget-create
  602. 'link
  603. :notify (let ((_path path))
  604. (lambda (&rest _ignore)
  605. (ein:file-delete url-or-port path)))
  606. "Delete")
  607. (widget-insert " : " (ein:format-nbitem-data name last-modified))
  608. (widget-insert "\n"))
  609. if (string= type "notebook")
  610. do (progn (widget-create
  611. 'link
  612. :notify (let ((url-or-port url-or-port)
  613. (path path))
  614. (lambda (&rest _ignore)
  615. (run-at-time 3 nil #'ein:notebooklist-reload)
  616. (ein:notebook-open url-or-port path)))
  617. "Open")
  618. (widget-insert " ")
  619. (if (gethash path sessions)
  620. (widget-create
  621. 'link
  622. :notify (let ((url url-or-port)
  623. (session (car (gethash path sessions))))
  624. (lambda (&rest _ignore)
  625. (ein:kernel-delete--from-session-id url session #'ein:notebooklist-reload)))
  626. "Stop")
  627. (widget-insert "------"))
  628. (widget-insert " ")
  629. (widget-create
  630. 'link
  631. :notify (let ((path path))
  632. (lambda (&rest _ignore)
  633. (ein:notebooklist-delete-notebook-ask
  634. path)))
  635. "Delete")
  636. (widget-insert " : " (ein:format-nbitem-data name last-modified))
  637. (widget-insert "\n")))))
  638. (defun ein:notebooklist-render (nb-version &optional restore-point)
  639. "Render notebook list widget.
  640. Notebook list data is passed via the buffer local variable
  641. `ein:notebooklist-data'."
  642. (kill-all-local-variables)
  643. (let ((inhibit-read-only t))
  644. (erase-buffer))
  645. (remove-overlays)
  646. (let ((url-or-port (ein:$notebooklist-url-or-port ein:%notebooklist%)))
  647. (ein:content-query-sessions url-or-port
  648. (apply-partially #'ein:notebooklist-render--finish nb-version url-or-port restore-point)
  649. nil)))
  650. (defun ein:notebooklist-render--finish (nb-version url-or-port restore-point sessions)
  651. (cl-letf (((symbol-function 'render-header) (if (< nb-version 3)
  652. #'render-header-ipy2
  653. #'render-header*)))
  654. (mapc (lambda (x) (funcall (symbol-function x) url-or-port sessions))
  655. ein:notebooklist-render-order))
  656. (with-current-buffer (ein:notebooklist-get-buffer url-or-port)
  657. (ein:notebooklist-mode)
  658. (widget-setup)
  659. (goto-char (or restore-point (point-min)))))
  660. ;;;###autoload
  661. (defun ein:notebooklist-list-paths (&optional content-type)
  662. "Return all files of CONTENT-TYPE for all sessions"
  663. (apply #'append
  664. (cl-loop for nblist in (ein:notebooklist-list)
  665. for url-or-port = (ein:$notebooklist-url-or-port nblist)
  666. collect
  667. (cl-loop for content in (ein:content-need-hierarchy url-or-port)
  668. when (or (null content-type)
  669. (string= (ein:$content-type content) content-type))
  670. collect (ein:url url-or-port (ein:$content-path content))))))
  671. (defun ein:notebooklist-parse-nbpath (nbpath)
  672. "Return `(,url-or-port ,path) from URL-OR-PORT/PATH"
  673. (cl-loop for url-or-port in (ein:notebooklist-keys)
  674. if (cl-search url-or-port nbpath :end2 (length url-or-port))
  675. return (list (substring nbpath 0 (length url-or-port))
  676. (substring nbpath (1+ (length url-or-port))))
  677. end
  678. finally (ein:display-warning
  679. (format "%s not among: %s" nbpath (ein:notebooklist-keys))
  680. :error)))
  681. (defsubst ein:notebooklist-ask-path (&optional content-type)
  682. (ein:completing-read (format "Open %s: " content-type)
  683. (ein:notebooklist-list-paths content-type)
  684. nil t))
  685. ;;;###autoload
  686. (defun ein:notebooklist-load (&optional url-or-port)
  687. "Load notebook list but do not pop-up the notebook list buffer.
  688. For example, if you want to load notebook list when Emacs starts,
  689. add this in the Emacs initialization file::
  690. (add-to-hook 'after-init-hook 'ein:notebooklist-load)
  691. or even this (if you want fast Emacs start-up)::
  692. ;; load notebook list if Emacs is idle for 3 sec after start-up
  693. (run-with-idle-timer 3 nil #'ein:notebooklist-load)
  694. You should setup `ein:url-or-port' or `ein:default-url-or-port'
  695. in order to make this code work.
  696. See also:
  697. `ein:connect-to-default-notebook', `ein:connect-default-notebook'."
  698. (ein:notebooklist-open* url-or-port))
  699. ;;; Login
  700. (defun ein:notebooklist-login--iteration (url-or-port callback errback token iteration response-status)
  701. (ein:log 'debug "Login attempt #%d in response to %s from %s."
  702. iteration response-status url-or-port)
  703. (unless callback
  704. (setq callback #'ignore))
  705. (unless errback
  706. (setq errback #'ignore))
  707. (ein:query-singleton-ajax
  708. (list 'notebooklist-login--iteration url-or-port)
  709. (ein:url url-or-port "login")
  710. ;; do not use :type "POST" here (see git history)
  711. :timeout ein:notebooklist-login-timeout
  712. :data (if token (concat "password=" (url-hexify-string token)))
  713. :parser #'ein:notebooklist-login--parser
  714. :complete (apply-partially #'ein:notebooklist-login--complete url-or-port)
  715. :error (apply-partially #'ein:notebooklist-login--error url-or-port token
  716. callback errback iteration)
  717. :success (apply-partially #'ein:notebooklist-login--success url-or-port callback
  718. errback token iteration)))
  719. ;;;###autoload
  720. (defun ein:notebooklist-open (url-or-port callback)
  721. "This is now an alias for ein:notebooklist-login"
  722. (interactive `(,(ein:notebooklist-ask-url-or-port)
  723. ,(lambda (buffer _url-or-port) (pop-to-buffer buffer))))
  724. (ein:notebooklist-login url-or-port callback))
  725. (make-obsolete 'ein:notebooklist-open 'ein:notebooklist-login "0.14.2")
  726. ;;;###autoload
  727. (defalias 'ein:login 'ein:notebooklist-login)
  728. (defun ein:notebooklist-ask-user-pw-pair (user-prompt pw-prompt)
  729. "Currently used for cookie and jupyterhub additional inputs. If we need more than one cookie, we first need to ask for how many. Returns list of name and content."
  730. (plist-put nil (intern (read-no-blanks-input (format "%s: " user-prompt)))
  731. (read-no-blanks-input (format "%s: " pw-prompt))))
  732. ;;;###autoload
  733. (defun ein:notebooklist-login (url-or-port callback &optional cookie-plist)
  734. "Deal with security before main entry of ein:notebooklist-open*.
  735. CALLBACK takes two arguments, the buffer created by ein:notebooklist-open--success
  736. and the url-or-port argument of ein:notebooklist-open*."
  737. (interactive `(,(ein:notebooklist-ask-url-or-port)
  738. ,(lambda (buffer _url-or-port) (pop-to-buffer buffer))
  739. ,(if current-prefix-arg (ein:notebooklist-ask-user-pw-pair "Cookie name" "Cookie content"))))
  740. (unless callback (setq callback (lambda (_buffer _url-or-port))))
  741. (when cookie-plist
  742. (let* ((parsed-url (url-generic-parse-url (file-name-as-directory url-or-port)))
  743. (domain (url-host parsed-url))
  744. (securep (string-match "^wss://" url-or-port)))
  745. (cl-loop for (name content) on cookie-plist by (function cddr)
  746. for line = (mapconcat #'identity (list domain "FALSE" (car (url-path-and-query parsed-url)) (if securep "TRUE" "FALSE") "0" (symbol-name name) (concat content "\n")) "\t")
  747. do (write-region line nil (request--curl-cookie-jar) 'append))))
  748. (let ((token (ein:notebooklist-token-or-password url-or-port)))
  749. (cond ((null token) ;; don't know
  750. (ein:notebooklist-login--iteration url-or-port callback nil nil -1 nil))
  751. ((string= token "") ;; all authentication disabled
  752. (ein:log 'verbose "Skipping login %s" url-or-port)
  753. (ein:notebooklist-open* url-or-port nil nil nil callback nil))
  754. (t (ein:notebooklist-login--iteration url-or-port callback nil token 0 nil)))))
  755. (defun ein:notebooklist-login--parser ()
  756. (goto-char (point-min))
  757. (list :bad-page (re-search-forward "<input type=.?password" nil t)))
  758. (defun ein:notebooklist-login--success-1 (url-or-port callback errback)
  759. (ein:log 'info "Login to %s complete." url-or-port)
  760. (ein:notebooklist-open* url-or-port nil nil nil callback errback))
  761. (defun ein:notebooklist-login--error-1 (url-or-port error-thrown response errback)
  762. (ein:log 'error "Login to %s failed, error-thrown %s, raw-header %s"
  763. url-or-port
  764. (subst-char-in-string ?\n ?\ (format "%s" error-thrown))
  765. (request-response--raw-header response))
  766. (funcall errback))
  767. (cl-defun ein:notebooklist-login--complete (_url-or-port &key data response &allow-other-keys
  768. &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
  769. (ein:log 'debug "ein:notebooklist-login--complete %s" resp-string))
  770. (cl-defun ein:notebooklist-login--success (url-or-port callback errback token iteration
  771. &key data response error-thrown &allow-other-keys
  772. &aux (response-status (request-response-status-code response)))
  773. (cond ((plist-get data :bad-page)
  774. (if (>= iteration 0)
  775. (ein:notebooklist-login--error-1 url-or-port error-thrown response errback)
  776. (setq token (read-passwd (format "Password for %s: " url-or-port)))
  777. (ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status)))
  778. ((request-response-header response "x-jupyterhub-version")
  779. (let ((pam-plist (ein:notebooklist-ask-user-pw-pair "User" "Password")))
  780. (cl-destructuring-bind (user pw)
  781. (cl-loop for (user pw) on pam-plist by (function cddr)
  782. return (list (symbol-name user) pw))
  783. (ein:jupyterhub-connect url-or-port user pw callback))))
  784. (t (ein:notebooklist-login--success-1 url-or-port callback errback))))
  785. (cl-defun ein:notebooklist-login--error
  786. (url-or-port token callback errback iteration
  787. &key
  788. _data
  789. symbol-status
  790. response
  791. error-thrown
  792. &allow-other-keys
  793. &aux (response-status (request-response-status-code response)))
  794. (cond ((and response-status (< iteration 0))
  795. (setq token (read-passwd (format "Password for %s: " url-or-port)))
  796. (ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status))
  797. ((and (eq response-status 403) (< iteration 1))
  798. (ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status))
  799. ((and (eq symbol-status 'timeout) ;; workaround for url-retrieve backend
  800. (eq response-status 302)
  801. (request-response-header response "set-cookie"))
  802. (ein:notebooklist-login--success-1 url-or-port callback errback))
  803. (t (ein:notebooklist-login--error-1 url-or-port error-thrown response errback))))
  804. ;;;###autoload
  805. (defun ein:notebooklist-change-url-port (new-url-or-port)
  806. "Update the ipython/jupyter notebook server URL for all the
  807. notebooks currently opened from the current notebooklist buffer.
  808. This function works by calling `ein:notebook-update-url-or-port'
  809. on all the notebooks opened from the current notebooklist."
  810. (interactive (list (ein:notebooklist-ask-url-or-port)))
  811. (unless (eql major-mode 'ein:notebooklist-mode)
  812. (error "This command needs to be called from within a notebooklist buffer."))
  813. (let* ((current-nblist ein:%notebooklist%)
  814. (old-url (ein:$notebooklist-url-or-port current-nblist))
  815. (new-url-or-port new-url-or-port)
  816. (open-nb (ein:notebook-opened-notebooks #'(lambda (nb)
  817. (equal (ein:$notebook-url-or-port nb)
  818. (ein:$notebooklist-url-or-port current-nblist))))))
  819. (ein:notebooklist-open* new-url-or-port)
  820. (cl-loop for x upfrom 0 by 1
  821. until (or (get-buffer (format ein:notebooklist-buffer-name-template new-url-or-port))
  822. (= x 100))
  823. do (sit-for 0.1))
  824. (dolist (nb open-nb)
  825. (ein:notebook-update-url-or-port new-url-or-port nb))
  826. (kill-buffer (ein:notebooklist-get-buffer old-url))
  827. (ein:notebooklist-open* new-url-or-port nil nil nil (lambda (buffer _url-or-port)
  828. (pop-to-buffer buffer)))))
  829. (defun ein:notebooklist-change-url-port--deferred (new-url-or-port)
  830. (let* ((current-nblist ein:%notebooklist%)
  831. (old-url (ein:$notebooklist-url-or-port current-nblist))
  832. (new-url-or-port new-url-or-port)
  833. (open-nb (ein:notebook-opened-notebooks
  834. (lambda (nb)
  835. (equal (ein:$notebook-url-or-port nb)
  836. (ein:$notebooklist-url-or-port current-nblist))))))
  837. (deferred:$
  838. (deferred:next
  839. (lambda ()
  840. (ein:notebooklist-open* new-url-or-port)
  841. (cl-loop until (get-buffer (format ein:notebooklist-buffer-name-template new-url-or-port))
  842. do (sit-for 0.1))))
  843. (deferred:nextc it
  844. (lambda ()
  845. (dolist (nb open-nb)
  846. (ein:notebook-update-url-or-port new-url-or-port nb))))
  847. (deferred:nextc it
  848. (lambda ()
  849. (kill-buffer (ein:notebooklist-get-buffer old-url))
  850. (ein:notebooklist-open* new-url-or-port nil nil nil (lambda (buffer _url-or-port)
  851. (pop-to-buffer buffer))))))))
  852. ;;; Generic getter
  853. (defun ein:get-url-or-port--notebooklist ()
  854. (when (ein:$notebooklist-p ein:%notebooklist%)
  855. (ein:$notebooklist-url-or-port ein:%notebooklist%)))
  856. ;;; Notebook list mode
  857. (defun ein:notebooklist-prev-item () (interactive) (move-beginning-of-line 0))
  858. (defun ein:notebooklist-next-item () (interactive) (move-beginning-of-line 2))
  859. (defvar ein:notebooklist-mode-map
  860. (let ((map (make-sparse-keymap)))
  861. (set-keymap-parent map (make-composed-keymap widget-keymap
  862. special-mode-map))
  863. (define-key map "\C-c\C-r" 'ein:notebooklist-reload)
  864. (define-key map "\C-c\C-f" 'ein:file-open)
  865. (define-key map "\C-c\C-o" 'ein:notebook-open)
  866. (define-key map "p" 'ein:notebooklist-prev-item)
  867. (define-key map "n" 'ein:notebooklist-next-item)
  868. map)
  869. "Keymap for ein:notebooklist-mode.")
  870. (easy-menu-define ein:notebooklist-menu ein:notebooklist-mode-map
  871. "EIN Notebook List Mode Menu"
  872. `("EIN Notebook List"
  873. ,@(ein:generate-menu
  874. '(("Reload" ein:notebooklist-reload)
  875. ("New Notebook" ein:notebooklist-new-notebook)
  876. ("New Notebook (with name)"
  877. ein:notebooklist-new-notebook-with-name)))))
  878. (defun ein:notebooklist-revert-wrapper (&optional _ignore-auto _noconfirm _preserve-modes)
  879. (ein:notebooklist-reload))
  880. (define-derived-mode ein:notebooklist-mode special-mode "ein:notebooklist"
  881. "IPython notebook list mode.
  882. Commands:
  883. \\{ein:notebooklist-mode-map}"
  884. (set (make-local-variable 'revert-buffer-function)
  885. 'ein:notebooklist-revert-wrapper))
  886. (provide 'ein-notebooklist)
  887. ;;; ein-notebooklist.el ends here