From b41e312a80a2290e047b4ffb3a6e91b1dab888cf Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Sat, 22 Aug 2026 14:05:07 +0200 Subject: [PATCH] Make rackedit tolerant when there's no GUI available --- README.md | 11 +++ info.rkt | 2 +- main.rkt | 175 ++++++----------------------------------------- private/gui.rkt | 166 ++++++++++++++++++++++++++++++++++++++++++++ scrbl/rkdt.scrbl | 13 ++++ 5 files changed, 212 insertions(+), 155 deletions(-) create mode 100644 private/gui.rkt diff --git a/README.md b/README.md index 28426bb..f6bd5eb 100644 --- a/README.md +++ b/README.md @@ -49,5 +49,16 @@ For example, the window title can be changed before the frame becomes visible: The standard File menu does not contain the Exit/Quit command, so closing one editor window does not imply exiting the application that opened it. +## Headless environments + +Requiring `rackedit` does not initialize Racket's GUI subsystem. This means that +`(require rackedit)` is safe in a headless Racket process, for example a server +process without access to an X11 or Wayland display. + +The GUI implementation is loaded only when `rkdt` is actually called. Calling +`rkdt` still requires a usable graphical environment. Programs that also support +a terminal editor can therefore choose that editor before calling `rkdt`, without +merely requiring `rackedit` causing GTK initialization. + Full API documentation is included as Scribble documentation in `scrbl/rkdt.scrbl`. diff --git a/info.rkt b/info.rkt index 94c23fc..f67f599 100644 --- a/info.rkt +++ b/info.rkt @@ -1,7 +1,7 @@ #lang info (define pkg-authors '(hnmdijkema)) -(define version "0.1.7") +(define version "0.1.8") (define license 'MIT) ; (define collection "rackedit") (define pkg-desc "rackedit exports the rkdt procedure, that can be used to open an editor window with a given file") diff --git a/main.rkt b/main.rkt index a4db70c..fc508c8 100644 --- a/main.rkt +++ b/main.rkt @@ -1,166 +1,33 @@ #lang racket/base -(require framework - racket/gui/base - racket/class - simple-ini/class - ) - +(require racket/runtime-path) (provide rkdt) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Internal stuff +;; Public interface +;; +;; Keep the GUI implementation out of this module. This allows packages to +;; require rackedit in a headless environment without initializing GTK. +;; The GUI is loaded only when rkdt is actually called. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(define open-editor-counter 0) -(define open-editor-free-list '()) -(define open-editor-use-list '()) - -(define ini (new ini% [file 'rackedit])) - -(define editor-frame% - (frame:text-mixin - (frame:editor-mixin - (frame:standard-menus-mixin - (frame:basic-mixin frame%))))) - -(define (make-editor-frame% closed close-cb) - (class editor-frame% - - (define my-number -1) - (define my-x -1) - (define my-y -1) - (define my-w -1) - (define my-h -1) - - (define/override (on-move x y) - (set! my-x x) - (set! my-y y) - (super on-move x y) - ) - - (define/override (on-size w h) - (set! my-w w) - (set! my-h h) - (super on-size w h) - ) - - (define/override (file-menu:create-quit?) - #f) - - (define cb (λ args #t)) - - (define/public (set-frame-cb! frame-cb) - (set! cb frame-cb) - ) - - (define/augment (on-close) - (close-cb my-number my-x my-y my-w my-h) - (set! open-editor-use-list - (filter (λ (x) - (when (= x my-number) - (set! open-editor-free-list - (sort (cons x open-editor-free-list) <))) - (not (= x my-number))) - open-editor-use-list)) - (semaphore-post closed) - (cb 'on-close this) - (inner (void) on-close)) - - (define/public (get-num) - my-number) - - (super-new) - - (begin - (if (null? open-editor-free-list) - (begin - (set! open-editor-counter (+ open-editor-counter 1)) - (set! my-number open-editor-counter)) - (begin - (set! my-number (car open-editor-free-list)) - (set! open-editor-free-list (cdr open-editor-free-list)))) - (set! open-editor-use-list (cons my-number open-editor-use-list)) - ) - - )) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Goal: start an editor window for a given file -;; pre : * -;; post: editor started -;; result: An editor window that can be used. -;; -;; The editor windows are numbered internally and for each editor -;; number, a window position x, y, w, h is stored. -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - +(define-runtime-path rkdt-gui-path "private/gui.rkt") (define (rkdt [filename #f] - #:wait? [wait? #f] - #:width [width 900] - #:height [height 650] - #:x [x 100] - #:y [y 100] + #:wait? [wait? #f] + #:width [width 900] + #:height [height 650] + #:x [x 100] + #:y [y 100] #:frame-cb [frame-cb (λ (call-time frame) #t)]) - (let* ((store-cfg #f) - (closed (make-semaphore 0)) - (frame (new (make-editor-frame% closed - (λ (num x y w h) (store-cfg num x y w h))) - [filename (if (eq? filename #f) - #f - (format "~a" filename))] - [editor% racket:text%] - [width 900] - [height 650] - [x 100] - [y 100])) - (editor (send frame get-editor)) - (my-num -1) - ) - - (define (get-win-id num) - (letrec ((displ (λ (i n) - (if (= i n) - "" - (let-values (((w h) (get-display-size #:monitor i))) - (string-append - (format "-~a-~a-~a" i w h) - (displ (+ i 1) n))))))) - (string->symbol - (format "win-~a~a" num (displ 0 (get-display-count)))))) - - (set! store-cfg (λ (num x y w h) - (let ((win (get-win-id num))) - (send ini set! 'geoms win (list x y w h))))) - - (send editor set-max-undo-history 100) - - (let* ((win-num (send frame get-num)) - (geom (send ini get 'geoms - (get-win-id win-num) - (list x y width height)))) - - (send frame move (car geom) (cadr geom)) - (send frame resize (caddr geom) (cadddr geom)) - - (send frame set-label (format "~a (~a)" - (send frame get-label) - win-num)) - - (send frame set-frame-cb! frame-cb) - (frame-cb 'before-show frame) - (send frame show #t) - (frame-cb 'after-show frame) - - (when wait? - (frame-cb 'wait-for-close frame) - (yield closed) - ) - - (void)))) - - + (define gui-rkdt + (dynamic-require rkdt-gui-path 'rkdt)) + (gui-rkdt filename + #:wait? wait? + #:width width + #:height height + #:x x + #:y y + #:frame-cb frame-cb)) diff --git a/private/gui.rkt b/private/gui.rkt new file mode 100644 index 0000000..a4db70c --- /dev/null +++ b/private/gui.rkt @@ -0,0 +1,166 @@ +#lang racket/base + +(require framework + racket/gui/base + racket/class + simple-ini/class + ) + +(provide rkdt) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Internal stuff +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define open-editor-counter 0) +(define open-editor-free-list '()) +(define open-editor-use-list '()) + +(define ini (new ini% [file 'rackedit])) + +(define editor-frame% + (frame:text-mixin + (frame:editor-mixin + (frame:standard-menus-mixin + (frame:basic-mixin frame%))))) + +(define (make-editor-frame% closed close-cb) + (class editor-frame% + + (define my-number -1) + (define my-x -1) + (define my-y -1) + (define my-w -1) + (define my-h -1) + + (define/override (on-move x y) + (set! my-x x) + (set! my-y y) + (super on-move x y) + ) + + (define/override (on-size w h) + (set! my-w w) + (set! my-h h) + (super on-size w h) + ) + + (define/override (file-menu:create-quit?) + #f) + + (define cb (λ args #t)) + + (define/public (set-frame-cb! frame-cb) + (set! cb frame-cb) + ) + + (define/augment (on-close) + (close-cb my-number my-x my-y my-w my-h) + (set! open-editor-use-list + (filter (λ (x) + (when (= x my-number) + (set! open-editor-free-list + (sort (cons x open-editor-free-list) <))) + (not (= x my-number))) + open-editor-use-list)) + (semaphore-post closed) + (cb 'on-close this) + (inner (void) on-close)) + + (define/public (get-num) + my-number) + + (super-new) + + (begin + (if (null? open-editor-free-list) + (begin + (set! open-editor-counter (+ open-editor-counter 1)) + (set! my-number open-editor-counter)) + (begin + (set! my-number (car open-editor-free-list)) + (set! open-editor-free-list (cdr open-editor-free-list)))) + (set! open-editor-use-list (cons my-number open-editor-use-list)) + ) + + )) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Goal: start an editor window for a given file +;; pre : * +;; post: editor started +;; result: An editor window that can be used. +;; +;; The editor windows are numbered internally and for each editor +;; number, a window position x, y, w, h is stored. +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(define (rkdt [filename #f] + #:wait? [wait? #f] + #:width [width 900] + #:height [height 650] + #:x [x 100] + #:y [y 100] + #:frame-cb [frame-cb (λ (call-time frame) #t)]) + + (let* ((store-cfg #f) + (closed (make-semaphore 0)) + (frame (new (make-editor-frame% closed + (λ (num x y w h) (store-cfg num x y w h))) + [filename (if (eq? filename #f) + #f + (format "~a" filename))] + [editor% racket:text%] + [width 900] + [height 650] + [x 100] + [y 100])) + (editor (send frame get-editor)) + (my-num -1) + ) + + (define (get-win-id num) + (letrec ((displ (λ (i n) + (if (= i n) + "" + (let-values (((w h) (get-display-size #:monitor i))) + (string-append + (format "-~a-~a-~a" i w h) + (displ (+ i 1) n))))))) + (string->symbol + (format "win-~a~a" num (displ 0 (get-display-count)))))) + + (set! store-cfg (λ (num x y w h) + (let ((win (get-win-id num))) + (send ini set! 'geoms win (list x y w h))))) + + (send editor set-max-undo-history 100) + + (let* ((win-num (send frame get-num)) + (geom (send ini get 'geoms + (get-win-id win-num) + (list x y width height)))) + + (send frame move (car geom) (cadr geom)) + (send frame resize (caddr geom) (cadddr geom)) + + (send frame set-label (format "~a (~a)" + (send frame get-label) + win-num)) + + (send frame set-frame-cb! frame-cb) + (frame-cb 'before-show frame) + (send frame show #t) + (frame-cb 'after-show frame) + + (when wait? + (frame-cb 'wait-for-close frame) + (yield closed) + ) + + (void)))) + + + diff --git a/scrbl/rkdt.scrbl b/scrbl/rkdt.scrbl index b0fb6e1..5c995ea 100644 --- a/scrbl/rkdt.scrbl +++ b/scrbl/rkdt.scrbl @@ -64,6 +64,19 @@ The standard File menu does not contain the @onscreen{Exit} or @onscreen{Quit} command. Closing an editor window closes only that window. } +@section{Headless environments} + +Loading @racketmodname[rackedit] does not initialize Racket's GUI subsystem. +The GUI implementation is loaded dynamically only when @racket[rkdt] is +actually called. This allows another package to depend on @tt{rackedit} and to +be loaded in a headless process without immediately attempting to initialize +GTK or connect to a graphical display. + +Calling @racket[rkdt] itself still requires a usable graphical environment. A +program that supports both graphical and terminal editors can therefore select +a terminal editor before calling @racket[rkdt], while safely keeping +@tt{rackedit} as a dependency. + @section{Frame callback lifecycle} The procedure supplied with @racket[#:frame-cb] is called as