tui_ssh.inc
8l_HeavyThing/tui_ssh.inc
	; ------------------------------------------------------------------------
	; HeavyThing x86_64 assembly language library and showcase programs
	; Copyright Â© 2015, 2016 2 Ton Digital 
	; Homepage: https://2ton.com.au/
	; Author: Jeff Marrison <jeff@2ton.com.au>
	;       
	; This file is part of the HeavyThing library.
	;       
	; HeavyThing is free software: you can redistribute it and/or modify
	; it under the terms of the GNU General Public License, or
	; (at your option) any later version.
	;       
	; HeavyThing is distributed in the hope that it will be useful, 
	; but WITHOUT ANY WARRANTY; without even the implied warranty of
	; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
	; GNU General Public License for more details.
	;       
	; You should have received a copy of the GNU General Public License along
	; with the HeavyThing library. If not, see <http://www.gnu.org/licenses/>.
	; ------------------------------------------------------------------------
	;       
	; tui_ssh.inc: terminal support for ssh, meant for use in the context of an ssh server
	;
	;	this has to do a number of ssh-specialized things, mainly dynamic window resizing
	;	and specialized clone operations
	;
	; this is an io descendent for io chaining, but _requires_ that its next in line be
	; an ssh object (because of our specialized callback handling for the ssh protocol itself)
	; (this is a semi-loose requirement, insofar as an intermediary or muxer could be put in
	; between the two if enough care is taken)
	;
	; tui_ssh$new is really the only one required, passed to it the tui_object descended onlychild
	; and this will take care of the rest provided the caller to tui_ssh$new establishes the io
	; chain correctly
	;

if used tui_ssh$vtable | defined include_everything
	; our io descendent vtable for the tui_ssh action
dalign
tui_ssh$vtable:
	dq	tui_ssh$destroy, tui_ssh$clone, tui_ssh$connected, io$send, tui_ssh$receive, io$error, io$timeout

end if


if used tui_ssh$new | defined include_everything

tui_ssh_renderer_ofs = io_base_size			; when vconnected gets called
tui_ssh_onlychild_ofs = io_base_size + 8		; this is appendchild'd to the renderer
tui_ssh_raddr_ofs = io_base_size + 16
tui_ssh_raddrlen_ofs = io_base_size + 126
tui_ssh_size = io_base_size + 134

	; single argument in rdi: the display tui_object to get created/cloned as the first child when connect happens
	; NOTE: argument in rdi cannot be NULL, and it doesn't make sense to use this without a TUI object in the first place
	; returns a new tui_ssh object suitable for io chaining, but does not create a parent/child for the io layer
falign
tui_ssh$new:
	prolog	tui_ssh$new
	push	rdi
	mov	edi, tui_ssh_size
	call	heap$alloc_clear
	mov	rsi, [rsp]
	mov	qword [rax], tui_ssh$vtable
	mov	[rax+tui_ssh_onlychild_ofs], rsi
	mov	[rsp], rax
	; create our renderer layer
	mov	rdi, rax
	call	tui_ssh_renderer$new
	mov	rdi, rax
	pop	rax
	mov	[rax+tui_ssh_renderer_ofs], rdi
	epilog

end if

if used tui_ssh_renderer$new | defined include_everything

; we store precisely one pointer in the renderer object
tui_ssh_renderer_base_ofs = tui_render_size		; pointer to the tui_ssh object
tui_ssh_renderer_size = tui_render_size + 8


