├── demo.gif ├── go.mod ├── LICENSE ├── README.md ├── main.go └── go.sum /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrium/tview-ssh/HEAD/demo.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/progrium/tcell-ssh 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.5.2 7 | github.com/gliderlabs/ssh v0.3.4 8 | github.com/rivo/tview v0.0.0-20220812085834-0e6b21a48e96 9 | ) 10 | 11 | require ( 12 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 13 | github.com/gdamore/encoding v1.0.0 // indirect 14 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 15 | github.com/mattn/go-runewidth v0.0.13 // indirect 16 | github.com/rivo/uniseg v0.3.4 // indirect 17 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect 18 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect 19 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect 20 | golang.org/x/text v0.3.7 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jeff Lindsay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tview-ssh 2 | Example using [tcell](https://github.com/gdamore/tcell)+[tview](https://github.com/rivo/tview) over SSH using [gliderlabs/ssh](https://github.com/gliderlabs/ssh) without allocating a PTY or creating a subprocess. 3 | 4 | ![Demo GIF](https://raw.githubusercontent.com/progrium/tview-ssh/main/demo.gif) 5 | 6 | There is a little bit of glue, but maybe not enough for a library? Plus it's probably incomplete. Here is what it looks like to make an SSH server that shows a modal when you connect: 7 | 8 | ```golang 9 | func main() { 10 | ssh.Handle(func(sess ssh.Session) { 11 | screen, err := NewSessionScreen(sess) 12 | if err != nil { 13 | fmt.Fprintln(sess.Stderr(), "unable to create screen:", err) 14 | return 15 | } 16 | 17 | // tview says we don't have to do this 18 | // when using SetScreen, but it lies 19 | if err := screen.Init(); err != nil { 20 | fmt.Fprintln(sess.Stderr(), "unable to init screen:", err) 21 | return 22 | } 23 | 24 | app := tview.NewApplication().SetScreen(screen).EnableMouse(true) 25 | 26 | modal := tview.NewModal(). 27 | SetText("Do you want to quit the application?"). 28 | AddButtons([]string{"Quit", "Cancel"}). 29 | SetDoneFunc(func(buttonIndex int, buttonLabel string) { 30 | if buttonLabel == "Quit" { 31 | app.Stop() 32 | } 33 | }) 34 | 35 | app.SetRoot(modal, false) 36 | if err := app.Run(); err != nil { 37 | fmt.Fprintln(sess.Stderr(), err) 38 | return 39 | } 40 | 41 | sess.Exit(0) 42 | }) 43 | 44 | log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa"))) 45 | } 46 | ``` 47 | 48 | If you try this, change the hostkey file to something that works for you. -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "sync" 8 | 9 | "github.com/gdamore/tcell/v2" 10 | "github.com/gdamore/tcell/v2/terminfo" 11 | "github.com/gliderlabs/ssh" 12 | "github.com/rivo/tview" 13 | ) 14 | 15 | func main() { 16 | ssh.Handle(func(sess ssh.Session) { 17 | screen, err := NewSessionScreen(sess) 18 | if err != nil { 19 | fmt.Fprintln(sess.Stderr(), "unable to create screen:", err) 20 | return 21 | } 22 | 23 | // tview says we don't have to do this 24 | // when using SetScreen, but it lies 25 | if err := screen.Init(); err != nil { 26 | fmt.Fprintln(sess.Stderr(), "unable to init screen:", err) 27 | return 28 | } 29 | 30 | app := tview.NewApplication().SetScreen(screen).EnableMouse(true) 31 | 32 | modal := tview.NewModal(). 33 | SetText("Do you want to quit the application?"). 34 | AddButtons([]string{"Quit", "Cancel"}). 35 | SetDoneFunc(func(buttonIndex int, buttonLabel string) { 36 | if buttonLabel == "Quit" { 37 | app.Stop() 38 | } 39 | }) 40 | 41 | app.SetRoot(modal, false) 42 | if err := app.Run(); err != nil { 43 | fmt.Fprintln(sess.Stderr(), err) 44 | return 45 | } 46 | 47 | sess.Exit(0) 48 | }) 49 | 50 | log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa"))) 51 | } 52 | 53 | func NewSessionScreen(s ssh.Session) (tcell.Screen, error) { 54 | pi, ch, ok := s.Pty() 55 | if !ok { 56 | return nil, errors.New("no pty requested") 57 | } 58 | ti, err := terminfo.LookupTerminfo(pi.Term) 59 | if err != nil { 60 | return nil, err 61 | } 62 | screen, err := tcell.NewTerminfoScreenFromTtyTerminfo(&tty{ 63 | Session: s, 64 | size: pi.Window, 65 | ch: ch, 66 | }, ti) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return screen, nil 71 | } 72 | 73 | type tty struct { 74 | ssh.Session 75 | size ssh.Window 76 | ch <-chan ssh.Window 77 | resizecb func() 78 | mu sync.Mutex 79 | } 80 | 81 | func (t *tty) Start() error { 82 | go func() { 83 | for win := range t.ch { 84 | t.size = win 85 | t.notifyResize() 86 | } 87 | }() 88 | return nil 89 | } 90 | 91 | func (t *tty) Stop() error { 92 | return nil 93 | } 94 | 95 | func (t *tty) Drain() error { 96 | return nil 97 | } 98 | 99 | func (t *tty) WindowSize() (width int, height int, err error) { 100 | return t.size.Width, t.size.Height, nil 101 | } 102 | 103 | func (t *tty) NotifyResize(cb func()) { 104 | t.mu.Lock() 105 | defer t.mu.Unlock() 106 | t.resizecb = cb 107 | } 108 | 109 | func (t *tty) notifyResize() { 110 | t.mu.Lock() 111 | defer t.mu.Unlock() 112 | if t.resizecb != nil { 113 | t.resizecb() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 2 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 3 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 4 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 5 | github.com/gdamore/tcell/v2 v2.5.2 h1:tKzG29kO9p2V++3oBY2W9zUjYu7IK1MENFeY/BzJSVY= 6 | github.com/gdamore/tcell/v2 v2.5.2/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= 7 | github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw= 8 | github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= 9 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 10 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 11 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 12 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 13 | github.com/rivo/tview v0.0.0-20220812085834-0e6b21a48e96 h1:O435d1KIgG6KxpP7NDdmj7SdaLIzq4F+PG8ZB/BHC4c= 14 | github.com/rivo/tview v0.0.0-20220812085834-0e6b21a48e96/go.mod h1:hyzpnqn4KWzZopTEjL1AxvlzOLMH1IuKo4lTw6vyOQc= 15 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 16 | github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= 17 | github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 18 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= 19 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 20 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 21 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= 25 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 27 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 28 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= 29 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 30 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 31 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 32 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 33 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 34 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 35 | --------------------------------------------------------------------------------