Skip to content

Conversation

jfaz1
Copy link
Contributor

@jfaz1 jfaz1 commented Aug 24, 2024

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.

image

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)))

image

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 😆
  • @vindarel let me know if you want me to do the docs for this one

@vindarel
Copy link
Collaborator

Wow, it's super cool 🚀

LGTM (only a minor request with the defvar)

@vindarel
Copy link
Collaborator

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
Copy link
Collaborator

@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)

@cxxxr
Copy link
Member

cxxxr commented Aug 25, 2024

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.

@cxxxr
Copy link
Member

cxxxr commented Aug 25, 2024

Your screenshot seems to be different from mine.
Do you have a special setting?
スクリーンショット 2024-08-25 16 35 15

Copy link
Member

@cxxxr cxxxr left a comment

Choose a reason for hiding this comment

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

I commented on a few things that bothered me.
Nevertheless, I think this feature and implementation is excellent.
Thank you.

Comment on lines 97 to 98
(move-to-line (buffer-point buffer) old-line)
(move-to-column (buffer-point buffer) old-column)))
Copy link
Member

Choose a reason for hiding this comment

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

Was there a problem with save-excursion?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It didn't work for me... I might be using it wrong. Here's the redraw function, works fine (keeps cursor position when resizing window)

(defun redraw-dashboard ()
  (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))))

Here's after changing it:

(defun redraw-dashboard ()
  (let* ((buffer (create-dashboard-buffer)))
    (save-excursion
      (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)))))

After making that change, if I resize the window it'll reset it to the top. I tried using it in a few other spots and couldn't get it to work.

@jfaz1
Copy link
Contributor Author

jfaz1 commented Aug 25, 2024

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

…rd command prefix, print blank project/file list, minor command/url fix)
Comment on lines 78 to 79
(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.

@jfaz1
Copy link
Contributor Author

jfaz1 commented Aug 25, 2024

@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:
image

Should I commit it?

@cxxxr
Copy link
Member

cxxxr commented Aug 25, 2024

I think it's good

@vindarel
Copy link
Collaborator

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.

@jfaz1
Copy link
Contributor Author

jfaz1 commented Aug 28, 2024

  • (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 😆 The arg in project-find-file is unused so I had to switch first. Fixed now, thanks.

@vindarel
Copy link
Collaborator

vindarel commented Sep 2, 2024

alright the PR looks great to me and cxxxr approved the changes. @jfaz1 you're done, I/we merge?

@cxxxr
Copy link
Member

cxxxr commented Sep 3, 2024

Looks good, so I'll merge it.

@cxxxr cxxxr merged commit cb2e1fe into lem-project:main Sep 3, 2024
2 checks passed
@jfaz1 jfaz1 deleted the dashboard branch September 3, 2024 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants