1 "Smart console that uses a plugin to read/write in a structured way to the
2 console. It keeps a buffer of the data on screen so that it's possible to
3 switch between consoles when needed. On some platforms the plugin may open new
4 terminal windows, whereas on other platforms it may use a shared console window.
7 - keep a buffer of data written to terminals and support switching between
11 prototypes ensureNamespace: #terminals &delegate: True.
13 terminals define: #SmartConsole &parents: {ExternalResource} &slots: {
15 "TODO: update data in this, or use the ncurses screen dumping feature?"
19 #autoScrollsAtBottom -> True. "Does the console automatically scroll when a char is put at the bottom-right?"
20 #singleCharBuffer -> (ByteArray newSize: 1).
23 SmartConsole traits `>> [
24 define: #Stream &parents: {SmartConsole Stream}.
25 define: #Event &parents: {Cloneable}.
26 define: #ResizeEvent &parents: {SmartConsole Event}.
27 define: #EventStream &parents: {ReadStream} &slots: {#console}.
28 "TODO: there should be one EventStream per Console instance,
29 and when switching between console windows,
30 the event readers should be managed properly."
32 define: #KeyEvent &parents: {SmartConsole Event}
33 &slots: {#keyCode. "A raw and potentially platform specific (basically ncurses/windoze) key code"
34 #keyName. "Key name (e.g. LeftArrow) interned as a symbol"
35 #char. "A Character or Nil if special key"
36 #controls -> (BitArray new &capacity: 5) "Internal BitArray of control chars"}.
37 define: #KeyDownEvent &parents: {SmartConsole KeyEvent}.
40 e@(SmartConsole ResizeEvent traits) new
45 e@(SmartConsole KeyEvent traits) new
47 e clone `>> [| :event | controls := event controls clone. ]
50 e@(SmartConsole KeyEvent traits) newForKeyCode: code
52 e new `>> [keyCode := code. ]
55 e@(SmartConsole KeyEvent traits) controlState
57 e leftControlState \/ [e rightControlState]
60 e@(SmartConsole KeyEvent traits) altState
62 e leftAltState \/ [e rightAltState]
71 } doWithIndex: [| :name :index |
72 [| :e | e controls at: index]
73 asMethod: ((name ; 'State') as: Symbol)
74 on: {SmartConsole KeyEvent traits}.
75 [| :e :value | e controls at: index put: value]
76 asMethod: ((name ; 'State:') as: Symbol)
77 on: {SmartConsole KeyEvent traits. Boolean traits}.
80 "TODO: these are basically ncurses specific, move them"
81 SmartConsole traits ensureNamespace: #modes &slots:
91 SmartConsole traits ensureNamespace: #colors &slots:
101 SmartConsole addLazySlot: #events initializer: [| :c | c EventStream newOn: c].
102 SmartConsole addLazySlot: #keyEvents initializer: [| :c | c events select: [| :event | event is: c KeyEvent]].
104 s@(SmartConsole EventStream traits) on: console
106 s console := console.
110 _@(SmartConsole EventStream traits) eventType
111 [SmartConsole Event].
113 s@(SmartConsole EventStream traits) isAtEnd
114 [s console reader isAtEnd].
116 s@(SmartConsole EventStream traits) next
117 "Emulates simple SmartConsole events, SmartConsole implementations should
118 provide specialized versions for next and isAtEnd."
120 event := SmartConsole KeyDownEvent new.
121 char := s console reader next.
122 code := char as: Integer.
123 (code between: 32 and: 255) ifTrue: [event char := char].
124 event keyCode := code.
125 s console resolveEvent: event.
129 s@(SmartConsole EventStream traits) hasNext
130 "Answer wether an event is available. TODO: factor out into AsyncStream or something"
134 addSlot: #defaultForegroundColor valued: SmartConsole colors White.
135 addSlot: #defaultBackgroundColor valued: SmartConsole colors Black.
136 addSlot: #currentFgColor valued: SmartConsole colors White.
137 addSlot: #currentBgColor valued: SmartConsole colors Black.
138 addSlot: #singleCharBuffer valued: (ByteArray newSize: 1).
141 c@(SmartConsole traits) new
143 Platform current name = 'Windows'
144 ifTrue: [WindowsConsole isAvailable ifTrue: [^ WindowsConsole clone]]
145 ifFalse: [CursesConsole isAvailable ifTrue: [^ CursesConsole clone]].
147 error: 'No SmartConsole plugin was found'.
150 c@(SmartConsole traits) isAvailable
152 Platform current name = 'Windows'
153 ifTrue: [[WindowsConsole isAvailable] on: SeriousCondition do: [| :_ | ^ False]]
154 ifFalse: [CursesConsole isAvailable]
157 c@(SmartConsole traits) enable
161 c@(SmartConsole traits) open
164 c handle := 'foo'. "Something else then Nil for now"
167 c@(SmartConsole traits) resized
171 c@(SmartConsole traits) close
177 c@(SmartConsole traits) width
180 c@(SmartConsole traits) columnLast
183 _@(SmartConsole traits) columns
186 c@(SmartConsole traits) height
189 c@(SmartConsole traits) rowLast
192 _@(SmartConsole traits) rows
195 _@(SmartConsole traits) clear
198 _@(SmartConsole traits) clearToEOS
201 _@(SmartConsole traits) clearToEOL
204 _@(SmartConsole traits) elementType
207 _@(SmartConsole traits) collectionType
210 c@(SmartConsole traits) cursorPosition
212 {c cursorColumn. c cursorRow}
215 c@(SmartConsole traits) isCursorAtLastRow
217 c cursorRow = c rowLast
220 c@(SmartConsole traits) isCursorAtLastColumn
222 c cursorColumn = c columnLast
225 c@(SmartConsole traits) moveCursorTo: pos
227 oldpos := c cursorPosition.
228 c cursorColumn := pos first.
229 c cursorRow := pos second.
233 c@(SmartConsole traits) moveCursorWithOffset: offset
234 [| oldpos newpos newcol newrow |
235 oldpos := c cursorPosition.
236 newcol := c cursorColumn + offset first min: c columnLast max: 0.
237 newrow := c cursorRow + offset second min: c rowLast max: 0.
238 newpos := {newcol. newrow}.
239 oldpos = newpos ifFalse: [c moveCursorTo: newpos].
243 c@(SmartConsole traits) writePosition: data
245 c writePosition: data at: c cursorPosition
248 c@(SmartConsole traits) writePosition: data at: pos
250 oldpos := c moveCursorTo: pos.
251 c ; '(' ; (data first as: String) ; ',' ; (data second as: String) ; ')'.
252 c moveCursorTo: oldpos.
255 c@(SmartConsole traits) writeCursorPositionAt: pos
257 c writePosition: c cursorPosition at: pos
260 c@(SmartConsole traits) moveCursorToEOL
262 c moveCursorTo: {c columnLast. c cursorRow}
265 c@(SmartConsole traits) moveCursorToBOL
267 c moveCursorTo: {0. c cursorRow}
270 c@(SmartConsole traits) moveCursorToBONL
272 c moveCursorTo: {0. (c cursorRow + 1 min: c rowLast)}
275 c@(SmartConsole traits) moveCursorLeft
277 c moveCursorWithOffset: {-1. 0}
280 c@(SmartConsole traits) moveCursorRight
282 c moveCursorWithOffset: {1. 0}
285 c@(SmartConsole traits) moveCursorUp
287 c moveCursorWithOffset: {0. -1}
290 c@(SmartConsole traits) moveCursorDown
292 c moveCursorWithOffset: {0. 1}
295 c@(SmartConsole traits) scroll &lines: lines
298 c@(SmartConsole traits) updateCursorPositionFrom: col@(Collection traits)
300 col do: [| :char | c updateCursorPositionFrom: char].
303 c@(SmartConsole traits) updateCursorPositionFrom: char
306 $\n -> [c cursorRow < c rowLast ifTrue: [
307 c cursorRow := c cursorRow + 1].
308 c cursorColumn := 0].
309 $\b -> [c cursorColumn > 0 ifTrue: [c cursorColumn := c cursorColumn - 1]].
310 $\t -> [c cursorColumn := c cursorColumn + 8 min: c columnLast].
312 c cursorColumn < c columnLast
313 ifTrue: [c cursorColumn := c cursorColumn + 1]
314 ifFalse: [c cursorColumn := 0.
315 c cursorRow := c cursorRow + 1 min: c rowLast]].
318 c@(SmartConsole traits) read: n from: handle into: array@(ByteArray traits) startingAt: start
324 event := c keyEvents next.
327 array at: start + index put: (event char as: Integer).
333 _@(SmartConsole traits) flush
336 _@(SmartConsole traits) deleteChar
339 c@(SmartConsole traits) deleteLine
342 _@(SmartConsole traits) deleteLines: n
345 c@(SmartConsole traits) insertLine
348 _@(SmartConsole traits) insertLines: n
351 _@(SmartConsole traits) hideCursor
354 _@(SmartConsole traits) showCursor
357 c@(SmartConsole traits) attributeMode: mode
359 old := c currentMode.
360 c currentMode := mode.
364 c@(SmartConsole traits) foregroundColor: color
366 old := c currentFgColor.
367 c currentFgColor := color.
371 c@(SmartConsole traits) backgroundColor: color
373 old := c currentBgColor.
374 c currentBgColor := color.
378 c@(SmartConsole traits) withAttributesDo: block &mode: mode &foreground: fg &background: bg
379 [| oldmode oldfg oldbg |
380 mode ifNotNil: [oldmode := c attributeMode := mode].
381 foregound ifNotNil: [oldfg := c foregroundColor := fg].
382 background ifNotNil: [oldbg := c backgroundColor := bg].
386 oldmode ifNotNil: [c attributeMode := oldmode].
387 oldfg ifNotNil: [c foregroundColor := oldfg].
388 oldbg ifNotNil: [c backgroundColor := oldbg].
392 "SmartConsole Streaming"
394 SmartConsole traits `>> [
395 define: #ReadStream &parents: {ExternalResource ReadStream. SmartConsole Stream}.
396 define: #WriteStream &parents:
397 {PrettyPrinterMixin. ExternalResource WriteStream. SmartConsole Stream}.
398 define: #ReadWriteStream &parents: {SmartConsole ReadStream. SmartConsole WriteStream}.
401 SmartConsole ReadStream `>> [
405 SmartConsole WriteStream `>> [
409 c@(SmartConsole traits) ; seq
410 "A convenience method for the Console as a Stream resource; do NOT repeat this
411 pattern without determining that ; can have no other meaning for the resource."
414 "TODO support encoding"
415 s@(SmartConsole Stream traits) elementType
416 [s resource elementType].
418 s@(SmartConsole Stream traits) collectionType
419 [s resource collectionType].
421 _@(SmartConsole Stream traits) isAtEnd
424 s@(SmartConsole Stream traits) flush
427 r@(SmartConsole ReadStream traits) nextLine
429 (Terminal newBasedOn: r resource) commandEditor readLine &echoNewLine: True
432 w@(SmartConsole WriteStream traits) next: n putAll: seq startingAt: start
434 w resource updateCursorPositionFrom: (seq sliceFrom: start below: start + n).
438 terminals define: #StructuredConsole &parents: {SmartConsole} &slots:
439 {#originalConsole "TODO: this is a hack"}.
441 c@(StructuredConsole traits) derive &mixins: others &rejects: rejectSlots
442 "When derive is used, add some slots to the new traits. We need
443 these slots on each derived traits object."
447 define: #instances &builder: [ExtensibleArray new].
448 define: #sequenceToKeyName &builder: [Dictionary new].
449 define: #keyCodeToKeyName &builder: [Dictionary new].
456 c@(StructuredConsole traits) isAvailable
459 c@(StructuredConsole traits) isInStructuredMode
460 [c instances isEmpty not].
462 c@(StructuredConsole traits) assumeStructuredMode
464 c isInStructuredMode ifFalse:
465 [error: 'This call requires the console to be in structured mode'].
468 c@(StructuredConsole traits) enterStructuredMode
471 c@(StructuredConsole traits) enteredStructuredMode
472 "This method is called after entering structured mode. At the time it is called
473 it's safe to call other methods that require the console to be in structured mode."
476 c attributeMode := c modes Normal.
477 c foregroundColor := c defaultForegroundColor.
478 c backgroundColor := c defaultBackgroundColor.
481 c@(StructuredConsole traits) leaveStructuredMode
484 c@(StructuredConsole traits) leftStructuredMode
485 "This method is called after structured mode is left and console mode is
486 restored to its original state."
489 c@(StructuredConsole traits) open
493 ifTrue: [c instances add: c]
495 c enterStructuredMode ifFalse: [^ Nil].
497 c enteredStructuredMode.
498 "TODO this is a hack"
499 conditions define: #DebugConsole -> c.
500 c originalConsole := lobby Console.
501 lobby addSlot: #Console valued: c].
506 c@(StructuredConsole traits) close
508 c instances remove: c ifAbsent:
509 [error: 'Tried to close a console that is not open'. ^ Nil].
511 c isInStructuredMode ifFalse: [
512 c leaveStructuredMode.
513 "TODO this is a hack"
514 lobby addSlot: #Console valued: c originalConsole.
515 conditions define: #DebugConsole -> c originalConsole.
516 c leftStructuredMode].
520 _@(StructuredConsole traits) actualColumns
523 _@(StructuredConsole traits) actualRows
526 c@(StructuredConsole traits) resized
528 "Actualize width/height slots and resend"
529 c columns := c actualColumns.
530 c rows := c actualRows.
534 StructuredConsole traits define: #DefaultKeyMappings &builder:
574 c@(StructuredConsole traits) initKeyMappings
575 "Add some defaults if they are not already defined"
577 c DefaultKeyMappings do:
580 ifTrue: [| sequence |
582 (c sequenceToKeyName includesKey: sequence)
583 ifFalse: [c sequenceToKeyName at: sequence put: value]]
584 ifFalse: [| keyCode keyName |
586 (value is: Collection)
587 ifTrue: [keyName := value first]
588 ifFalse: [keyName := value. value := {keyName. Nil}].
589 (c keyCodeToKeyName includesKey: keyCode) not
590 /\ [(c keyCodeToKeyName detect: [| :each | each first = keyName]) isNil]
591 ifTrue: [c keyCodeToKeyName at: keyCode put: value]].
595 c@(StructuredConsole traits) printKeyMappings
596 "This method prints the key mappings to the console for debug purposes"
598 c sequenceToKeyName keysAndValuesDo: [| :key :value|
599 c ; ((key as: String) ; '->' ; (value as: String)) ; '\n'].
600 c keyCodeToKeyName keysAndValuesDo: [| :key :value |
601 c ; ((key as: String) ; '->' ; (value first as: String)) ; '\n'].
604 c@(StructuredConsole traits) resolveEvent: e
606 "TODO: handle encoding"
607 c keyCodeToKeyName at: e keyCode
608 ifPresent: [| :value |
609 e keyName := value first.
610 value second ifNotNil: [e char := value second]]
614 e leftControlState := True.
615 e char := (e keyCode bitOr: 2r01100000) as: ASCIICharacter]
618 ifTrue: [e char := e keyCode as: Character]]].
622 c@(StructuredConsole traits) translateEscapedSequence: seq into: event
626 [c keyCodeToKeyName at: (seq first as: Integer)
627 ifPresent: [| :value | event keyName := value first. ^ True]
629 [event leftAltState := True.
630 event char := seq first as: ASCIICharacter.
633 [event keyName := c sequenceToKeyName at: seq
634 ifAbsent: [^ False]].
638 c@(StructuredConsole traits) encode: n into: bytes@(ByteArray traits) from: seq &startingAt: start
639 "Filter out control chars except \n and \t and put ^ instead of them."
643 (byte := bytes at: index) < 32 /\ [(#{9. 10. 13} includes: byte) not]
644 ifTrue: [bytes at: index put: #[$^ as: Integer]]].