Packages

A modern terminal control library for LFE

Current section

Files

Jump to
xrepl_term src xrepl-term.lfe
Raw

src/xrepl-term.lfe

(defmodule xrepl-term
"Core terminal control and detection.
Provides terminal capability detection, control sequences,
hyperlinks, notifications, and utility functions."
(export
;; Terminal detection
(detect-terminal 0)
(capabilities 0)
(terminal-info 0)
;; Terminal control
(clear-screen 0)
(clear-line 0)
(set-title 1)
(set-cwd 1)
(bell 0)
(urgent 0)
;; Screen management
(enter-alt-screen 0)
(exit-alt-screen 0)
(with-alt-screen 1)
;; Cursor control
(cursor-hide 0)
(cursor-show 0)
(cursor-save 0)
(cursor-restore 0)
(cursor-position 2)
(cursor-up 1)
(cursor-down 1)
(cursor-forward 1)
(cursor-backward 1)
;; Hyperlinks (OSC 8)
(link 2)
(link 3)
;; Notifications
(notify 1)
(notify 2)
;; Terminal queries
(terminal-size 0)
;; Progress indicators
(progress-bar 2)
(progress-bar 3)
;; Utilities
(supports? 1)))
;;; Terminal Detection
(defun detect-terminal ()
"Detect terminal type.
Returns:
wezterm | iterm2 | kitty | apple-terminal | vscode | unknown"
(let ((term-program (os:getenv "TERM_PROGRAM"))
(term (os:getenv "TERM")))
(cond
((== term-program "WezTerm") 'wezterm)
((== term-program "iTerm.app") 'iterm2)
((== term-program "Apple_Terminal") 'apple-terminal)
((== term-program "vscode") 'vscode)
((== term "xterm-kitty") 'kitty)
((andalso (is_list term-program)
(=/= (string:find term-program "kitty") 'nomatch))
'kitty)
('true 'unknown))))
(defun terminal-info ()
"Get detailed terminal information.
Returns:
Map with terminal metadata"
(let* ((term-program (os:getenv "TERM_PROGRAM"))
(term (os:getenv "TERM"))
(version (os:getenv "TERM_PROGRAM_VERSION"))
(colorterm (os:getenv "COLORTERM")))
`#m(terminal ,(detect-terminal)
term-program ,(if (=/= term-program 'false) term-program "unknown")
term ,(if (=/= term 'false) term "unknown")
version ,(if (=/= version 'false) version "unknown")
colorterm ,(if (=/= colorterm 'false) colorterm "unknown"))))
(defun capabilities ()
"Detect terminal capabilities.
Returns:
Map of supported features"
(let ((terminal (detect-terminal)))
(maps:from_list
`(#(terminal ,terminal)
#(graphics ,(detect-graphics-support terminal))
#(hyperlinks ,(supports-hyperlinks? terminal))
#(notifications ,(supports-notifications? terminal))
#(true-colour ,(supports-true-colour?))
#(unicode ,(supports-unicode?))
#(alt-screen true)
#(cursor-control true)))))
(defun detect-graphics-support (terminal)
"Detect graphics protocol support.
Returns:
List of supported protocols: (iterm2), (kitty), etc."
(case terminal
('wezterm '(iterm2))
('iterm2 '(iterm2))
('kitty '(kitty iterm2))
(_ '())))
(defun supports-hyperlinks? (terminal)
"Check if terminal supports OSC 8 hyperlinks."
(lists:member terminal '(wezterm iterm2 kitty)))
(defun supports-notifications? (terminal)
"Check if terminal supports desktop notifications."
(lists:member terminal '(iterm2 kitty)))
(defun supports-true-colour? ()
"Check if terminal supports 24-bit colour."
(case (os:getenv "COLORTERM")
("truecolor" 'true)
("24bit" 'true)
(_ 'false)))
(defun supports-unicode? ()
"Check if terminal supports Unicode."
(case (os:getenv "LANG")
('false 'false)
(lang (=/= (string:find lang "UTF") 'nomatch))))
(defun supports? (capability)
"Check if terminal supports a specific capability.
Args:
capability: Atom like 'graphics, 'hyperlinks, 'true-colour
Returns:
true | false"
(let ((caps (capabilities)))
(case capability
('graphics (not (== (maps:get 'graphics caps) '())))
('hyperlinks (maps:get 'hyperlinks caps))
('notifications (maps:get 'notifications caps))
('true-colour (maps:get 'true-colour caps))
('unicode (maps:get 'unicode caps))
(_ 'false))))
;;; Terminal Control
(defun clear-screen ()
"Clear entire screen and move cursor to home."
(io:put_chars "\e[2J\e[H")
'ok)
(defun clear-line ()
"Clear current line."
(io:put_chars "\e[2K")
'ok)
(defun set-title (title)
"Set terminal window/tab title (OSC 0).
Args:
title: String or binary
Returns:
ok"
(let ((title-str (to-string title)))
(io:format "\e]0;~s\a" (list title-str)))
'ok)
(defun set-cwd (path)
"Tell terminal the current working directory (OSC 7).
Enables terminal features like 'Open in Finder/Explorer'.
Args:
path: Directory path (string or binary)
Returns:
ok"
(let* ((path-str (to-string path))
(hostname (case (inet:gethostname)
(`#(ok ,host) host)
(_ "localhost")))
;; Simple URL encoding for path
(encoded-path (uri_string:quote path-str)))
(io:format "\e]7;file://~s~s\a" (list hostname encoded-path)))
'ok)
(defun bell ()
"Ring terminal bell (usually visual flash)."
(io:put_chars "\a")
'ok)
(defun urgent ()
"Set urgent flag (flash taskbar/dock icon)."
(bell))
(defun to-string (value)
"Convert value to string."
(cond
((is_binary value) (binary_to_list value))
((is_list value) value)
((is_atom value) (atom_to_list value))
('true (lists:flatten (io_lib:format "~p" (list value))))))
;;; Screen Management
(defun enter-alt-screen ()
"Switch to alternate screen buffer.
Like vim/less - creates clean screen, preserves original."
(io:put_chars "\e[?1049h")
'ok)
(defun exit-alt-screen ()
"Return to normal screen buffer."
(io:put_chars "\e[?1049l")
'ok)
(defun with-alt-screen (fun)
"Execute function in alternate screen buffer.
Automatically returns to normal screen when done.
Args:
fun: Zero-arity function to execute
Returns:
{ok, result} | {error, reason}"
(enter-alt-screen)
(try
(let ((result (funcall fun)))
(exit-alt-screen)
(tuple 'ok result))
(catch
((tuple class reason stacktrace)
(exit-alt-screen)
(tuple 'error (tuple class reason))))))
;;; Cursor Control
(defun cursor-hide ()
"Hide cursor (DECTCEM)."
(io:put_chars "\e[?25l")
'ok)
(defun cursor-show ()
"Show cursor (DECTCEM)."
(io:put_chars "\e[?25h")
'ok)
(defun cursor-save ()
"Save cursor position (DECSC)."
(io:put_chars "\e7")
'ok)
(defun cursor-restore ()
"Restore cursor position (DECRC)."
(io:put_chars "\e8")
'ok)
(defun cursor-position (row col)
"Move cursor to position (CUP).
Args:
row: Row number (1-based)
col: Column number (1-based)"
(io:format "\e[~p;~pH" (list row col))
'ok)
(defun cursor-up (n)
"Move cursor up n lines (CUU)."
(io:format "\e[~pA" (list n))
'ok)
(defun cursor-down (n)
"Move cursor down n lines (CUD)."
(io:format "\e[~pB" (list n))
'ok)
(defun cursor-forward (n)
"Move cursor forward n columns (CUF)."
(io:format "\e[~pC" (list n))
'ok)
(defun cursor-backward (n)
"Move cursor backward n columns (CUB)."
(io:format "\e[~pD" (list n))
'ok)
;;; Hyperlinks
(defun link (url text)
"Create clickable hyperlink (OSC 8).
Args:
url: Target URL (string)
text: Link text to display (string)
Returns:
{ok, string} | {error, not-supported}"
(link url text #m()))
(defun link (url text opts)
"Create hyperlink with options.
Args:
url: Target URL
text: Link text
opts: Options map (reserved for future use)
Returns:
{ok, string} | {error, not-supported}"
(case (supports? 'hyperlinks)
('true
(let ((url-str (to-string url))
(text-str (to-string text)))
(tuple 'ok (++ "\e]8;;" url-str "\e\\" text-str "\e]8;;\e\\"))))
('false
(tuple 'error 'not-supported))))
;;; Notifications
(defun notify (message)
"Send desktop notification.
Args:
message: Notification message (string)
Returns:
{ok, sent} | {error, not-supported}"
(notify "" message))
(defun notify (title message)
"Send desktop notification with title.
Args:
title: Notification title (string, can be empty)
message: Notification message (string)
Returns:
{ok, sent} | {error, not-supported}"
(case (supports? 'notifications)
('true
(let* ((title-str (to-string title))
(msg-str (to-string message))
(full-msg (if (== title-str "")
msg-str
(++ title-str ": " msg-str))))
;; OSC 9 - iTerm2 notification format
(io:format "\e]9;~s\a" (list full-msg))
(tuple 'ok 'sent)))
('false
(tuple 'error 'not-supported))))
;;; Terminal Queries
(defun terminal-size ()
"Get terminal size in columns and rows.
Returns:
{ok, {cols, rows}} | {error, reason}"
(case (io:columns)
(`#(ok ,cols)
(case (io:rows)
(`#(ok ,rows)
(tuple 'ok (tuple cols rows)))
(error error)))
(error error)))
;;; Progress Indicators
(defun progress-bar (progress label)
"Display progress bar.
Args:
progress: Float between 0.0 and 1.0
label: Label text (string)
Returns:
ok"
(progress-bar progress label #m()))
(defun progress-bar (progress label opts)
"Display progress bar with options.
Args:
progress: Float between 0.0 and 1.0
label: Label text
opts: Options map:
- width: Bar width in characters (default 40)
- filled-char: String for filled portion (default \"=\")
- empty-char: String for empty portion (default \" \")
Returns:
ok"
(let* ((width (maps:get 'width opts 40))
(filled-char (maps:get 'filled-char opts "="))
(empty-char (maps:get 'empty-char opts " "))
(filled (round (* progress width)))
(empty (- width filled))
(bar (++ (string:copies filled-char filled)
(string:copies empty-char empty)))
(percent (round (* progress 100))))
(io:format "\r~s [~s] ~p%" (list label bar percent))
'ok))