Skip to content

Commit 919facc

Browse files
committed
add `pushbullet-ui'
1 parent 5ad3308 commit 919facc

File tree

2 files changed

+551
-233
lines changed

2 files changed

+551
-233
lines changed

pushbullet-ui.el

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
;;; pushbullet-ui.el --- Pushbullet UI -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2025 Savio Sena <savio.sena@gmail.com>
4+
5+
;; Author: Savio Sena <savio.sena@gmail.com>
6+
;; Version: 1.0.0
7+
;; Package-Requires: ((emacs "29.1") (all-the-icons "5.0.0"))
8+
;; Keywords: pushbullet, client, tool, internet
9+
;; URL: https://github.com/sav/emacs-pushbullet
10+
11+
;; This file is not part of GNU Emacs.
12+
13+
;; This program is free software: you can redistribute it and/or modify
14+
;; it under the terms of the GNU General Public License as published by
15+
;; the Free Software Foundation, either version 3 of the License, or
16+
;; (at your option) any later version.
17+
18+
;; This program is distributed in the hope that it will be useful,
19+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21+
;; GNU General Public License for more details.
22+
23+
;; You should have received a copy of the GNU General Public License
24+
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
25+
26+
;;; Commentary:
27+
;;; This package provides the user interface components for the Emacs Pushbullet
28+
;;; client. It includes functions for rendering pushes, handling user
29+
;;; interactions, and managing the display of Pushbullet data within Emacs.
30+
;;;
31+
32+
;;; Code:
33+
34+
(require 'cl-lib)
35+
(require 'button)
36+
(require 'widget)
37+
(require 'wid-edit)
38+
(require 'all-the-icons)
39+
40+
(defgroup pushbullet-ui nil
41+
"User interface for the Pushbullet client."
42+
:group 'extensions)
43+
44+
(defcustom pushbullet-ui-columns 70
45+
"Maximum number of columns for wrapping lines in the Pushbullet UI buffer."
46+
:type 'integer
47+
:group 'pushbullet-ui)
48+
49+
(defcustom pushbullet-ui-left-alignment 8
50+
"The size of the left alignment padding in the Pushbullet UI."
51+
:type 'integer
52+
:group 'pushbullet-ui)
53+
54+
(defcustom pushbullet-ui-textfield-width
55+
(truncate
56+
(* (- pushbullet-ui-columns pushbullet-ui-left-alignment) 0.90))
57+
"The calculated width for editable text fields within the Pushbullet UI."
58+
:type 'integer
59+
:group 'pushbullet-ui)
60+
61+
(defcustom pushbullet-ui-debug nil
62+
"Enable verbose logging for Pushbullet UI operations.
63+
When non-nil, additional debug messages will be printed to the *Messages* buffer."
64+
:type 'boolean
65+
:group 'pushbullet-ui)
66+
67+
(defcustom pushbullet-ui-show-send-form t
68+
"Whether to display the send form in the Pushbullet UI."
69+
:type 'boolean
70+
:group 'pushbullet-ui)
71+
72+
(defvar pushbullet-ui--buffer nil
73+
"The buffer currently used for rendering the Pushbullet UI. This is a
74+
buffer-local variable.")
75+
76+
(defvar pushbullet-ui--title nil
77+
"The title string displayed at the top of the Pushbullet UI buffer.
78+
This is a buffer-local variable.")
79+
80+
(defvar pushbullet-ui--pushes nil
81+
"A buffer-local list of Pushbullet pushes currently displayed in the
82+
UI, where each push is an alist.")
83+
84+
(defvar pushbullet-ui--mode-map nil
85+
"The keymap active within the Pushbullet UI buffer.")
86+
87+
(defvar pushbullet-ui--api nil
88+
"A buffer-local alist of callback functions for Pushbullet API
89+
interactions. See `pushbullet-api'.")
90+
91+
(defmacro pushbullet-ui--log (fmt &rest args)
92+
"Logs a debug message with FMT and ARGS if `pushbullet-ui-debug' is
93+
enabled.
94+
The message is prefixed with '[pushbullet-ui]' for easy identification
95+
in the `*Messages*' buffer."
96+
`(when pushbullet-ui-debug
97+
(message (concat "[pushbullet-ui] " ,fmt) ,@args)))
98+
99+
(defun pushbullet-ui--align-right (max str)
100+
"Inserts spaces to right-align STR within a field of MAX width in the
101+
current buffer."
102+
(let ((len (length str)))
103+
(when (>= max len)
104+
(widget-insert (make-string (- max (length str)) ?\s)))))
105+
106+
(defun pushbullet-ui--insert-aligned (str)
107+
"Inserts a newline and then the string STR, right-aligned by
108+
`pushbullet-ui-left-alignment'."
109+
(widget-insert "\n")
110+
(pushbullet-ui--align-right pushbullet-ui-left-alignment str)
111+
(widget-insert str))
112+
113+
(defun pushbullet-ui--list-filter (pushes)
114+
"Filters a list of PUSHES, returning only those that are active and
115+
have at least a title, URL, or body."
116+
(let ((is-active (alist-get 'active pushbullet-ui--api)))
117+
(seq-filter (lambda (push) (funcall is-active push)) pushes)))
118+
119+
(defun pushbullet-ui--list-remove (list push)
120+
"Removes PUSH from LIST where elements in LIST match PUSH
121+
based on the `'iden' key-value pairs."
122+
(let ((iden (alist-get 'iden push)))
123+
(seq-remove (lambda (item) (equal (alist-get 'iden item) iden)) list)))
124+
125+
(defun pushbullet-ui--send (title body url)
126+
(let ((send (alist-get 'send pushbullet-ui--api)))
127+
(funcall send title body url)
128+
(pushbullet-ui--log "Pushed: (%S, %S, %S)" title body url)
129+
(pushbullet-ui--load-more 1)))
130+
131+
(defun pushbullet-ui--load-more (&optional limit)
132+
"Fetches additional pushes from the Pushbullet server using the `fetch'
133+
callback from `pushbullet-ui--api', and then re-renders the UI."
134+
(let ((fetch (alist-get 'fetch pushbullet-ui--api)))
135+
(funcall fetch
136+
#'(lambda (pushes)
137+
(setq pushbullet-ui--pushes
138+
(pushbullet-ui--list-filter
139+
(append pushbullet-ui--pushes pushes)))
140+
(pushbullet-ui--log "Loaded more %S pushes. Total: %S"
141+
(length pushes) (length pushbullet-ui--pushes))
142+
(pushbullet-ui--render)
143+
;; Move cursor back to its original position when called from
144+
;; "Load More" button.
145+
(when (not limit)
146+
(goto-char (point-max))
147+
(search-backward "Load More")))
148+
limit))
149+
nil)
150+
151+
(defun pushbullet-ui--export-all ()
152+
"Exports all currently loaded pushes to an Org-mode buffer using the
153+
`export' callback from `pushbullet-ui--api'."
154+
(let* ((export (alist-get 'export pushbullet-ui--api)))
155+
(funcall export pushbullet-ui--pushes)))
156+
157+
(defun pushbullet-ui--delete-all (&rest args)
158+
"Deletes all pushes currently displayed in the UI from the Pushbullet
159+
server using the `delete' callback from `pushbullet-ui--api', then
160+
re-renders the UI."
161+
(let* ((del (alist-get 'del pushbullet-ui--api)))
162+
(dolist (push pushbullet-ui--pushes)
163+
(funcall del push)))
164+
(setq pushbullet-ui--pushes nil)
165+
(pushbullet-ui--render))
166+
167+
(defun pushbullet-ui--delete-row (push)
168+
"Deletes a single PUSH from the `pushbullet-ui--pushes' list, invokes
169+
the `delete' callback from `pushbullet-ui--api', and then re-renders
170+
the UI."
171+
(setq pushbullet-ui--pushes
172+
(pushbullet-ui--list-remove pushbullet-ui--pushes push))
173+
(let* ((del (alist-get 'del pushbullet-ui--api)))
174+
(funcall del push))
175+
(pushbullet-ui--log "Row deleted")
176+
(pushbullet-ui--render))
177+
178+
(defun pushbullet-ui--render-top (title)
179+
"Renders the top section of the Pushbullet UI, displaying the
180+
provided TITLE as a banner."
181+
(let ((len (length title)))
182+
(widget-insert
183+
(propertize
184+
(concat "══ " title " " (make-string (- pushbullet-ui-columns 4 len) ?═) "\n")
185+
'face 'bold))))
186+
187+
(defun pushbullet-ui--render-bottom ()
188+
"Renders the bottom section of the Pushbullet UI, including action
189+
buttons such as 'Load More', 'Export', 'Delete All', and 'Close'."
190+
(widget-insert "\n" (make-string pushbullet-ui-columns ?═) "\n")
191+
(pushbullet-ui--align-right
192+
pushbullet-ui-columns
193+
" [Load More] [Export] [Delete All] [Close]")
194+
(widget-create 'push-button
195+
:notify (lambda (&rest _) (pushbullet-ui--load-more))
196+
"Load More")
197+
(widget-insert " ")
198+
(widget-create 'push-button
199+
:notify (lambda (&rest _) (pushbullet-ui--export-all))
200+
"Export")
201+
(widget-insert " ")
202+
(widget-create 'push-button
203+
:notify (lambda (&rest _) (pushbullet-ui--delete-all))
204+
"Delete All")
205+
(widget-insert " ")
206+
(widget-create 'push-button
207+
:notify (lambda (&rest _) (kill-buffer))
208+
"Close")
209+
(widget-insert "\n"))
210+
211+
(defun pushbullet-ui--render-push (push)
212+
"Renders a single PUSH (an alist) as a set of editable widgets in the UI.
213+
This includes displaying its creation datetime, editable fields for
214+
title, URL, and body, and 'Delete' buttons."
215+
(let* ((created (seconds-to-time (alist-get 'created push)))
216+
(datetime (propertize
217+
(format "%s %s %s %s"
218+
(all-the-icons-faicon "calendar")
219+
(format-time-string "%Y-%b-%d" created)
220+
(all-the-icons-faicon "clock-o")
221+
(format-time-string "%H:%M" created))
222+
'face 'shadow))
223+
(_ (widget-insert (format "\n ── %s " datetime)))
224+
(_ (widget-insert (make-string
225+
(- pushbullet-ui-columns
226+
(+ 7 (length datetime))) ?─) "\n"))
227+
(_ (pushbullet-ui--insert-aligned "Title: "))
228+
(title-w (widget-create 'editable-field
229+
:size pushbullet-ui-textfield-width
230+
:format "%v"
231+
:value (or (alist-get 'title push) "")))
232+
(_ (pushbullet-ui--insert-aligned "URL: "))
233+
(url-w (widget-create 'editable-field
234+
:size pushbullet-ui-textfield-width
235+
:format "%v"
236+
:value (or (alist-get 'url push) "")))
237+
(_ (pushbullet-ui--insert-aligned "Body: "))
238+
(body-w (widget-create 'text
239+
:size pushbullet-ui-textfield-width
240+
:format "%v"
241+
:value (or (alist-get 'body push) ""))))
242+
(widget-insert "\n\n")
243+
(pushbullet-ui--align-right pushbullet-ui-columns "[Delete]")
244+
(widget-create 'push-button
245+
:notify
246+
(lambda (&rest _)
247+
(pushbullet-ui--delete-row push))
248+
"Delete")
249+
(widget-insert "\n")))
250+
251+
(defun pushbullet-ui--render-pushes ()
252+
"Render the list of pushes in the UI (`pushbullet-ai--pushes')."
253+
(dolist (push pushbullet-ui--pushes)
254+
(let* ((is-active (alist-get 'active pushbullet-ui--api)))
255+
(when (funcall is-active push)
256+
(pushbullet-ui--render-push push)))))
257+
258+
(defun pushbullet-ui--render-form ()
259+
"Renders the 'New Push' form, allowing users to input a title, URL,
260+
and body for a new Pushbullet push.
261+
Includes a 'Push' button to submit the form via the `send' callback from
262+
`pushbullet-ui--api'."
263+
(widget-insert
264+
(propertize
265+
(concat "\n\n\n══ New Push "
266+
(make-string (- pushbullet-ui-columns 12) ?═) " \n")
267+
'face 'bold))
268+
(pushbullet-ui--insert-aligned "Title: ")
269+
(let* ((new-title (widget-create
270+
'editable-field
271+
:size pushbullet-ui-textfield-width
272+
:value ""))
273+
(_ (pushbullet-ui--insert-aligned "URL: "))
274+
(new-url (widget-create
275+
'editable-field
276+
:size pushbullet-ui-textfield-width
277+
:value ""))
278+
(_ (pushbullet-ui--insert-aligned "Body: "))
279+
(new-body (widget-create
280+
'editable-field
281+
:size pushbullet-ui-textfield-width
282+
:value "")))
283+
284+
(widget-insert "\n\n")
285+
(widget-insert (make-string pushbullet-ui-columns ?═) "\n")
286+
(pushbullet-ui--align-right pushbullet-ui-columns " Push ")
287+
(widget-create 'push-button
288+
:notify (lambda (&rest _)
289+
(pushbullet-ui--send
290+
(widget-value new-title)
291+
(widget-value new-body)
292+
(widget-value new-url)))
293+
"Push")
294+
(widget-insert "\n")))
295+
296+
(defun pushbullet-ui--render ()
297+
"Renders the complete Pushbullet UI in the buffer specified by
298+
`pushbullet-ui--buffer'.
299+
This involves rendering the top banner, iterating through
300+
`pushbullet-ui--pushes' to display each push, rendering the bottom
301+
action buttons, and optionally rendering the 'New Push' form if
302+
`pushbullet-ui-show-send-form' is non-nil."
303+
(with-current-buffer pushbullet-ui--buffer
304+
(let* ((inhibit-read-only t)
305+
(inhibit-modification-hooks t))
306+
(remove-overlays)
307+
(erase-buffer)
308+
(goto-address-mode 1)
309+
(pushbullet-ui--render-top pushbullet-ui--title)
310+
;; cleanup inactive pushes
311+
(setq pushbullet-ui--pushes
312+
(pushbullet-ui--list-filter pushbullet-ui--pushes))
313+
(pushbullet-ui--render-pushes)
314+
(pushbullet-ui--render-bottom)
315+
(when pushbullet-ui-show-send-form
316+
(pushbullet-ui--render-form))
317+
(widget-setup)
318+
(use-local-map
319+
(make-composed-keymap
320+
pushbullet-ui--mode-map widget-keymap)))))
321+
322+
(defun pushbullet-ui (title buffer api mode-map)
323+
"Initializes and displays the Pushbullet UI in the specified BUFFER.
324+
325+
This function sets up buffer-local variables, including the UI TITLE,
326+
the API CALLBACKS alist for interaction, and the MODE-MAP for
327+
keybindings. It then triggers the initial fetch of pushes and renders
328+
the complete UI.
329+
330+
BUFFER is the buffer to create or use.
331+
TITLE is a string to be displayed as the main title of the UI.
332+
API is an alist of callback functions for Pushbullet API operations.
333+
MODE-MAP is the keymap to use in the UI buffer."
334+
(setq pushbullet-ui--title title
335+
pushbullet-ui--buffer buffer
336+
pushbullet-ui--api api
337+
pushbullet-ui--pushes nil
338+
pushbullet-ui--mode-map mode-map)
339+
(pushbullet-ui--load-more)
340+
nil)
341+
342+
(provide 'pushbullet-ui)
343+
344+
;;; pushbullet-ui.el ends here

0 commit comments

Comments
 (0)