;;; emacs-web.el --- Browse the running Emacs's buffers over the web -*- lexical-binding: t; -*-
;; Author: Pavel Popov
;; Version: 0.1
;; Package-Requires: ((emacs "29.1") (web-server "0.1.2") (htmlize "1.59"))
;; Keywords: hypermedia, tools
;;; Commentary:
;; A read-only buffer browser for the Emacs it runs in. The index shows
;; the live buffers as folders, one per major mode; each buffer opens as
;; a page coloured the way Emacs shows it.
;;
;; The server listens on 127.0.0.1 only and authenticates nobody: it is
;; meant to sit behind a reverse proxy that does. Every link it emits is
;; relative, so it works under any path prefix.
;;
;; Start it with `emacs-web-start', stop it with `emacs-web-stop'.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(require 'url-util)
(require 'json)
(require 'web-server)
(require 'htmlize)
(require 'mailcap)
(declare-function ghostel-paste-string "ghostel" (string))
(declare-function ghostel-status--shown "ghostel-status" ())
(declare-function ghostel-send-key "ghostel" (key-name &optional mods))
(declare-function ghostel--event-key-spec "ghostel" (event &optional meta))
(declare-function ghostel--redraw-now "ghostel" (buffer &optional force))
(declare-function ghostel--set-size-with-cell-dims "ghostel" (term rows cols))
(declare-function ghostel--adjust-size "ghostel" (window &optional force))
(declare-function ghostel--mouse-event "ghostel-module")
(defvar ghostel--term)
(defvar ghostel--term-rows)
(defvar ghostel--term-cols)
(defgroup emacs-web nil
"Browse the running Emacs's buffers over the web."
:group 'hypermedia)
(defcustom emacs-web-port 4678
"Port the server listens on."
:type 'integer)
(defcustom emacs-web-favorite-modes '(ghostel-mode)
"Major modes the home page lists beside All Buffers, not among the modes."
:type '(repeat symbol))
(defcustom emacs-web-shared-port nil
"Port of a second server showing only the shared buffers, or nil for none.
It lists and opens the buffers shared with `emacs-web-toggle-share' and
takes text, dictation and keys for them, and nothing else: no
buffer's menu, no Emacs prompt, no forms to evaluate."
:type '(choice (const :tag "None" nil) integer))
(defcustom emacs-web-max-chars 500000
"Most characters of a buffer one page shows."
:type 'integer)
(defcustom emacs-web-id-modes '(ghostel-mode)
"Major modes whose buffers are linked by an id as well as by name.
For buffers that get renamed while they live, so a link to one keeps
leading to it."
:type '(repeat symbol))
(defvar-local emacs-web--buffer-id nil
"The id this buffer is linked by, once it has one.")
(put 'emacs-web--buffer-id 'permanent-local t)
(defconst emacs-web--manifest
"{
\"name\": \"Emacs\",
\"short_name\": \"Emacs\",
\"start_url\": \"./\",
\"scope\": \"./\",
\"display\": \"standalone\",
\"background_color\": \"#000000\",
\"theme_color\": \"#000000\",
\"icons\": [
{\"src\": \"apple-touch-icon.png\", \"sizes\": \"180x180\", \"type\": \"image/png\"},
{\"src\": \"icon-512.png\", \"sizes\": \"512x512\", \"type\": \"image/png\", \"purpose\": \"any\"}
]
}"
"The web app manifest, which lets a phone install the pages as an app.")
(defconst emacs-web--icon-180
"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
AABJRU5ErkJggg=="
"The app's 180px icon, a PNG in base64.")
(defconst emacs-web--icon-512
"iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAdRklEQVR4nO3deXhcVd3A8XPv7JlJ
JlvTJt3bUGhKa1lq01IoyFKRRZYCCsKrIKhYtT6uvKKCiKLg0ldBHxBQdrEgKFvZrGwthAKFthS6
ULqkbbZmkpnMPvd9xlahNW2TdDL3zPy+n4enf7iQ+0xn5ptz7rnnGIceOkUBAORxWpZl9zUAAGxg
2vFDAQD2IwAAIBQBAAChCAAACEUAAEAoAgAAQhEAABCKAACAUAQAAIQiAAAgFAEAAKEIAAAIRQAA
QCgCAABCEQAAEIoAAIBQBAAAhCIAACAUR0ICgFCMAABAKAIAAEIRAAAQigAAgFAEAACEIgAAIBQB
AAChCAAACEUAAEAoAgAAQhEAABCKAACAUAQAAIQiAAAgFAEAAKEIAAAIRQAAQCgCAABCEQAAEIoz
gQFAKEYAACAUAQAAoQgAAAhFAABAKAIAAEIRAAAQigAAgFAEAACEIgAAIBQBAAChCAAACEUAAEAo
AgAAQhEAABCKAACAUAQAAIQiAAAgFAEAAKEIAAAIxZnAACAUIwAAEIoAAIBQBAAAhCIAACAUAQAA
oQgAAAhFAABAKAIAAEIRAAAQigAAgFAEAACEIgAAIBQBAAChCAAACEUAAEAoAgAAQhEAABCKAACA
UBwJCQBCMQIAAKEIAAAIRQAAQCgCAABCEQAAEIoAAIBQBAAAhCIAACAUAQAAoQgAAAhFAABAKAIA
AEIRAAAQigAAgFAEAACEIgAAIBQBAAChCAAACEUAAEAozgQGAKEYAQCAUAQAAIQiAAAgFAEAAKEI
AAAIRQAAQCgCAABCEQAAEIoAAIBQBAAAhCIAACAUAQAAoQgAAAhFAABAKAIAAEIRAAAQigAAgFAE
AACEIgAAIBRnAgOAUIwAAEAoAgAAQhEAABCKAACAUAQAAIQiAAAgFAEAAKEIAAAIRQAAQCgCAABC
EQAAEIoAAIBQBAAAhCIAACAUAQAAoQgAAAhFAABAKAIAAEJxJCQACMUIAACEIgAAIBQBAAChCAAA
CEUAAEAoAgAAQhEAABCKAACAUAQAAIQiAAAgFAEAAKEIAAAIRQAAQCgCAABCEQAAEIoAAIBQBAAA
hCIAACAUAQAAoTgTGACEYgQAAEIRAAAQigAAgFAEAACEIgAAIJTT7gsABsWQoDliiLM6aFaUmuWB
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
bnss9LcXw6k0D/di0BEAaOHwCd7PnhycKfjJ4bfWx+99puuZZT3pDF/9yBMCAI1MGOH+1PFlJ03z
e91SVoum0tbTy3ruebpr1QbWdyLfCAC0E/CZn2gMnH1MYPzwYl4wumpD/LGlkUVNEQ7UhF0IAPR1
8Cj3nGn+k6b5h1U6VbHY0ppa1BR5dGn4/W2s7ITNCAB0Zxhq8jjPcYeVHDW5ZFytSxWgTEYtXxd7
4a3o82/2rG/mex+6IAAoJLVVzlmTfdMbfFPrPeUBh9KYZan3tiXfWBNb9m5syYpoFwelQT8EAIVq
bK1rar33I/WeiaPcY2pdDtP++8ahSGbN5sSK9+LL18aXr43xpQ/NEQAUA7fTGFfnmjDSPb7OPbLG
ObLGNXyI0z2YG89lLNUeSm/tSG3cnly7ObF2S3LtlkRbKD14PxHIOQKA4mQaqqbCWVPhGFLurA46
qoOOqjJHaYkZ8Jl+n5H902u6HIbToRw7/zSNdMZKprLrMpMpK5lWqZTVE890RTKhyM4/012RTHtX
emt7amtHantHmme1UOgIAAAIxTlNACAUAQAAoQgAAAhFAABAKAIAAEIRAAAQymlZbD4OABIxAgAA
oQgAAAhFAABAKAIAAEIRAAAQigAAgFAEAACEIgAAIBQBAAChCAAACEUAAEAoAgAAQhEAABCKAACA
UAQAAIQiAAAgFAEAAKEIAAAIRQAAQCjOBAYAoRgBAIBQBAAAhCIAACAUAQAAoQgAAAhFAABAKAIA
AEIRAAAQigAAgFAEAACEIgAAIBQBAAChCAAACEUAAEAoAgAAQhEAABCKAACAUAQAAITiSEgAEIoR
AAAIRQAAQCgCAABCEQAAEIoAAIBQBAAAhCIAACAUAQAAoQgAAAhFAABAKAIAAEIRAAAQigAAgFAE
AACEIgAAIBQBAAChCAAACEUAAEAoAgAAQnEmMAAIxQgAAIQiAAAgFAEAAKEIAAAIRQAAQCgCAABC
EQAAEIoAAIBQBAAAhCIAACAUAQAAoQgAAAhFAABAKAIAAEIRAAAQigAAgFAEAACEIgAAIBQBAACh
OBMYAIRiBAAAQhEAABCKAACAUAQAAIQiAAAgFAEAAKEIAAAIRQAAQCgCAABCEQAAEIoAAIBQBAAA
hCIAACAUAQAAoQgAAAhFAABAKAIAAEIRAAAQiiMhAUAoRgAAIBQBAAChCAAACEUAAEAoAgAAQhEA
ABCKAACAUAQAAIQiAAAgFAEAAKEIAAAIRQAAQCgCAABCEQAAEIoAAIBQBAAAhCIAACAUAQAAoQgA
AAjFmcAAIBQjAAAQigAAgFAEAACEIgAAIBQBAAChCAAACEUAAEAoAgAAQhEAABCKAACAUAQAAIQi
AAAgFAEAAKEIAAAIRQAAQCgCAABCEQAAEIoAAIBQBAAAhOJMYAAQihEAAAhFAABAKAIAAEIRAAAQ
igAAgFAEAACEIgAAIBQBAAChCAAACEUAAEAoAgAAQhEAABCKAACAUAQAAIQiAAAgFAEAAKEIAAAI
RQAAQMn0/77VifTzbZLjAAAAAElFTkSuQmCC"
"The app's 512px icon, a PNG in base64.")
(defconst emacs-web--asset-types
'(("manifest.webmanifest" "application/manifest+json" emacs-web--manifest)
("apple-touch-icon.png" "image/png" emacs-web--icon-180 base64)
("icon-512.png" "image/png" emacs-web--icon-512 base64))
"Files that make the pages an installable app: (NAME TYPE VARIABLE ENCODING).")
(defcustom emacs-web-transcribe-command
(list (expand-file-name "~/.local/share/uv/tools/mlx-whisper/bin/python"))
"Python that has mlx_whisper, to turn recorded speech into text."
:type '(repeat string))
(defcustom emacs-web-transcribe-model "mlx-community/whisper-large-v3-turbo"
"The Whisper model dictation goes through."
:type 'string)
(defcustom emacs-web-transcribe-languages '("en" "ru" "nl")
"Languages dictation may be in, as Whisper's codes.
Whisper picks the likeliest of them for each recording."
:type '(repeat string))
(defcustom emacs-web-list-actions nil
"Commands the home page's menu offers, as (LABEL . FUNCTION).
FUNCTION is called with no arguments, interactively if it is a
command, as `this-command', without any window here
changing; when it returns a buffer, the phone opens that buffer's page.
What FUNCTION asks in the minibuffer the phone asks too."
:type '(alist :key-type string :value-type function))
(defcustom emacs-web-completion-actions
'((emacs-web-embark-collect-label . emacs-web-embark-collect))
"Commands offered while Emacs asks in the minibuffer with completions.
Each is (LABEL . FUNCTION); LABEL is a string, or a function giving one
or nil to leave the command out. FUNCTION is called in the minibuffer;
when it returns a buffer, the question is left and the phone opens that
buffer's page."
:type '(alist :key-type (choice string function) :value-type function))
(defcustom emacs-web-icons
'(("pin" . "")
("unpin" . "")
("link" . "")
("save" . "")
("share" . "")
("folder" . "")
("trash" . ""))
"Icons a swipe action may show, as (NAME . SVG).
SVG is what goes inside an