; this is the default tui_render vtable, except for ansioutput and exit
dalign
tui_ssh_renderer$vtable:
        dq      tui_render$cleanup, tui_object$clone, tui_object$draw, tui_object$redraw, tui_render$updatedisplaylist, tui_object$sizechanged
        dq      tui_object$timer, tui_render$layoutchanged, tui_object$move, tui_object$setfocus, tui_object$gotfocus, tui_object$lostfocus
        dq      tui_object$keyevent, tui_object$domodal, tui_object$endmodal, tui_ssh_renderer$exit, tui_object$calcbounds, tui_object$calcchildbounds
        dq      tui_object$appendchild, tui_object$appendbastard, tui_object$prependchild, tui_object$contains, tui_object$getchildindex
        dq      tui_object$removechild, tui_object$removebastard, tui_object$removeallchildren, tui_object$removeallbastards
        dq      tui_object$getobjectsunderpoint, tui_object$flatten, tui_object$firekeyevent, tui_object$ontab, tui_object$onshifttab
        dq      tui_render$setcursor, tui_render$showcursor, tui_render$hidecursor, tui_object$click, tui_object$clicked
        ; our render-specific additions:
        dq      tui_ssh_renderer$ansioutput, tui_render$newlayoutonresize, tui_render$newwindowsize

	; single argument in rdi: our ssh base pointer, returns a new initialized tui_ssh_renderer object in rax
falign
tui_ssh_renderer$new:
	prolog	tui_ssh_renderer$new
	push	rdi
	mov	edi, tui_ssh_renderer_size
	call	heap$alloc_clear
	mov	rdx, [rsp]
	mov	rdi, rax
	mov	[rsp], rax
	mov	[rax+tui_ssh_renderer_base_ofs], rdx
	mov	qword [rax], tui_ssh_renderer$vtable
	call	tui_render$init_defaults
	pop	rax
	epilog

end if


if used tui_ssh_renderer$ansioutput | defined include_everything
	; three arguments: rdi == our tui_ssh_renderer object, rsi == pointer to utf8 bytes to send out, rdx == count
falign
tui_ssh_renderer$ansioutput:
	prolog	tui_ssh_renderer$ansioutput
	mov	rdi, [rdi+tui_ssh_renderer_base_ofs]
	mov	rcx, [rdi]
	call	qword [rcx+io_vsend]
	epilog

end if

if used tui_ssh_renderer$exit | defined include_everything
	; two arguments: rdi == our tui_ssh_renderer object, esi == exit code
falign
tui_ssh_renderer$exit:
	prolog	tui_ssh_renderer$exit
	; we don't really care about the exit code, but we do need our base object
	push	r12 r13 r14
	mov	r12, [rdi+tui_ssh_renderer_base_ofs]
	mov	edi, [rdi+tui_width_ofs]	; its height
	mov	esi, 10
	call	string$from_unsigned
	mov	r13, rax
	sub	rsp, 128
	mov	rdi, rsp
	mov	rsi, .seeyalater
	mov	edx, .seeyalater_size
	call	memcpy
	mov	rdi, r13
	lea	rsi, [rsp+.seeyalater_size]
	call	string$to_utf8
	mov	r14d, eax			; how many bytes it wrote
	lea	rdi, [rsp+rax+.seeyalater_size]
	mov	rsi, .reallyseeya
	mov	edx, .reallyseeya_size
	call	memcpy
	mov	rdi, r12
	mov	rsi, rsp
	mov	edx, r14d
	add	edx, .seeyalater_size + .reallyseeya_size
	mov	rcx, [rdi]
	call	qword [rcx+io_vsend]
	mov	rdi, r13
	call	heap$free
	add	rsp, 128

	; so now, hopefully that went down the wire, ideally we need to let the ssh object know that it wants a cleanexit, and well-behaved clients
	; will exit as soon as we send the goods
	mov	rdi, [r12+io_child_ofs]		; better be our ssh object
	call	ssh$cleanexit
	pop	r14 r13 r12
	epilog
if tui_ssh_alternatescreen

dalign
.seeyalater:
        db      27,'[r',27,'[?25h',27,'['
        .seeyalater_size = $ - .seeyalater
dalign
.reallyseeya:
        db      ';1H',27,'[?1049l!!Exit!!',13,10
        .reallyseeya_size = $ - .reallyseeya

else

dalign
.seeyalater:
        db      27,'[r',27,'[?25h',27,'['
        .seeyalater_size = $ - .seeyalater
dalign
.reallyseeya:
        db      ';1H!!Exit!!',13,10
        .reallyseeya_size = $ - .reallyseeya

end if


end if



if used tui_ssh$destroy | defined include_everything
	; single argument in rdi: our tui_ssh object
	; similar to io$destroy, only that we cleanup our renderer as well
falign
tui_ssh$destroy:
	prolog	tui_ssh$destroy
	push	rdi
	mov	rdi, [rdi+tui_ssh_renderer_ofs]
	mov	rsi, [rdi]
	call	qword [rsi+tui_vcleanup]
	mov	rdx, [rsp]
	mov	rdi, [rdx+tui_ssh_renderer_ofs]
	call	heap$free
	; if we still have an onlychild pointer, it needs to be taken care of as well
	mov	rdx, [rsp]
	mov	rdi, [rdx+tui_ssh_onlychild_ofs]
	test	rdi, rdi
	jz	.nochild
	mov	rsi, [rdi]
	call	qword [rsi+tui_vcleanup]
	mov	rdx, [rsp]
	mov	rdi, [rdx+tui_ssh_onlychild_ofs]
	call	heap$free
calign
.nochild:
	; now that our renderer has been taken care of, we can pass the rest
	; of our effort onto io$destroy
	pop	rdi
	call	io$destroy
	epilog

end if

if used tui_ssh$clone | defined include_everything
	; single argument in rdi: our tui_ssh object to clone
	; returns new (copy), with new _copies_ of our onlychild (and any other io children)
falign
tui_ssh$clone:
	prolog	tui_ssh$clone
	sub	rsp, 16
	mov	[rsp], rdi
	; we are free to create a virgin/non-cloned renderer, but we need to clone our onlychild, as well as our io children
	mov	rdi, [rdi+tui_ssh_onlychild_ofs]
	mov	rsi, [rdi]
	call	qword [rsi+tui_vclone]
	mov	[rsp+8], rax
	mov	edi, tui_ssh_size
	call	heap$alloc_clear
	mov	rsi, [rsp+8]
	mov	qword [rax], tui_ssh$vtable
	mov	[rax+tui_ssh_onlychild_ofs], rsi
	mov	[rsp+8], rax
	; create our renderer layer
	mov	rdi, rax
	call	tui_ssh_renderer$new
	mov	rdi, rax
	mov	rax, [rsp+8]
	mov	[rax+tui_ssh_renderer_ofs], rdi

	; so now, we need to determine whether we have children or not (which we probably do)
	mov	rdi, [rsp]
	cmp	qword [rdi+io_child_ofs], 0
	je	.nochild
	mov	rdi, [rdi+io_child_ofs]
	mov	rsi, [rdi]	; virtual method pointer location for the child
	call	qword [rsi+io_vclone]
	mov	rsi, rax	; new child
	mov	rdx, [rsp]	; our original object
	mov	rcx, [rdx]	; our original's vmethod pointer
	mov	rax, [rsp+8]	; our new object
	mov	[rax], rcx	; set our new object's vmethod pointer
	mov	qword [rax+io_parent_ofs], 0	; parent = null
	mov	[rax+io_child_ofs], rsi		; our child
	mov	[rsi+io_parent_ofs], rax	; set our child's parent to us
	add	rsp, 16
	epilog
calign
.nochild:
	; rax is already setup, and we have a clean version already done, return
	add	rsp, 16
	epilog
end if

if used tui_ssh$wsize | defined include_everything
	; three arguments: rdi == our tui_ssh object, esi == width, edx == height
falign
tui_ssh$wsize:
	prolog	tui_ssh$wsize
	mov	rdi, [rdi+tui_ssh_renderer_ofs]
	mov	rcx, [rdi]
	call	qword [rcx+tui_vnewwindowsize]
	epilog

end if

if used tui_ssh$connected | defined include_everything
	; single argument in rdi: our tui_ssh object
	; this is overridden because now is the time that we actually do our appendchild to the rendering layer
falign
tui_ssh$connected:
	prolog	tui_ssh$connected
	; copy our remote address
	push	rdi
	mov	[rdi+tui_ssh_raddrlen_ofs], edx
	lea	rdi, [rdi+tui_ssh_raddr_ofs]
	call	memcpy
	pop	rdi

	; if alternatescreen is enabled, before we do _anything_, we need to send out the altscreen goods
	push	rdi
	mov	rsi, .altscr
	mov	edx, .altscrsize
	mov	rcx, [rdi]
	call	qword [rcx+io_vsend]
	pop	rdi
	; now, and only now, do we set our wsizecb for the ssh layer, and this is because: at tui_ssh$new, we don't necessarily have a child ssh object yet
	; and further, before now we aren't interested anyway
	mov	rsi, [rdi+io_child_ofs]		; our underlying ssh object, which better exist, otherwise how did we get called in the first place?
	mov	qword [rsi+ssh_wsizecb_ofs], tui_ssh$wsize
	mov	[rsi+ssh_wsizecbarg_ofs], rdi
	; so now that we did that, we also need to pick out its width/height
	mov	edx, [rsi+ssh_height_ofs]
	mov	esi, [rsi+ssh_width_ofs]
	push	rdi
	mov	rdi, [rdi+tui_ssh_renderer_ofs]
	mov	rcx, [rdi]
	call	qword [rcx+tui_vnewwindowsize]

	pop	rdi
	; and finally, we need to append our only child
	mov	rsi, [rdi+tui_ssh_onlychild_ofs]
	mov	qword [rdi+tui_ssh_onlychild_ofs], 0
	mov	rdi, [rdi+tui_ssh_renderer_ofs]
	mov	rdx, [rdi]
	call	qword [rdx+tui_vappendchild]
	epilog
if tui_ssh_alternatescreen
dalign
.altscr db 27,'[?1049h',27,'[12h'
.altscrsize = $ - .altscr
else
dalign
.altscr db 27,'[12h'
.altscrsize = $ - .altscr

end if

end if



if used tui_ssh$receive | defined include_everything
	; three arguments: rdi == our tui_ssh object, rsi == ptr to data, rdx == length of same
	; we return false for not destroying the objects
	; we get incoming data from the ssh layer here, that we parse for ANSI escapes, etc and pass onward
	; to the rendering layer as normal keyevents
falign
tui_ssh$receive:
	prolog	tui_ssh$receive
	push	r12 r13 r14
	mov	r12, rdi
	mov	r13, rsi
	mov	r14, rdx
	add	r14, rsi
calign
.loop:
	cmp	r13, r14
	jae	.alldone
	movzx	eax, byte [r13]
	mov	ecx, eax			; save the full char in ecx
	shr	eax, 4
	cmp	eax, 12
	jae	.unichar
	; else, it isn't a unicode character, check it for escapes
