Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1328e38
Add `lem-dashboard`
jfaz1 Jul 25, 2024
5ce2c73
Load `lem-dashboard` with lem
jfaz1 Jul 25, 2024
a704d21
Initial dashboard attempt with dummy data
jfaz1 Jul 25, 2024
e81caf5
Center project list
jfaz1 Jul 26, 2024
0a9f71a
Export `saved-projects`
jfaz1 Jul 27, 2024
24721aa
Use real projects
jfaz1 Jul 27, 2024
ec4be1f
Add project opening functionality
jfaz1 Jul 27, 2024
c52c1bd
Add next/previous shortcuts
jfaz1 Jul 27, 2024
72c7c24
Add recent files
jfaz1 Jul 30, 2024
7acdae9
Set maximum project limit
jfaz1 Jul 31, 2024
4a77ebc
Add project/file limit variables
jfaz1 Aug 1, 2024
75a7374
Add icons, format a bit better, misc. changes
jfaz1 Aug 2, 2024
49081a2
Add attributes + divider
jfaz1 Aug 8, 2024
f07562b
Add open-lem-docs/open-lem-github shortcuts, make buttons
jfaz1 Aug 11, 2024
90171d3
Remove divider
jfaz1 Aug 12, 2024
9dbb4a7
Add working dir
jfaz1 Aug 12, 2024
d84ec76
Add vi keybinds
jfaz1 Aug 12, 2024
3b147ad
Add footer message/MOTD
jfaz1 Aug 12, 2024
51d2165
Add startup hook + resizing
jfaz1 Aug 12, 2024
7948d4c
Add custom layout, user variables
jfaz1 Aug 12, 2024
ada2363
Invert recent file list
jfaz1 Aug 12, 2024
10c5dda
Export `lisp-scratch`
jfaz1 Aug 12, 2024
9b1907e
Add `dashboard-enable` var
jfaz1 Aug 12, 2024
c3504c7
Generalize dashboard features with CLOS
jfaz1 Aug 13, 2024
fdf356a
Cache footer/splash messages for resizing
jfaz1 Aug 14, 2024
3f90e47
Refactor, move default dashboard to its own file
jfaz1 Aug 15, 2024
f909ab6
Fix missing icons at build time
jfaz1 Aug 15, 2024
476f24a
Add default actions
jfaz1 Aug 15, 2024
974e8a2
Add default dashboard creation function
jfaz1 Aug 16, 2024
e77103f
Break `vertical-padding` into top/bottom margin
jfaz1 Aug 16, 2024
3bccc4d
Use window size for create-centered-string
jfaz1 Aug 17, 2024
8a53cb5
Move dashboard-items to its own file
jfaz1 Aug 17, 2024
8cfdef4
Remove keybind/keybind-command from dashboard-item
jfaz1 Aug 24, 2024
35f3423
Use `*splash-function*` instead of init hook
jfaz1 Aug 24, 2024
bc9e51c
Fix scratch/docs icon
jfaz1 Aug 24, 2024
3d578bc
Use str, minor refactor
jfaz1 Aug 24, 2024
cb68a56
Use smaller ascii art
jfaz1 Aug 25, 2024
9415e8a
Refactor (Use local-nicknames, use with-buffer-read-only, add dashboa…
jfaz1 Aug 25, 2024
0060f1e
Move default dashboard keybinds
jfaz1 Aug 25, 2024
4d0d9a9
Even smaller ascii art
jfaz1 Aug 25, 2024
0e1c92a
Switch to project before finding file
jfaz1 Aug 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions extensions/lem-dashboard/dashboard-items.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
(in-package :lem-dashboard)

;; Splash
(defclass dashboard-splash (dashboard-item)
((splash-texts :initarg :splash-texts :accessor splash-texts
:initform '("Welcome!"))
(selected-splash :initarg :selected-splash :accessor selected-splash
:initform nil))
(:documentation "Randomly displays one of SPLASH-TEXTS")
(:default-initargs
:item-attribute 'document-text-attribute))

(defmethod draw-dashboard-item ((item dashboard-splash) point)
(unless (selected-splash item)
(setf (selected-splash item)
(nth (random (length (splash-texts item))) (splash-texts item))))
(dolist (line (str:lines (selected-splash item)))
(insert-string point (create-centered-string line) :attribute (item-attribute item))
(insert-character point #\Newline)))

;; Url
(defclass dashboard-url (dashboard-item)
((url :initarg :url :accessor url)
(display-text :initarg :display-text :accessor display-text))
(:documentation "Creates link/button with DISPLAY-TEXT that opens URL externally.")
(:default-initargs
:item-attribute 'document-text-attribute))

(defmethod initialize-instance :after ((item dashboard-url) &key)
(with-slots (url action) item
(setf action (lambda () (open-external-file url)))))

(defmethod draw-dashboard-item ((item dashboard-url) point)
(button:insert-button point
(create-centered-string (display-text item))
(lambda () (open-external-file (url item)))
:attribute (item-attribute item)))

;; Working dir
(defclass dashboard-working-dir (dashboard-item)
()
(:documentation "Prints current working directory")
(:default-initargs
:item-attribute 'document-header4-attribute))

(defmethod draw-dashboard-item ((item dashboard-working-dir) point)
(let ((working-dir (format nil "> ~A" (buffer-directory))))
(insert-string point (create-centered-string working-dir) :attribute 'document-header4-attribute)
(insert-character point #\Newline)))

;; Footer
(defclass dashboard-footer-message (dashboard-item)
((messages :initarg :messages :accessor messages :initform '("Happy Coding!"))
(selected-message :initarg :selected-message :accessor selected-message
:initform nil))
(:documentation "Randomly displays one of the passed-in MESSAGES")
(:default-initargs
:item-attribute 'document-text-attribute))

(defmethod draw-dashboard-item ((item dashboard-footer-message) point)
(unless (selected-message item)
(setf (selected-message item)
(nth (random (length (messages item))) (messages item))))
(insert-string point
(create-centered-string (format nil "> ~A" (selected-message item)))
:attribute (item-attribute item)))

;; Command
(defclass dashboard-command (dashboard-item)
((display-text :initarg :display-text :accessor display-text)
(action-command :initarg :action-command :accessor action-command))
(:documentation "Creates a link/button with DISPLAY-TEXT that executes ACTION-COMMAND when clicked.")
(:default-initargs
:item-attribute 'document-text-attribute))

(defmethod initialize-instance :after ((item dashboard-command) &key)
(with-slots (action-command action) item
(setf action (lambda () (funcall action-command)))))

(defmethod draw-dashboard-item ((item dashboard-command) point)
(button:insert-button point
(create-centered-string (display-text item))
(lambda () (funcall (action-command item)))
:attribute (item-attribute item)))

;; Recent projects
(defclass dashboard-recent-projects (dashboard-item)
((project-count :initarg :project-count :accessor project-count :initform *dashboard-project-count*))
(:documentation "Displays a list of recent projects, limited to the last PROJECT-COUNT.")
(:default-initargs
:item-attribute 'document-text-attribute
:action (lambda ()
(let ((project (str:trim (line-string (current-point)))))
(when project
(project:project-find-file project))))))

(define-command dashboard-move-to-recent-projects () ()
(let ((point (buffer-point (current-buffer))))
(buffer-start point)
(search-forward-regexp point "Recent Projects")
(line-offset point 2)
(move-to-beginning-of-line)))

(defmethod draw-dashboard-item ((item dashboard-recent-projects) point)
(let* ((title (format nil "~A Recent Projects (r)" (icon-string "package")))
(title-line (create-centered-string title)))
(insert-string point title-line :attribute 'document-header1-attribute)
(insert-character point #\Newline)
(insert-character point #\Newline)
(let* ((projects (reverse (project:saved-projects)))
(display-projects (subseq projects 0 (min (project-count item) (length projects)))))
(when display-projects
(let* ((longest-project (reduce #'(lambda (a b) (if (> (length a) (length b)) a b)) display-projects))
(max-length (length longest-project))
(left-padding (floor (- (window-width (current-window)) max-length) 2)))
(loop for project in display-projects
do (insert-string point (format nil "~v@{~A~:*~}" left-padding " "))
(insert-string point (format nil "~A~%" project))))))))

;; Recent files
(defclass dashboard-recent-files (dashboard-item)
((file-count :initarg :file-count :accessor file-count :initform *dashboard-file-count*))
(:documentation "Displays a list of recent files, limited to the last FILE-COUNT.")
(:default-initargs
:item-attribute 'document-text-attribute
:action (lambda ()
(let ((file (str:trim (line-string (current-point)))))
(when file
(find-file file))))))

(define-command dashboard-move-to-recent-files () ()
(let ((point (buffer-point (current-buffer))))
(buffer-start point)
(search-forward-regexp point "Recent Files")
(line-offset point 2)
(move-to-beginning-of-line)))

(defmethod draw-dashboard-item ((item dashboard-recent-files) point)
(let* ((title (format nil "~A Recent Files (f)" (icon-string "file-text")))
(title-line (create-centered-string title))
(recent-files (reverse (history:history-data-list (file:file-history)))))
(insert-string point title-line :attribute 'document-header1-attribute)
(insert-character point #\Newline)
(insert-character point #\Newline)
(let ((display-files (subseq recent-files 0 (min (file-count item) (length recent-files)))))
(when display-files
(let* ((longest-file (reduce #'(lambda (a b) (if (> (length a) (length b)) a b)) display-files))
(max-length (length longest-file))
(left-padding (floor (- (window-width (current-window)) max-length) 2)))
(loop for file in display-files
do (insert-string point (str:fit left-padding " "))
(insert-string point (format nil "~A~%" file))))))))
93 changes: 93 additions & 0 deletions extensions/lem-dashboard/default-dashboard.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
(in-package :lem-dashboard)

(defvar *default-footer-messages* '("Happy Coding!"
"ほげほげ"
"In Lisp we trust, for bugs we adjust"
"Read, Evaluate, Print, Love"
"May your parentheses always be balanced"
"(setf productivity 'high)"
"<M-x> load-library <RET> tetris"
"Lem Editor Modules? Lisp EMacs? Lem's Not Emacs?"
"(cons 'fun 'programming)"))

(defvar *default-splash* '("
-----------------------
[ Welcome to Lem! ]
-----------------------

#*++======
%#*===============
%%*===================
%%%%%%%%%%%*+============
%%%%%%%%%%%%% +===========
%%%%%%%%%%%%%% -===========
%%%%%%%%%%%%%#*+=-:::==========
%%%%%%%%%%%%=:::::::::-==-:% *
%%%%%%%%%%*::::::::::::::::::==:
%%%%%%%%%*-+*##-:::::::::::::
%%%%%%%%%%%%%#-----
%%%%%%%%%%%%%=:::::
%%%%%%%%%%%*:::::::
%%%%%%%%%%=:::::::::
%%%%%%%%%*:::::::::::::::
%%%%%%%%%*::::::::::::
%%%%%%%%%#-::::::
%%%%%%%%%*-:::::
#+=-:::::::: "))

(define-command open-lem-docs () ()
(open-external-file "https://lem-project.github.io/usage/usage/"))

(define-command open-lem-github () ()
(open-external-file "https://github.com/lem-project/lem"))

(defun set-default-dashboard (&key
(project-count 5)
(file-count 5)
(splash *default-splash*)
(footer-messages *default-footer-messages*)
hide-links)
(let ((dashboard-items
(list (make-instance 'dashboard-splash
:item-attribute 'document-metadata-attribute
:splash-texts splash)
(make-instance 'dashboard-working-dir)
(make-instance 'dashboard-recent-projects
:project-count project-count
:bottom-margin 1)
(make-instance 'dashboard-recent-files
:file-count file-count
:bottom-margin 1)
(make-instance 'dashboard-command
:display-text " New Lisp Scratch Buffer (l)"
:action-command 'lem-lisp-mode:lisp-scratch
:item-attribute 'document-header2-attribute
:bottom-margin 2))))
(unless hide-links
(setf dashboard-items
(append dashboard-items
(list (make-instance 'dashboard-url
:display-text " Getting Started (s)"
:url "https://lem-project.github.io/usage/usage/"
:item-attribute 'document-header3-attribute)
(make-instance 'dashboard-url
:display-text " GitHub (g)"
:url "https://github.com/lem-project/lem"
:item-attribute 'document-header3-attribute
:bottom-margin 2))))
(define-key *dashboard-mode-keymap* "s" 'open-lem-docs)
(define-key *dashboard-mode-keymap* "g" 'open-lem-github))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed this code, is there any reason why it is not defined at the top level?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I had bindings previously worked a bit differently. The idea was to not apply the keybind until it's used in case the user has a dashboard that doesn't contain an item like the docs/github link. That being said, after changing the way the default dashboard is created, it doesn't work since it gets called at compile time, so further calls to set-default-dashboard won't remove the stale binds.

I went ahead and moved it to the top-level, users can just replace the stale bind in their config if they want to bind something else there.


(setf dashboard-items
(append dashboard-items
(list (make-instance 'dashboard-footer-message
:item-attribute 'document-blockquote-attribute
:messages footer-messages))))

(define-key *dashboard-mode-keymap* "r" 'dashboard-move-to-recent-projects)
(define-key *dashboard-mode-keymap* "f" 'dashboard-move-to-recent-files)
(define-key *dashboard-mode-keymap* "l" 'lem-lisp-mode/internal:lisp-scratch)

(set-dashboard dashboard-items)))

(set-default-dashboard)
6 changes: 6 additions & 0 deletions extensions/lem-dashboard/lem-dashboard.asd
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
(defsystem "lem-dashboard"
:depends-on (:lem)
:serial t
:components ((:file "lem-dashboard")
(:file "dashboard-items")
(:file "default-dashboard")))
131 changes: 131 additions & 0 deletions extensions/lem-dashboard/lem-dashboard.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
(defpackage :lem-dashboard
(:use :cl :lem)
(:export :open-dashboard
:*dashboard-mode-keymap*
:*dashboard-enable*
:dashboard-move-to-recent-projects
:dashboard-move-to-recent-files
:set-dashboard
:set-default-dashboard
:create-centered-string)
(:local-nicknames (:button :lem/button)
(:history :lem/common/history)
(:project :lem-core/commands/project)
(:file :lem-core/commands/file)))

(in-package :lem-dashboard)

(defvar *dashboard-buffer-name* "*dashboard*")
(defvar *dashboard-enable* t)
(defvar *dashboard-mode-keymap* (make-keymap :name '*dashboard-mode-keymap* :parent *global-keymap*))
(defvar *dashboard-layout* nil
"List of dashboard-item instances; will be drawn in order.")

(defun create-centered-string (str)
"Creates a 'centered' string by padding with space to fill the screen width halfway."
(let* ((padding (max 0 (floor (- (window-width (current-window)) (length str)) 2)))
(spaces (make-string padding :initial-element #\Space)))
(concatenate 'string spaces str)))

(defclass dashboard-item ()
((item-attribute
:initarg :item-attribute
:accessor item-attribute
:initform 'document-text-attribute
:documentation "Attribute to use when drawing this item.")
(top-margin
:initarg :top-margin
:accessor top-margin
:initform 0
:documentation "The amount of vertical space (lines) to apply before the item.")
(bottom-margin
:initarg :bottom-margin
:accessor bottom-margin
:initform 1
:documentation "The amount of vertical space (lines) to apply after the item.")
(action
:initarg :action
:accessor action
:initform nil
:documentation "Function to execute when <return> is pressed over this item."))
(:documentation "Base class for all dashboard items."))

(defgeneric draw-dashboard-item (item point)
(:documentation "Called to draw the dashboard item.")
(:method :before ((item dashboard-item) point)
(dotimes (i (top-margin item))
(insert-character point #\Newline)))
(:method :after ((item dashboard-item) point)
(dotimes (i (bottom-margin item))
(insert-character point #\Newline))))

(define-command dashboard-open-selected-item () ()
"Execute action on selected dashboard item."
(let* ((point (current-point))
(item (text-property-at point :dashboard-item)))
(when (and item (action item))
(funcall (action item)))))

(defmethod draw-dashboard-item :around ((item dashboard-item) point)
"Inserts a :dashboard-item text property in between the starting and ending POINT, useful for tracking."
(let ((start (copy-point point :temporary)))
(call-next-method)
(let ((end (copy-point point :temporary)))
(put-text-property start end :dashboard-item item)
(delete-point start)
(delete-point end))))

(define-major-mode dashboard-mode ()
(:name "Dashboard"
:keymap *dashboard-mode-keymap*))

(defun create-dashboard-buffer ()
"Creates the dashboard buffer."
(or (get-buffer *dashboard-buffer-name*)
(make-buffer *dashboard-buffer-name*
:enable-undo-p nil
:read-only-p t)))

(defun redraw-dashboard ()
"Redraws the dashboard, clearing and redrawing all content while attempting to maintain point position."
(let* ((buffer (create-dashboard-buffer))
(old-line (line-number-at-point (buffer-point buffer)))
(old-column (point-column (buffer-point buffer))))
(with-buffer-read-only buffer nil
(erase-buffer buffer)
(let ((point (buffer-point buffer)))
(dolist (item *dashboard-layout*)
(draw-dashboard-item item point)))
(change-buffer-mode buffer 'dashboard-mode)
(move-to-line (buffer-point buffer) old-line)
(move-to-column (buffer-point buffer) old-column))))

(define-command open-dashboard () ()
"Opens the dashboard if it doesn't exist, or switches to it if it does."
(when *dashboard-enable*
(if (get-buffer *dashboard-buffer-name*)
(switch-to-buffer (get-buffer *dashboard-buffer-name*))
(progn
(redraw-dashboard)
(switch-to-buffer (get-buffer *dashboard-buffer-name*))))))

(defun set-dashboard (dashboard-items)
"Sets the new dashboard layout to DASHBOARD-ITEMS list and applies new keymap."
(when dashboard-items
(setf *dashboard-layout* dashboard-items)
(when (get-buffer *dashboard-buffer-name*)
(redraw-dashboard))))

(defun handle-resize (window)
"Handle resizing; in this case, redraw the dashboard to keep it centered."
(when (string= (buffer-name (window-buffer window)) *dashboard-buffer-name*)
(redraw-dashboard)))

(define-key *dashboard-mode-keymap* "n" 'next-line)
(define-key *dashboard-mode-keymap* "p" 'previous-line)
(define-key *dashboard-mode-keymap* "j" 'next-line)
(define-key *dashboard-mode-keymap* "k" 'previous-line)
(define-key *dashboard-mode-keymap* "Return" 'dashboard-open-selected-item)

(setf lem:*splash-function* #'open-dashboard)
(add-hook *window-size-change-functions* 'handle-resize)
1 change: 1 addition & 0 deletions extensions/lisp-mode/internal-package.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
:with-remote-eval
:lisp-eval-from-string
:lisp-eval
:lisp-scratch
:lisp-eval-async
:eval-with-transcript
:re-eval-defvar
Expand Down
Loading
Loading