Compare commits

...

3 Commits

Author SHA1 Message Date
hans b41e312a80 Make rackedit tolerant when there's no GUI available 2026-08-22 14:05:07 +02:00
hans 0d3b55f252 Made it possible not to give a filename 2026-08-20 23:21:43 +02:00
hans f780c7f5b4 Documentation added 2026-08-20 22:31:25 +02:00
5 changed files with 329 additions and 201 deletions
+30 -7
View File
@@ -1,16 +1,22 @@
# rackedit # rackedit
`rackedit` opens a simple Racket editor window for a file. It can be used from `rackedit` opens a simple Racket editor window, optionally with a file. It can be
DrRacket, from a Racket program, or from the Racket command line. used from DrRacket, from a Racket program, or from the Racket command line.
The package exports one procedure: The package exports one procedure. With no filename, `rkdt` opens an empty editor:
```racket ```racket
(require rackedit) (require rackedit)
(rkdt)
```
Pass a filename to open an existing file:
```racket
(rkdt "notes.rkt") (rkdt "notes.rkt")
``` ```
By default `rkdt` shows the editor and returns immediately. Use `#:wait? #t` By default `rkdt` shows the editor and returns immediately. Use `#:wait? #t`
when execution should continue only after the editor is closed: when execution should continue only after the editor is closed:
@@ -22,7 +28,12 @@ The initial window geometry can be specified with `#:width`, `#:height`, `#:x`,
and `#:y`. Window positions and sizes are remembered per internal editor number and `#:y`. Window positions and sizes are remembered per internal editor number
and per screen configuration. and per screen configuration.
The frame can be customized with `#:frame-cb`. For example: The frame can be observed or customized with `#:frame-cb`. The callback receives
an event symbol and the editor frame. It is called at the lifecycle points
`'before-show`, `'after-show`, `'wait-for-close`, and `'on-close`.
`'wait-for-close` is emitted only when `#:wait? #t` is used.
For example, the window title can be changed before the frame becomes visible:
```racket ```racket
(require racket/class (require racket/class
@@ -30,12 +41,24 @@ The frame can be customized with `#:frame-cb`. For example:
(rkdt "notes.rkt" (rkdt "notes.rkt"
#:frame-cb #:frame-cb
(λ (frame) (λ (event frame)
(send frame set-label "Notes"))) (when (eq? event 'before-show)
(send frame set-label "Notes"))))
``` ```
The standard File menu does not contain the Exit/Quit command, so closing one 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. 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 Full API documentation is included as Scribble documentation in
`scrbl/rkdt.scrbl`. `scrbl/rkdt.scrbl`.
+1 -1
View File
@@ -1,7 +1,7 @@
#lang info #lang info
(define pkg-authors '(hnmdijkema)) (define pkg-authors '(hnmdijkema))
(define version "0.1.5") (define version "0.1.8")
(define license 'MIT) ; (define license 'MIT) ;
(define collection "rackedit") (define collection "rackedit")
(define pkg-desc "rackedit exports the rkdt procedure, that can be used to open an editor window with a given file") (define pkg-desc "rackedit exports the rkdt procedure, that can be used to open an editor window with a given file")
+22 -153
View File
@@ -1,164 +1,33 @@
#lang racket/base #lang racket/base
(require framework (require racket/runtime-path)
racket/gui/base
racket/class
simple-ini/class
)
(provide rkdt) (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-runtime-path rkdt-gui-path "private/gui.rkt")
(define open-editor-free-list '())
(define open-editor-use-list '())
(define ini (new ini% [file 'rackedit])) (define (rkdt [filename #f]
#:wait? [wait? #f]
(define editor-frame% #:width [width 900]
(frame:text-mixin #:height [height 650]
(frame:editor-mixin #:x [x 100]
(frame:standard-menus-mixin #:y [y 100]
(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
#:wait? [wait? #f]
#:width [width 900]
#:height [height 650]
#:x [x 100]
#:y [y 100]
#:frame-cb [frame-cb (λ (call-time frame) #t)]) #:frame-cb [frame-cb (λ (call-time frame) #t)])
(let* ((store-cfg #f) (define gui-rkdt
(closed (make-semaphore 0)) (dynamic-require rkdt-gui-path 'rkdt))
(frame (new (make-editor-frame% closed
(λ (num x y w h) (store-cfg num x y w h)))
[filename (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))))
(gui-rkdt filename
#:wait? wait?
#:width width
#:height height
#:x x
#:y y
#:frame-cb frame-cb))
+166
View File
@@ -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))))
+110 -40
View File
@@ -2,66 +2,121 @@
@(require (for-label racket/base @(require (for-label racket/base
racket/class racket/class
racket/contract
"../main.rkt")) "../main.rkt"))
@title{rackedit} @title{rackedit}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[rackedit] @defmodule[rackedit]
@tt{rackedit} provides a small editor window based on Racket's GUI framework. @tt{rackedit} provides a small editor window based on Racket's GUI framework.
It is intended for situations where a program, DrRacket interaction, or command It is intended for situations where a program, DrRacket interaction, or command
line session needs to open a file in a simple Racket editor without starting a line session needs a simple Racket editor, optionally for a specific file, without
separate full editor application. starting a separate full editor application.
@section{Opening an editor} @section{Opening an editor}
@defproc[(rkdt [filename path-string?] @defproc[(rkdt [filename (or/c path-string? #f) #f]
[#:wait? wait? boolean? #f] [#:wait? wait? boolean? #f]
[#:width width exact-positive-integer? 900] [#:width width exact-positive-integer? 900]
[#:height height exact-positive-integer? 650] [#:height height exact-positive-integer? 650]
[#:x x exact-integer? 100] [#:x x exact-integer? 100]
[#:y y exact-integer? 100] [#:y y exact-integer? 100]
[#:frame-cb frame-cb procedure? (λ (frame) #t)]) [#:frame-cb frame-cb procedure?
(λ (call-time frame) #t)])
void?]{ void?]{
Opens an editor window for @racket[filename]. The file name is passed to the @bold{Purpose.} Opens an editor window. When @racket[filename] is a path or
Racket editor frame and the editor is shown immediately. string, that file is opened. When @racket[filename] is @racket[#f], which is the
default, an empty editor is opened.
@bold{Preconditions.} When supplied, @racket[filename] identifies the file to
open. @racket[frame-cb] must accept two arguments: a lifecycle symbol and the
editor frame. The geometry arguments specify valid dimensions and screen
coordinates.
@bold{Postconditions.} An editor frame has been created and shown. When
@racket[wait?] is @racket[#f], @racket[rkdt] returns after the frame has been
shown and the @racket['after-show] callback has run. When @racket[wait?] is
@racket[#t], @racket[rkdt] waits until the frame is closed.
@bold{Result.} The procedure returns @racket[(void)]. The editor frame itself is
made available to @racket[frame-cb] during its lifecycle callbacks.
@bold{Internal workings.} A non-false @racket[filename] is converted to a
string before it is supplied to the Framework editor frame. A false filename is
passed through as @racket[#f], causing the frame to start without a file.
Each open editor receives an internal number. That
number is appended to the window title and is used to distinguish stored window
geometry. Numbers belonging to closed editors are reused. On close, the current
position and size are stored for that internal number and the current screen
configuration.
The @racket[#:width], @racket[#:height], @racket[#:x], and @racket[#:y] The @racket[#:width], @racket[#:height], @racket[#:x], and @racket[#:y]
arguments specify the initial geometry for a window for which no stored geometry arguments specify the initial geometry when no stored geometry is available.
is available. When an editor window is closed, @tt{rackedit} stores its position Stored geometry is associated with the screen configuration derived from the
and size. A later editor window with the same internal window number restores number and dimensions of the displays visible to Racket. Changing the monitor
that geometry. configuration therefore selects a different set of stored geometry values.
Stored geometry is associated with the current screen configuration. The screen The standard File menu does not contain the @onscreen{Exit} or
configuration is derived from the number and dimensions of the displays visible @onscreen{Quit} command. Closing an editor window closes only that window.
to Racket. Consequently, changing the monitor configuration causes the default
geometry, or geometry previously stored for that configuration, to be used.
If @racket[wait?] is @racket[#f], the procedure returns after the editor has
been shown. If @racket[wait?] is @racket[#t], the current event handler waits
until the editor window is closed before the procedure continues.
The @racket[frame-cb] procedure receives the editor frame. With the default
@racket[#:wait?] value it is called after the frame has been shown and can be
used to customize the frame, for example to change its title. When
@racket[wait?] is @racket[#t], the callback is called only after the window has
been closed. Its return value is ignored.
Each open editor has an internal number. That number is shown in the window
title and is also used to distinguish stored window geometries. Numbers of
closed editor windows are reused.
} }
@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
@racket[(frame-cb call-time frame)]. Its return value is ignored. The
@racket[call-time] argument has the following meanings.
@itemlist[
@item{@racket['before-show] is called after the frame has been fully created,
positioned, resized, numbered, and supplied with the callback, but before
@racket[show] is called. This is the preferred point for changes that
should already be visible when the window first appears, such as changing
the title.}
@item{@racket['after-show] is called immediately after @racket[show] has been
called. It can be used for actions that require a shown frame.}
@item{@racket['wait-for-close] is called only when @racket[#:wait? #t] is in
effect, immediately before @racket[rkdt] starts waiting for the frame to
close.}
@item{@racket['on-close] is called while the frame's close event is being
handled. At that point its geometry has been stored, its internal number
has been released for reuse, and the close semaphore has been posted.}
]
Thus, without waiting, the normal lifecycle is
@racket['before-show], @racket['after-show], and eventually
@racket['on-close]. With @racket[#:wait? #t], @racket['wait-for-close] occurs
between @racket['after-show] and @racket['on-close].
@section{Examples} @section{Examples}
The simplest use opens a file and returns immediately: The simplest use opens an empty editor and returns immediately:
@racketblock[ @racketblock[
(require rackedit) (require rackedit)
(rkdt)
]
Supply a filename to open a file:
@racketblock[
(rkdt "notes.rkt") (rkdt "notes.rkt")
] ]
@@ -72,7 +127,7 @@ closed:
(rkdt "notes.rkt" #:wait? #t) (rkdt "notes.rkt" #:wait? #t)
] ]
A frame callback can customize the editor frame: A frame callback can customize the editor before it becomes visible:
@racketblock[ @racketblock[
(require racket/class (require racket/class
@@ -80,8 +135,30 @@ A frame callback can customize the editor frame:
(rkdt "notes.rkt" (rkdt "notes.rkt"
#:frame-cb #:frame-cb
(λ (frame) (λ (call-time frame)
(send frame set-label "Notes"))) (when (eq? call-time 'before-show)
(send frame
set-label
(string-append (send frame get-label)
" - Rash")))))
]
The same callback can react to multiple lifecycle moments:
@racketblock[
(rkdt "notes.rkt"
#:wait? #t
#:frame-cb
(λ (call-time frame)
(case call-time
[(before-show)
(displayln "Editor will be shown")]
[(after-show)
(displayln "Editor is shown")]
[(wait-for-close)
(displayln "Waiting for the editor to close")]
[(on-close)
(displayln "Editor is closing")])))
] ]
The initial window geometry can be supplied explicitly: The initial window geometry can be supplied explicitly:
@@ -93,10 +170,3 @@ The initial window geometry can be supplied explicitly:
#:x 50 #:x 50
#:y 50) #:y 50)
] ]
@section{Window behavior}
The editor uses Racket's standard editor menus, except that the standard
@onscreen{Exit} or @onscreen{Quit} command is not added to the File menu.
Closing an editor window closes only that window and releases its internal
window number for reuse by a later editor window.