Compare commits

..

5 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
hans 96778ac47d Removed unnecessary stuff 2026-08-20 19:13:58 +02:00
hans 86ebac5578 frame callback perfectioned 2026-08-20 19:13:12 +02:00
6 changed files with 428 additions and 151 deletions
+3
View File
@@ -16,3 +16,6 @@ compiled/
# Dependency tracking files
*.dep
scrbl/*.html
scrbl/*.js
scrbl/*.css
+62 -1
View File
@@ -1,3 +1,64 @@
# rackedit
Starts a simple racket editor window from DrRacket or the racket command line, given a file to edit.
`rackedit` opens a simple Racket editor window, optionally with a file. It can be
used from DrRacket, from a Racket program, or from the Racket command line.
The package exports one procedure. With no filename, `rkdt` opens an empty editor:
```racket
(require rackedit)
(rkdt)
```
Pass a filename to open an existing file:
```racket
(rkdt "notes.rkt")
```
By default `rkdt` shows the editor and returns immediately. Use `#:wait? #t`
when execution should continue only after the editor is closed:
```racket
(rkdt "notes.rkt" #:wait? #t)
```
The initial window geometry can be specified with `#:width`, `#:height`, `#:x`,
and `#:y`. Window positions and sizes are remembered per internal editor number
and per screen configuration.
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
(require racket/class
rackedit)
(rkdt "notes.rkt"
#:frame-cb
(λ (event frame)
(when (eq? event 'before-show)
(send frame set-label "Notes"))))
```
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`.
+1 -3
View File
@@ -1,7 +1,7 @@
#lang info
(define pkg-authors '(hnmdijkema))
(define version "0.1.1")
(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")
@@ -25,5 +25,3 @@
"rackunit-lib"
"scribble-lib"
))
+23 -146
View File
@@ -1,156 +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/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)
(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 (λ (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 (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))
(string-append (send frame get-label)
" - Rash"))
(send frame show #t)
(when wait? (yield closed))
(frame-cb frame)
(void)))
(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]
#:frame-cb [frame-cb (λ (call-time frame) #t)])
(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))
+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))))
+172
View File
@@ -0,0 +1,172 @@
#lang scribble/manual
@(require (for-label racket/base
racket/class
racket/contract
"../main.rkt"))
@title{rackedit}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[rackedit]
@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
line session needs a simple Racket editor, optionally for a specific file, without
starting a separate full editor application.
@section{Opening an editor}
@defproc[(rkdt [filename (or/c path-string? #f) #f]
[#:wait? wait? boolean? #f]
[#:width width exact-positive-integer? 900]
[#:height height exact-positive-integer? 650]
[#:x x exact-integer? 100]
[#:y y exact-integer? 100]
[#:frame-cb frame-cb procedure?
(λ (call-time frame) #t)])
void?]{
@bold{Purpose.} Opens an editor window. When @racket[filename] is a path or
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]
arguments specify the initial geometry when no stored geometry is available.
Stored geometry is associated with the screen configuration derived from the
number and dimensions of the displays visible to Racket. Changing the monitor
configuration therefore selects a different set of stored geometry values.
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
@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}
The simplest use opens an empty editor and returns immediately:
@racketblock[
(require rackedit)
(rkdt)
]
Supply a filename to open a file:
@racketblock[
(rkdt "notes.rkt")
]
Use @racket[#:wait? #t] when execution should continue only after the editor is
closed:
@racketblock[
(rkdt "notes.rkt" #:wait? #t)
]
A frame callback can customize the editor before it becomes visible:
@racketblock[
(require racket/class
rackedit)
(rkdt "notes.rkt"
#:frame-cb
(λ (call-time frame)
(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:
@racketblock[
(rkdt "notes.rkt"
#:width 1200
#:height 800
#:x 50
#:y 50)
]