calign
.notunicode:
	; our unmolested byte is sitting in ecx
	cmp	ecx, 27
	jne	.normalchar
	mov	rdx, r14			; the end of our buffer
	sub	rdx, r13			; where we are now
	cmp	rdx, 3
	jl	.normalchar
	movzx	edx, byte [r13+1]		; next char has to be [ or O
	cmp	edx, '['
	je	.escbracket
	cmp	edx, 'O'
	je	.escoh
	; else, send it out as a normal key
calign
.normalchar:
	cmp	ecx, 3				; we treat Ctrl-C as "special" and bailout
	je	.ctrlc
	mov	rdi, [r12+tui_ssh_renderer_ofs]	; our tui object
	mov	esi, ecx			; our key
	xor	edx, edx			; our esc_key
	mov	rcx, [rdi]			; its vtable
	call	qword [rcx+tui_vfirekeyevent]
	add	r13, 1
	jmp	.loop
calign
.fireescaped:
	; fire the escape key in ecx
	mov	rdi, [r12+tui_ssh_renderer_ofs]	; our tui object
	xor	esi, esi			; our key
	mov	edx, ecx			; our esc_key
	mov	rcx, [rdi]			; its vtable
	call	qword [rcx+tui_vfirekeyevent]
	add	r13, 1
	jmp	.loop
calign
.escbracket:
	; ESC [ received
	add	r13, 2				; skip ESC [
	movzx	ecx, byte [r13]
	cmp	ecx, 'A'
	je	.fireescaped
	cmp	ecx, 'B'
	je	.fireescaped
	cmp	ecx, 'C'
	je	.fireescaped
	cmp	edx, 'D'
	je	.fireescaped
	; otherwise, go forward til we run out of room, or til we hit a ~, shl by 8 each one we get
	xor	eax, eax
calign
.escloop:
	cmp	r13, r14
	jae	.escloopdone
	movzx	ecx, byte [r13]
	cmp	ecx, '~'
	je	.escloopdone
	shl	eax, 8
	or	eax, ecx
	add	r13, 1
	jmp	.escloop
calign
.escloopdone:
	mov	ecx, eax
	jmp	.fireescaped			; this could make r13 go one past the end, quite alright
calign
.escoh:
	; ESC O received
	shl	ecx, 8				; 'O' << 8
	movzx	eax, byte [r13+1]
	or	ecx, eax			; | next byte
	add	r13, 2
	jmp	.fireescaped
calign
.unichar:
	cmp	eax, 12
	je	.unichar_2
	cmp	eax, 13
	je	.unichar_2
	cmp	eax, 14
	je	.unichar_3
	cmp	eax, 15
	je	.unichar_4
	; anything else is invalid unicode, no?
	; so we can let the "normal" non-unicode parser see what it makes of it
	jmp	.notunicode
calign
.unichar_2:
	mov	rdx, r14			; the end of our buffer
	sub	rdx, r13			; where we are now
	cmp	rdx, 2
	jl	.alldone			; kakked if we ran to the end (shouldn't happen in normal circumstances)
	movzx	edx, byte [r13+1]		; get the next char
	mov	r8d, edx			; save it
	and	edx, 0xc0
	cmp	edx, 0x80
	jne	.notunicode
	mov	eax, ecx		; first byte
	shl	eax, 6
	and	r8d, 0x3f
	and	eax, 0x7c0
	or	eax, r8d
	cmp	eax, 0x80
	jb	.notunicode
	; otherwise, unicode point in eax is valid, fire it
	mov	rdi, [r12+tui_ssh_renderer_ofs]	; our tui object
	mov	esi, eax		; our key
	xor	edx, edx		; our esc_key
	mov	rcx, [rdi]		; its vtable
	call	qword [rcx+tui_vfirekeyevent]
	; swallow two characters and keep going
	add	r13, 2
	jmp	.loop
calign
.unichar_3:
	mov	rdx, r14		; the end of our buffer
	sub	rdx, r13		; where we are now
	cmp	rdx, 3
	jl	.alldone		; kakked if we ran to the end (shouldn't happen in normal circumstances)
	movzx	edx, byte [r13+1]	; get the next char
	movzx	r9d, byte [r13+2]	; and the next char
	mov	r10d, edx		; save
	mov	r11d, r9d		; save
	and	edx, 0xc0
	cmp	edx, 0x80
	jne	.notunicode
	and	r9d, 0xc0
	cmp	r9d, 0x80
	jne	.notunicode
	mov	r8d, ecx
	shl	r8d, 12
	and	r8d, 0xf000
	shl	r10d, 6
	and	r10d, 0xfc0
	or	r8d, r10d
	and	r11d, 0x3f
	or	r8d, r11d
	cmp	r8d, 0x800
	jb	.notunicode
	; otherwise, unicode point in r8d is valid, fire it
	mov	rdi, [r12+tui_ssh_renderer_ofs]	; our tui object
	mov	esi, r8d		; our key
	xor	edx, edx		; our esc_key
	mov	rcx, [rdi]		; its vtable
	call	qword [rcx+tui_vfirekeyevent]
	; swallow three characters and keep going
	add	r13, 3
	jmp	.loop
calign
.unichar_4:
	mov	rdx, r14		; the end of our buffer
	sub	rdx, r13		; where we are now
	cmp	rdx, 4
	jl	.alldone		; kakked if we ran to the end (shouldn't happen in normal circumstances)
	; ecx is our unmolested first byte
	movzx	eax, byte [r13+1]	; second byte
	movzx	edx, byte [r13+2]	; third byte
	movzx	r8d, byte [r13+3]	; fourth byte
	mov	r9d, eax
	and	r9d, 0xc0
	cmp	r9d, 0x80
	jne	.notunicode
	mov	r9d, edx
	and	r9d, 0xc0
	cmp	r9d, 0x80
	jne	.notunicode
	mov	r9d, r8d
	and	r9d, 0xc0
	cmp	r9d, 0x80
	jne	.notunicode
	mov	r9d, ecx
	shl	r9d, 18
	and	r9d, 0x1c0000
	shl	eax, 12
	and	eax, 0x3f000
	or	r9d, eax
	shl	edx, 6
	and	edx, 0xfc0
	or	r9d, edx
	and	r8d, 0x3f
	or	r9d, r8d
	cmp	r9d, 0x10000
	jb	.notunicode
	; otherwise, unicode point in r9d is valid, fire it
	mov	rdi, [r12+tui_ssh_renderer_ofs]	; our tui object
	mov	esi, r9d		; our key
	xor	edx, edx		; our esc_key
	mov	rcx, [rdi]		; its vtable
	call	qword [rcx+tui_vfirekeyevent]
	; swallow four characters and keep going
	add	r13, 4
	jmp	.loop
calign
.alldone:
	pop	r14 r13 r12
	xor	eax, eax		; don't kill us off
	epilog
calign
.ctrlc:
	; send out ansi to do screen restore before we off ourselves
	mov	rdi, [r12+tui_ssh_renderer_ofs]	; our tui object
	mov	edi, [rdi+tui_width_ofs]	; its height
	mov	esi, 10
	call	string$from_unsigned
	mov	r13, rax
	sub	rsp, 128
	mov	rdi, rsp
	mov	rsi, .seeyalater
	mov	edx, .seeyalater_size
	call	memcpy
	mov	rdi, r13
	lea	rsi, [rsp+.seeyalater_size]
	call	string$to_utf8
	mov	r14d, eax			; how many bytes it wrote
	lea	rdi, [rsp+rax+.seeyalater_size]
	mov	rsi, .reallyseeya
	mov	edx, .reallyseeya_size
	call	memcpy
	mov	rdi, r12
	mov	rsi, rsp
	mov	edx, r14d
	add	edx, .seeyalater_size + .reallyseeya_size
	mov	rcx, [rdi]
	call	qword [rcx+io_vsend]
	mov	rdi, r13
	call	heap$free
	add	rsp, 128

	; so now, hopefully that went down the wire, ideally we need to let the ssh object know that it wants a cleanexit, and well-behaved clients
	; will exit as soon as we send the goods
	mov	rdi, [r12+io_child_ofs]		; better be our ssh object
	call	ssh$cleanexit

	pop	r14 r13 r12
	mov	eax, 1			; suicide
	epilog
if tui_ssh_alternatescreen

dalign
.seeyalater:
        db      27,'[r',27,'[?25h',27,'['
        .seeyalater_size = $ - .seeyalater
dalign
.reallyseeya:
        db      ';1H',27,'[?1049l!!Ctrl-C!!',13,10
        .reallyseeya_size = $ - .reallyseeya

else

dalign
.seeyalater:
        db      27,'[r',27,'[?25h',27,'['
        .seeyalater_size = $ - .seeyalater
dalign
.reallyseeya:
        db      ';1H!!Ctrl-C!!',13,10
        .reallyseeya_size = $ - .reallyseeya

end if

end if


