;;; ein-notebooklist.el --- Notebook list buffer -*- lexical-binding:t -*- ;; Copyright (C) 2018- John M. Miller ;; Authors: Takafumi Arakaki ;; John M. Miller ;; This file is NOT part of GNU Emacs. ;; ein-notebooklist.el is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ein-notebooklist.el is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with ein-notebooklist.el. If not, see . ;;; Commentary: ;;; Code: (require 'widget) (require 'cus-edit) (require 'ein-core) (require 'ein-contents-api) (require 'deferred) (require 'dash) (require 'ido) (declare-function ein:jupyter-crib-token "ein-jupyter") (declare-function ein:jupyter-get-default-kernel "ein-jupyter") (declare-function ein:jupyter-crib-running-servers "ein-jupyter") (declare-function ein:file-open "ein-file") (autoload 'ein:get-notebook "ein-notebook") (defcustom ein:notebooklist-login-timeout (truncate (* 6.3 1000)) "Timeout in milliseconds for logging into server" :group 'ein :type 'integer) (make-obsolete-variable 'ein:notebooklist-first-open-hook nil "0.17.0") (cl-defstruct ein:$notebooklist "Hold notebooklist variables. `ein:$notebooklist-url-or-port' URL or port of IPython server. `ein:$notebooklist-path' The path for the notebooklist. `ein:$notebooklist-data' JSON data sent from the server. `ein:$notebooklist-api-version' Major version of the IPython notebook server we are talking to." url-or-port path data api-version) (define-obsolete-variable-alias 'ein:notebooklist 'ein:%notebooklist% "0.1.2") (ein:deflocal ein:%notebooklist% nil "Buffer local variable to store an instance of `ein:$notebooklist'.") (ein:deflocal ein:%notebooklist-new-kernel% nil "Buffer local variable to store kernel type for newly created notebooks.") (defcustom ein:notebooklist-sort-field :name "The notebook list sort field." :type '(choice (const :tag "Name" :name) (const :tag "Last modified" :last_modified)) :group 'ein) (defcustom ein:notebooklist-sort-order :ascending "The notebook list sort order." :type '(choice (const :tag "Ascending" :ascending) (const :tag "Descending" :descending)) :group 'ein) (defvar ein:notebooklist-buffer-name-template "*ein:notebooklist %s*") (defvar ein:notebooklist-map (make-hash-table :test 'equal) "Data store for `ein:notebooklist-list'. Mapping from URL-OR-PORT to an instance of `ein:$notebooklist'.") (defun ein:notebooklist-keys () "Get a list of registered server urls." (hash-table-keys ein:notebooklist-map)) (defun ein:notebooklist-list () "Get a list of opened `ein:$notebooklist'." (hash-table-values ein:notebooklist-map)) (defun ein:notebooklist-list-remove (url-or-port) (remhash url-or-port ein:notebooklist-map)) (defun ein:notebooklist-list-add (nblist) "Register notebook list instance NBLIST for global lookup. This function adds NBLIST to `ein:notebooklist-map'." (puthash (ein:$notebooklist-url-or-port nblist) nblist ein:notebooklist-map)) (defun ein:notebooklist-list-get (url-or-port) "Get an instance of `ein:$notebooklist' by URL-OR-PORT as a key." (gethash url-or-port ein:notebooklist-map)) (defsubst ein:notebooklist-url (url-or-port &rest paths) (apply #'ein:url url-or-port "api/contents" paths)) (defun ein:notebooklist-sentinel (url-or-port process event) "Remove URL-OR-PORT from ein:notebooklist-map when PROCESS dies" (when (not (string= "open" (substring event 0 4))) (ein:log 'info "Process %s %s %s" (car (process-command process)) (replace-regexp-in-string "\n$" "" event) url-or-port) (ein:notebooklist-list-remove url-or-port))) (defun ein:notebooklist-get-buffer (url-or-port) (get-buffer-create (format ein:notebooklist-buffer-name-template url-or-port))) (defun ein:notebooklist-token-or-password (url-or-port) "Return token or password for URL-OR-PORT. Jupyter requires one or the other but not both. Return empty string token if all authentication disabled. Return nil if unclear what, if any, authentication applies." (cl-multiple-value-bind (password-p token) (ein:jupyter-crib-token url-or-port) (cond ((eq password-p t) (read-passwd (format "Password for %s: " url-or-port))) ((and (stringp token) (eq password-p :json-false)) token) (t nil)))) (defun ein:notebooklist-ask-url-or-port () (let* ((default (ein:url (aif (ein:get-notebook) (ein:$notebook-url-or-port it) (aif ein:%notebooklist% (ein:$notebooklist-url-or-port it))))) (url-or-port-list (-distinct (mapcar #'ein:url (append (when default (list default)) (if (stringp ein:urls) (list ein:urls) ein:urls) (mapcar (lambda (json) (cl-destructuring-bind (&key url &allow-other-keys) json (ein:url url))) (ein:jupyter-crib-running-servers)))))) (url-or-port (let (ido-report-no-match ido-use-faces) (ein:completing-read "URL or port: " url-or-port-list nil nil nil nil (car-safe url-or-port-list))))) (ein:url url-or-port))) (defsubst ein:notebooklist-canonical-url-or-port (url-host username) "Canonicalize. For the record, https://hub.data8x.berkeley.edu needs to look like https://hub.data8x.berkeley.edu/user/1dcdab3c2f59736806b85af865a1a28d" (ein:url url-host "user" username)) (cl-defun ein:notebooklist-open* (url-or-port &optional path resync callback errback hub-p &aux (canonical-p (not hub-p)) tokens-key) "Workhorse of `ein:login'. A notebooklist can be opened from any PATH within the server root hierarchy. PATH is empty at the root. RESYNC, when non-nil, requeries the contents-api version and kernelspecs. Full jupyterhub url is https://hub.data8x.berkeley.edu/user/1dcdab3c2f59736806b85af865a1a28d/?token=c421c6863ddb4e7ea5a311c31c948cd0 URL-HOST is hub.data8x.berkeley.edu USERNAME is 1dcdab3c2f59736806b85af865a1a28d TOKEN is c421c6863ddb4e7ea5a311c31c948cd0 CALLBACK takes two arguments, the resulting buffer and URL-OR-PORT. ERRBACK takes one argument, the resulting buffer." (setq path (or path "")) (if (and (not resync) (ein:notebooklist-list-get url-or-port)) (ein:content-query-contents url-or-port path (apply-partially #'ein:notebooklist-open--finish url-or-port callback) errback) (when hub-p (let* ((parsed-url (url-generic-parse-url url-or-port)) (url-host (url-host parsed-url)) (cookies (ein:query-get-cookies url-host "/user/")) (previous-users (mapcar (lambda (entry) (file-name-nondirectory (directory-file-name (plist-get entry :path)))) cookies)) (pq (url-path-and-query parsed-url)) (path0 (car pq)) (query (cdr pq)) (_ (setf canonical-p (and (stringp path0) (string-match "user/\\([a-z0-9]+\\)" path0)))) (username (if canonical-p (match-string-no-properties 1 path0) (read-no-blanks-input "User: " (car previous-users)))) (_ (setf url-or-port (ein:notebooklist-canonical-url-or-port url-host username))) (_ (setf tokens-key (ein:query-divine-authorization-tokens-key url-or-port))) (token (if (and (stringp query) (string-match "token=\\([a-z0-9]+\\)" query)) (prog1 (match-string-no-properties 1 query) (cl-assert canonical-p)) (when canonical-p (read-no-blanks-input "Token: "))))) (when token (setf (gethash tokens-key ein:query-authorization-tokens) token)))) (if (not canonical-p) ;; Retread to get _xsrf for canonical url (progn (ein:notebooklist-list-remove url-or-port) (ein:notebooklist-login--iteration url-or-port callback errback nil -1 nil)) (when tokens-key (let ((belay-tokens (lambda (&rest _args) (remhash tokens-key ein:query-authorization-tokens)))) (add-function :before (var errback) belay-tokens) (add-function :before (var callback) belay-tokens))) (ein:query-notebook-api-version url-or-port (lambda () (ein:query-kernelspecs url-or-port (lambda () (deferred:$ (deferred:next (lambda () (ein:content-query-hierarchy url-or-port)))) (ein:content-query-contents url-or-port path (apply-partially #'ein:notebooklist-open--finish url-or-port callback) errback)))))))) (make-obsolete-variable 'ein:notebooklist-keepalive-refresh-time nil "0.17.0") (make-obsolete-variable 'ein:enable-keepalive nil "0.17.0") (defcustom ein:notebooklist-date-format "%F" "The format spec for date in notebooklist mode. See `ein:format-time-string'." :type '(or string function) :group 'ein) (defun ein:notebooklist-open--finish (url-or-port callback content) "Called via `ein:notebooklist-open*'." (ein:log 'verbose "Opening notebooklist at %s" (ein:url url-or-port (ein:$content-path content))) (with-current-buffer (ein:notebooklist-get-buffer url-or-port) (ein:notebooklist-mode) (let ((restore-point (aand (widget-at) (awhen (widget-value it) (and (stringp it) it)) (string-match-p "Open\\|Stop\\|Delete" it) (point)))) (awhen ein:%notebooklist% (ein:notebooklist-list-remove (ein:$notebooklist-url-or-port it))) (setq ein:%notebooklist% (make-ein:$notebooklist :url-or-port url-or-port :path (ein:$content-path content) :data (ein:$content-raw-content content) :api-version (ein:$content-notebook-api-version content))) (ein:notebooklist-list-add ein:%notebooklist%) (let ((inhibit-read-only t)) (erase-buffer)) (when callback (funcall callback (current-buffer) url-or-port)) (ein:content-query-sessions url-or-port (apply-partially #'ein:notebooklist-render url-or-port restore-point)) (current-buffer)))) (cl-defun ein:notebooklist-open-error (url-or-port path &key error-thrown &allow-other-keys) (ein:log 'error "ein:notebooklist-open-error %s: ERROR %s DATA %s" (concat (file-name-as-directory url-or-port) path) (car error-thrown) (cdr error-thrown))) ;;;###autoload (defun ein:notebooklist-reload (&optional nblist resync callback) "Reload current Notebook list." (interactive) (setq nblist (or nblist ein:%notebooklist%)) (ein:notebooklist-open* (ein:$notebooklist-url-or-port nblist) (ein:$notebooklist-path nblist) resync callback)) ;;;###autoload (defun ein:notebooklist-new-notebook (url-or-port kernelspec &optional callback no-pop retry explicit-path) (interactive (list (ein:notebooklist-ask-url-or-port) (ein:completing-read "Select kernel: " (ein:list-available-kernels (ein:$notebooklist-url-or-port ein:%notebooklist%)) nil t nil nil "default" nil))) (let* ((notebooklist (ein:notebooklist-list-get url-or-port)) (path (or explicit-path (ein:$notebooklist-path notebooklist))) (url (ein:notebooklist-url url-or-port path))) (ein:query-singleton-ajax url :type "POST" :data (ein:json-encode '((type . "notebook"))) :headers (list (cons "Content-Type" "application/json")) :parser #'ein:json-read :error (apply-partially #'ein:notebooklist-new-notebook-error url-or-port kernelspec callback no-pop retry explicit-path) :success (apply-partially #'ein:notebooklist-new-notebook-success url-or-port kernelspec path callback no-pop)))) (cl-defun ein:notebooklist-new-notebook-success (url-or-port kernelspec path callback no-pop &key data &allow-other-keys) (let ((nbpath (plist-get data :path))) (ein:notebook-open url-or-port nbpath kernelspec callback nil no-pop) (ein:notebooklist-open* url-or-port path))) (cl-defun ein:notebooklist-new-notebook-error (url-or-port kernelspec callback no-pop retry explicit-path &key symbol-status error-thrown &allow-other-keys) (let ((notice (format "ein:notebooklist-new-notebook-error: %s %s" symbol-status error-thrown))) (if retry (ein:log 'error notice) (ein:log 'info notice) (sleep-for 0 1500) (ein:notebooklist-new-notebook url-or-port kernelspec callback no-pop t explicit-path)))) ;;;###autoload (defun ein:notebooklist-new-notebook-with-name (url-or-port kernelspec name &optional callback no-pop) "Upon notebook-open, rename the notebook, then funcall CALLBACK." (interactive (let ((url-or-port (ein:get-url-or-port))) (unless url-or-port (error "ein:notebooklist-new-notebook-with-name: no server context")) (let ((kernelspec (ein:completing-read "Select kernel: " (ein:list-available-kernels url-or-port) nil t nil nil "default" nil)) (name (read-from-minibuffer (format "Notebook name (at %s): " url-or-port)))) (list url-or-port kernelspec name)))) (unless callback (setq callback #'ignore)) (add-function :before (var callback) (apply-partially (lambda (name* notebook _created) (with-current-buffer (ein:notebook-buffer notebook) (ein:notebook-rename-command name*))) name)) (ein:notebooklist-new-notebook url-or-port kernelspec callback no-pop)) (defun ein:notebooklist-delete-notebook (_notebooklist url-or-port path &optional callback) "CALLBACK with no arguments, e.g., semaphore" (setq callback (or callback #'ignore)) (dolist (buf (seq-filter (lambda (b) (with-current-buffer b (aif (ein:get-notebook) (string= path (ein:$notebook-notebook-path it))))) (buffer-list))) (cl-letf (((symbol-function 'y-or-n-p) (lambda (&rest _args) nil))) (kill-buffer buf))) (if (ein:notebook-opened-notebooks (lambda (nb) (string= path (ein:$notebook-notebook-path nb)))) (ein:log 'error "ein:notebooklist-delete-notebook: cannot close %s" path) (let ((delete-nb (apply-partially (lambda (url* settings* _kernel) (apply #'ein:query-singleton-ajax url* settings*)) (ein:notebooklist-url url-or-port path) (list :type "DELETE" :complete (apply-partially #'ein:notebooklist-delete-notebook--complete (ein:url url-or-port path) callback))))) (ein:message-whir "Ending session" (var delete-nb) (ein:kernel-delete-session delete-nb :url-or-port url-or-port :path path))))) (cl-defun ein:notebooklist-delete-notebook--complete (_url callback &key data response _symbol-status &allow-other-keys &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data))) (ein:log 'debug "ein:notebooklist-delete-notebook--complete %s" resp-string) (when callback (funcall callback))) (defun generate-breadcrumbs (path) "Given notebooklist path, generate alist of breadcrumps of form (name . path)." (let* ((paths (split-string path "/" t)) (current-path "/") (pairs (list (cons "Home" "")))) (dolist (p paths pairs) (setf current-path (concat current-path "/" p) pairs (append pairs (list (cons p current-path))))))) (cl-defun ein:nblist--sort-group (group by-param order) (sort group #'(lambda (x y) (cond ((eq order :ascending) (string-lessp (plist-get x by-param) (plist-get y by-param))) ((eq order :descending) (string-greaterp (plist-get x by-param) (plist-get y by-param))))))) (defun ein:notebooklist--order-data (nblist-data sort-param sort-order) "Try to sanely sort the notebooklist data for the current path." (let* ((groups (-group-by (lambda (x) (plist-get x :type)) nblist-data)) (dirs (ein:nblist--sort-group (cdr (assoc "directory" groups)) sort-param sort-order)) (nbs (ein:nblist--sort-group (cdr (assoc "notebook" groups)) sort-param sort-order)) (files (ein:nblist--sort-group (-flatten-n 1 (-map #'cdr (-group-by #'(lambda (x) (car (last (split-string (plist-get x :name) "\\.")))) (cdr (assoc "file" groups))))) sort-param sort-order))) (-concat dirs nbs files))) (defun render-header (url-or-port &rest _args) (with-current-buffer (ein:notebooklist-get-buffer url-or-port) (widget-insert (format "Contents API %s (%s)\n\n" (ein:need-notebook-api-version url-or-port) url-or-port)) (let ((breadcrumbs (generate-breadcrumbs (ein:$notebooklist-path ein:%notebooklist%)))) (dolist (p breadcrumbs) (let ((url-or-port url-or-port) (name (car p)) (path (cdr p))) (widget-insert " | ") (widget-create 'link :notify (lambda (&rest _ignore) (ein:notebooklist-open* url-or-port path nil (lambda (buffer _url-or-port) (pop-to-buffer buffer)))) name))) (widget-insert " |\n\n")) (let* ((url-or-port url-or-port) (kernels (ein:list-available-kernels url-or-port))) (widget-create 'link :notify (lambda (&rest _ignore) (ein:notebooklist-new-notebook url-or-port ein:%notebooklist-new-kernel%)) "New Notebook") (widget-insert " ") (widget-create 'link :notify (lambda (&rest _ignore) (ein:notebooklist-reload nil t)) "Resync") (widget-insert " ") (widget-create 'link :notify (lambda (&rest _ignore) (browse-url (ein:url url-or-port))) "Open In Browser") (widget-insert "\n\nCreate New Notebooks Using Kernel:\n") (let ((radio-widget (widget-create 'radio-button-choice :notify (lambda (widget &rest _args) (let ((update (ein:get-kernelspec url-or-port (widget-value widget)))) (unless (equal ein:%notebooklist-new-kernel% update) (when ein:%notebooklist-new-kernel% (message "New notebooks started with %s kernel" (ein:$kernelspec-display-name update))) (setq ein:%notebooklist-new-kernel% update))))))) (if kernels (let ((initial (ein:jupyter-get-default-kernel kernels))) (dolist (k kernels) (let ((child (widget-radio-add-item radio-widget (list 'item :value (car k) :format (format "%s\n" (cdr k)))))) (when (string= initial (car k)) (widget-apply-action (widget-get child :button))))) (widget-insert "\n")) (widget-insert "\n No kernels found\n")))))) (defun ein:format-nbitem-data (name last-modified) (let ((dt (date-to-time last-modified))) (format "%-40s%+20s" name (ein:format-time-string ein:notebooklist-date-format dt)))) (defun render-directory (url-or-port sessions) ;; SESSIONS is a hashtable of path to (session-id . kernel-id) pairs (with-current-buffer (ein:notebooklist-get-buffer url-or-port) (cl-loop with reloader = (apply-partially (lambda (nblist _kernel) (ein:notebooklist-reload nblist)) ein:%notebooklist%) for note in (ein:notebooklist--order-data (ein:$notebooklist-data ein:%notebooklist%) ein:notebooklist-sort-field ein:notebooklist-sort-order) for name = (plist-get note :name) for path = (plist-get note :path) for last-modified = (plist-get note :last_modified) for type = (plist-get note :type) do (ein:notebook-get-opened-notebook url-or-port path) if (string= type "directory") do (progn (widget-create 'link :notify (let ((url-or-port url-or-port) (name name)) (lambda (&rest _ignore) ;; each directory creates a whole new notebooklist (ein:notebooklist-open* url-or-port (concat (file-name-as-directory (ein:$notebooklist-path ein:%notebooklist%)) name) nil (lambda (buffer _url-or-port) (pop-to-buffer buffer))))) "Dir") (widget-insert " : " name) (widget-insert "\n")) end if (string= type "file") do (progn (widget-create 'link :notify (apply-partially (lambda (url-or-port* path* &rest _args) (ein:file-open url-or-port* path*)) url-or-port path) "Open") (widget-insert " ") (widget-insert " : " (ein:format-nbitem-data name last-modified)) (widget-insert "\n")) end if (string= type "notebook") do (progn (widget-create 'link :notify (apply-partially (lambda (url-or-port* path* &rest _args) (ein:notebook-open url-or-port* path*)) url-or-port path) "Open") (widget-insert " ") (if (gethash path sessions) (widget-create 'link :notify (apply-partially (cl-function (lambda (url-or-port* path* &rest _ignore &aux (callback (lambda (_kernel) t))) (ein:message-whir "Ending session" (var callback) (ein:kernel-delete-session callback :url-or-port url-or-port* :path path*)))) url-or-port path) "Stop") (widget-insert "[----]")) (widget-insert " ") (widget-create 'link :notify (apply-partially (lambda (notebooklist* url-or-port* path* callback* &rest _args) (when (or noninteractive (y-or-n-p (format "Delete notebook %s?" path*))) (ein:notebooklist-delete-notebook notebooklist* url-or-port* path* (apply-partially callback* nil)))) ein:%notebooklist% url-or-port path reloader) "Delete") (widget-insert " : " (ein:format-nbitem-data name last-modified)) (widget-insert "\n")) end))) (defun ein:notebooklist-render (url-or-port restore-point sessions) (with-current-buffer (ein:notebooklist-get-buffer url-or-port) (if (not (ein:$notebooklist-path ein:%notebooklist%)) (ein:log 'error "ein:notebooklist-render: cannot render null") (render-header url-or-port sessions) (render-directory url-or-port sessions) (widget-setup) (awhen (get-buffer-window (current-buffer)) (set-window-point it (or restore-point (point-min))))))) ;;;###autoload (defun ein:notebooklist-list-paths (&optional content-type) "Return all files of CONTENT-TYPE for all sessions" (apply #'append (cl-loop for nblist in (ein:notebooklist-list) for url-or-port = (ein:$notebooklist-url-or-port nblist) collect (cl-loop for content in (ein:content-need-hierarchy url-or-port) when (or (null content-type) (string= (ein:$content-type content) content-type)) collect (ein:url url-or-port (ein:$content-path content)))))) (defun ein:notebooklist-parse-nbpath (nbpath) "Return `(,url-or-port ,path) from URL-OR-PORT/PATH" (cl-loop for url-or-port in (ein:notebooklist-keys) if (cl-search url-or-port nbpath :end2 (length url-or-port)) return (list (substring nbpath 0 (length url-or-port)) (substring nbpath (1+ (length url-or-port)))) end finally (ein:display-warning (format "%s not among: %s" nbpath (ein:notebooklist-keys)) :error))) (defsubst ein:notebooklist-ask-path (&optional content-type) (ein:completing-read (format "Open %s: " content-type) (ein:notebooklist-list-paths content-type) nil t)) ;;;###autoload (defun ein:notebooklist-load (&optional url-or-port) "Load notebook list but do not pop-up the notebook list buffer. For example, if you want to load notebook list when Emacs starts, add this in the Emacs initialization file: (add-to-hook \\='after-init-hook \\='ein:notebooklist-load) or even this (if you want fast Emacs start-up): ;; load notebook list if Emacs is idle for 3 sec after start-up (run-with-idle-timer 3 nil #\\='ein:notebooklist-load)" (ein:notebooklist-open* url-or-port)) ;;; Login (defun ein:notebooklist-login--iteration (url-or-port callback errback token iteration response-status) (ein:log 'debug "Login attempt #%d in response to %s from %s." iteration response-status url-or-port) (setq callback (or callback #'ignore)) (setq errback (or errback #'ignore)) (let* ((reset-p (not response-status)) (request-curl-options (if reset-p (cons "--junk-session-cookies" request-curl-options) request-curl-options)) (parsed-url (url-generic-parse-url (file-name-as-directory url-or-port))) (host (url-host parsed-url)) (query (cdr (url-path-and-query parsed-url)))) (when reset-p (remhash host ein:query-xsrf-cache)) (ein:query-singleton-ajax (ein:url url-or-port (if query "" "login")) ;; do not use :type "POST" here (see git history) :timeout ein:notebooklist-login-timeout :data (when (and token (not query)) (concat "password=" (url-hexify-string token))) :parser #'ein:notebooklist-login--parser :complete (apply-partially #'ein:notebooklist-login--complete url-or-port) :error (apply-partially #'ein:notebooklist-login--error url-or-port token callback errback iteration) :success (apply-partially #'ein:notebooklist-login--success url-or-port callback errback token iteration)))) ;;;###autoload (defun ein:notebooklist-open (url-or-port callback) "This is now an alias for `ein:notebooklist-login'." (interactive `(,(ein:notebooklist-ask-url-or-port) ,(lambda (buffer _url-or-port) (pop-to-buffer buffer)))) (ein:notebooklist-login url-or-port callback)) (make-obsolete 'ein:notebooklist-open 'ein:notebooklist-login "0.14.2") ;;;###autoload (defalias 'ein:login 'ein:notebooklist-login) ;;;###autoload (defun ein:notebooklist-login (url-or-port callback &optional cookie-name cookie-content token) "Deal with security before main entry of ein:notebooklist-open*. CALLBACK takes two arguments, the buffer created by ein:notebooklist-open--success and the url-or-port argument of ein:notebooklist-open*." (interactive `(,(ein:notebooklist-ask-url-or-port) ,(lambda (buffer _url-or-port) (pop-to-buffer buffer)) ,(when current-prefix-arg (read-no-blanks-input "Cookie name: ")) ,(when current-prefix-arg (read-no-blanks-input "Cookie content: ")) nil)) (when cookie-name (let* ((parsed-url (url-generic-parse-url (file-name-as-directory url-or-port))) (domain (url-host parsed-url)) (securep (string-match "^wss://" url-or-port)) (line (mapconcat #'identity (list domain "FALSE" (car (url-path-and-query parsed-url)) (if securep "TRUE" "FALSE") "0" cookie-name (concat cookie-content "\n")) "\t"))) (write-region line nil (request--curl-cookie-jar) 'append))) (let ((token (or token (ein:notebooklist-token-or-password url-or-port)))) (cond ((null token) ;; don't know (ein:notebooklist-login--iteration url-or-port callback nil nil -1 nil)) ((string= token "") ;; all authentication disabled (ein:log 'verbose "Skipping login %s" url-or-port) (ein:notebooklist-open* url-or-port nil nil callback nil)) (t (ein:notebooklist-login--iteration url-or-port callback nil token 0 nil))))) (defun ein:notebooklist-login--parser () (save-excursion (goto-char (point-min)) (when (re-search-forward "= iteration 0) (ein:notebooklist-login--error-1 url-or-port error-thrown response errback)) (hub-p (ein:notebooklist-open* url-or-port nil nil callback errback t)) (t (setq token (read-passwd (format "Password for %s: " url-or-port))) (ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status))) (ein:notebooklist-login--success-1 url-or-port callback errback hub-p))) (cl-defun ein:notebooklist-login--error (url-or-port token callback errback iteration &key _data response error-thrown &allow-other-keys &aux (response-status (request-response-status-code response)) (hub-p (request-response-header response "x-jupyterhub-version"))) (cond (hub-p (if (< iteration 0) (ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status) (if (and (eq response-status 405)) ;; no javascript is okay (ein:notebooklist-login--success-1 url-or-port callback errback hub-p) (ein:notebooklist-login--error-1 url-or-port error-thrown response errback)))) ((and response-status (< iteration 0)) (setq token (read-passwd (format "Password for %s: " url-or-port))) (ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status)) ((and (eq response-status 403) (< iteration 1)) (ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status)) (t (ein:notebooklist-login--error-1 url-or-port error-thrown response errback)))) (defun ein:get-url-or-port--notebooklist () (when (ein:$notebooklist-p ein:%notebooklist%) (ein:$notebooklist-url-or-port ein:%notebooklist%))) (defun ein:notebooklist-prev-item () (interactive) (move-beginning-of-line 0)) (defun ein:notebooklist-next-item () (interactive) (move-beginning-of-line 2)) (defvar ein:notebooklist-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map (make-composed-keymap widget-keymap special-mode-map)) (define-key map "\C-c\C-r" 'ein:notebooklist-reload) (define-key map "\C-c\C-f" 'ein:file-open) (define-key map "\C-c\C-o" 'ein:notebook-open) (define-key map "p" 'ein:notebooklist-prev-item) (define-key map "n" 'ein:notebooklist-next-item) map) "Keymap for ein:notebooklist-mode.") (easy-menu-define ein:notebooklist-menu ein:notebooklist-mode-map "EIN Notebook List Mode Menu" `("EIN Notebook List" ,@(ein:generate-menu '(("Reload" ein:notebooklist-reload) ("New Notebook" ein:notebooklist-new-notebook) ("New Notebook (with name)" ein:notebooklist-new-notebook-with-name))))) (define-derived-mode ein:notebooklist-mode special-mode "ein:notebooklist" "IPython notebook list mode. Commands: \\{ein:notebooklist-mode-map}" (set (make-local-variable 'revert-buffer-function) (lambda (&rest _args) (ein:notebooklist-reload)))) (provide 'ein-notebooklist) ;;; ein-notebooklist.el ends here