Add `lem-dashboard`
This PR adds a customizable dashboard to lem, inspired by how distributions like doom/spacemacs/lazyvim ship out of the box. Besides a more friendly greeting than a *tmp* buffer, it ships with useful shortcuts like starting a lisp scratch or seeing recent files/projects at a glance.
The dashboard is simply a list of dashboard-items, of which a few are included by default, like a splash/footer message,
commands/urls, working directory, and recent projects/files.
When making a dashboard-item, the only method you need to implement is draw-dashboard-item, which is called whenever the dashboard is (re)drawn.
Let's say you want to add a dashboard-item that prints a random fact/quote from an http endpoint, you can make it like this (multithreading is overkill here, just for fun):
(defclass dashboard-random-fact (dashboard-item)
((fact :initform nil :accessor fact)
(fetching :initform nil :accessor fetching))
(:documentation "Creates a dashboard item that displays a random fact"))
(defmethod draw-dashboard-item ((item dashboard-random-fact) point)
;; Display initial or current fact
(insert-string point
(create-centered-string (or (fact item) "Loading fact..."))
:attribute (item-attribute item))
(unless (or (fact item) (fetching item))
(setf (fetching item) t)
;; Start a background thread to fetch the fact
(bt2:make-thread
(lambda ()
(handler-case
(let* ((response (dexador:get "https://uselessfacts.jsph.pl/api/v2/facts/random"))
(json-data (json:decode-json-from-string response))
(fact-text (cdr (assoc :text json-data))))
(send-event (lambda ()
(setf (fact item) fact-text)
(redraw-dashboard))))
(error (e)
(send-event (lambda () (message (format nil "Error fetching fact: ~A" e))))))
:name "random-fact-fetcher"))))
To make a custom dashboard layout, all you have to do is call set-dashboard and pass a list of items:
(in-package :lem-dashboard)
(set-dashboard (list (make-instance 'dashboard-splash
:item-attribute 'document-metadata-attribute
:splash-texts '("Welcome!" "Second splash message!")
:top-margin 4
:bottom-margin 2)
(make-instance 'dashboard-working-dir)
(make-instance 'dashboard-recent-files
:file-count 5
:bottom-margin 1)
(make-instance 'dashboard-random-fact
:bottom-margin 2
:item-attribute 'document-header3-attribute)
(make-instance 'dashboard-command
:display-text " New Lisp Scratch Buffer (l)"
:command 'lem-lisp-mode/internal:lisp-scratch
:item-attribute 'document-header2-attribute
:bottom-margin 2)))
Users might want to keep most of the default dashboard's functionality, but customize it a bit (e.g. change the splash, number of projects, etc.). There's a set-default-dashboard available to make it easy so they don't have to re-do the whole layout:
(lem-dashboard:set-default-dashboard :project-count 10 :file-count 8 :hide-links t :splash '("hello" "hello2"))
Notes:
- Set
*dashboard-enable*to nil to disable - Only vertical stacking is currently supported. If you want to stack multiple elements horizontally, you'll have to make a dashboard item that does so. At some point I'd like to revisit this, but the way of centering elements here is just adding space padding. I think maybe lem itself should have line attributes, like left-adjust/right-adjust/center. Then we also wouldn't need to redraw on resize.
- I threw in some cheesy footer messages, feel free to add/remove whatever you want from there :laughing:
- @vindarel let me know if you want me to do the docs for this one
Wow, it's super cool :rocket:
LGTM (only a minor request with the defvar)
user feedback:
- watch out for the logo vertical size. On my screen, not a small one, I can see the logo, the cwd, "recent projects", then I have to scroll to see the rest. Not very ergonomic.
- have the cursor on a meaningful by default?
- have the ascii logo for ncurses, but the beautiful image in the GUI?
- more shortcuts like M-n and M-p to move between items?
last but not least:
- it may be a big win to display a short help, like C-x C-f to open files, M-x (alt-x) help to read some help or M-x documentation-describe-bindings.
- the link to the online manual is great but not self-contained.
@vindarel let me know if you want me to do the docs for this one
accepted, thanks. I can see a short description with screenshot in the usage page, then link to a more detailed page of its own.
Also, what about a dashboard-help? (where we see the lack of more self-documentation features)
That's great!
It just seems a little buggy. For example, I get the following error at startup
invalid number of arguments: 0
Backtrace for: #<SB-THREAD:THREAD tid=3075 "editor" RUNNING {7005B23643}>
0: ((LAMBDA (LEM-DASHBOARD::A LEM-DASHBOARD::B) :IN LEM-DASHBOARD::DRAW-DASHBOARD-ITEM)) [external]
1: (REDUCE #<FUNCTION (LAMBDA (LEM-DASHBOARD::A LEM-DASHBOARD::B) :IN LEM-DASHBOARD::DRAW-DASHBOARD-ITEM) {700CF7260B}> NIL)
2: ((:METHOD LEM-DASHBOARD::DRAW-DASHBOARD-ITEM (LEM-DASHBOARD::DASHBOARD-RECENT-PROJECTS T)) #<LEM-DASHBOARD::DASHBOARD-RECENT-PROJECTS {700CFD9353}> #<LEM-CORE:CURSOR (30, 0) "" {700CC28693}>) [fast-method]
3: ((SB-PCL::EMF LEM-DASHBOARD::DRAW-DASHBOARD-ITEM) #<unused argument> #<unused argument> #<LEM-DASHBOARD::DASHBOARD-RECENT-PROJECTS {700CFD9353}> #<LEM-CORE:CURSOR (30, 0) "" {700CC28693}>)
4: ((:METHOD LEM-DASHBOARD::DRAW-DASHBOARD-ITEM :AROUND (LEM-DASHBOARD::DASHBOARD-ITEM T)) #<LEM-DASHBOARD::DASHBOARD-RECENT-PROJECTS {700CFD9353}> #<LEM-CORE:CURSOR (30, 0) "" {700CC28693}>) [fast-method]
5: (LEM-DASHBOARD::REDRAW-DASHBOARD)
6: (LEM-DASHBOARD:OPEN-DASHBOARD)
7: (SB-INT:SIMPLE-EVAL-IN-LEXENV (FUNCALL #<FUNCTION LEM-DASHBOARD:OPEN-DASHBOARD>) #<NULL-LEXENV>)
8: (EVAL (FUNCALL #<FUNCTION LEM-DASHBOARD:OPEN-DASHBOARD>))
9: (LEM-CORE::APPLY-ARGS #S(LEM-CORE::COMMAND-LINE-ARGUMENTS :ARGS (#1="~/.lem/log" (FUNCALL #<FUNCTION LEM-DASHBOARD:OPEN-DASHBOARD>)) :DEBUG NIL :LOG-FILENAME #1# :NO-INIT-FILE NIL :INTERFACE NIL))
10: (LEM-CORE::TOPLEVEL-COMMAND-LOOP #<FUNCTION (LAMBDA NIL :IN LEM-CORE::RUN-EDITOR-THREAD) {700B0E58FB}>)
11: ((LAMBDA NIL :IN LEM-CORE::RUN-EDITOR-THREAD))
12: ((LAMBDA NIL :IN LEM-CORE::RUN-EDITOR-THREAD))
13: ((FLET BORDEAUX-THREADS-2::RUN-FUNCTION :IN BORDEAUX-THREADS-2::ESTABLISH-DYNAMIC-ENV))
14: ((LABELS BORDEAUX-THREADS-2::%ESTABLISH-DYNAMIC-ENV-WRAPPER :IN BORDEAUX-THREADS-2::ESTABLISH-DYNAMIC-ENV))
15: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN))
16: ((FLET "WITHOUT-INTERRUPTS-BODY-" :IN SB-THREAD::RUN))
17: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN))
18: ((FLET "WITHOUT-INTERRUPTS-BODY-" :IN SB-THREAD::RUN))
19: (SB-THREAD::RUN)
I was able to open the dashboard with M-x open-dashboard. However, I randomly get the above error.
Your screenshot seems to be different from mine.
Do you have a special setting?
For example, I get the following error at startup,,
Bah my bad, I lost logic I had there in a commit I reverted. Thanks for pointing that out.
Your screenshot seems to be different from mine. Do you have a special setting?
~~That's due to the above error, I'll fix it now.~~ Should work now
@cxxxr I see what vindarel was talking about with the default font making the ascii art look too big. I can try making it smaller.
This is the smallest I think I can make it without it starting to look strange:
Should I commit it?
I think it's good
User feedback:
- (minor) ncurses version, I see "NIL recent projects (r)": "NIL" instead of an icon or a character
- bug: on any line of the recent projects, pressing Enter makes me open… files from the cwd, not the project my cursor is on.
- (minor) ncurses version, I see "NIL recent projects (r)": "NIL" instead of an icon or a character
Hmm I don't see that, I'm guessing I just happen to have the font it needs installed? Is there a way to detect if the font is available and then just not show it if it's not? I guess worst-case scenario I can omit icons if on ncurses.
- bug: on any line of the recent projects, pressing Enter makes me open… files from the cwd, not the project my cursor is on.
Whoops, that's my bad. I tested in same folder so I missed that :laughing: The arg in project-find-file is unused so I had to switch first. Fixed now, thanks.
alright the PR looks great to me and cxxxr approved the changes. @jfaz1 you're done, I/we merge?
Looks good, so I'll merge it.