init
This commit is contained in:
commit
d382b0526d
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.ssh
|
||||
sshww
|
||||
warm-welcome
|
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -0,0 +1,15 @@
|
||||
ssh-warm-welcome: warm welcome pages over SSH
|
||||
Copyright (C) 2022 decentral1se <d34ae261d3@sinenomine.email>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
67
README.md
Normal file
67
README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# ssh-warm-welcome
|
||||
|
||||
<img align="right" width="150" src="./logo.png"/>
|
||||
|
||||
SSH as a publishing interface :tada:
|
||||
|
||||
`ssh-warm-welcome` serves a [TUI] for reading markdown documents via a built-in
|
||||
SSH server. In the context of a [pubnix] or [femserver] it can be a convenient
|
||||
way to publish documentation to a general public via the terminal instead of
|
||||
the browser. It's designed to be easy to use for beginners: no user accounts or
|
||||
SSH keys are required, simply use your standard `/usr/bin/ssh` command to
|
||||
connect & start reading.
|
||||
|
||||
Here's a little demo video: [`vvvvvvaria.org/~decentral1se/sshww/demo.mp4`](https://vvvvvvaria.org/~decentral1se/sshww/demo.mp4)
|
||||
|
||||
[pubnix]: https://tilde.town/~cmccabe/online-communities.html
|
||||
[femserver]: https://gendersec.tacticaltech.org/wiki/index.php/Servers:_From_autonomous_servers_to_feminist_servers
|
||||
[TUI]: https://en.wikipedia.org/wiki/Text-based_user_interface
|
||||
|
||||
## Quickstart
|
||||
|
||||
Download `ssh-warm-welcome` like so.
|
||||
|
||||
(only `x86_64` is available right now but we can build more if you need it)
|
||||
|
||||
```bash
|
||||
curl https://vvvvvvaria.org/~decentral1se/sshww/sshww -o sshww
|
||||
chmod +x sshww
|
||||
```
|
||||
|
||||
Write your warm welcome home page in markdown:
|
||||
|
||||
```
|
||||
mkdir -p warm-welcome
|
||||
echo "# hello, world!\n\nwelcome, welcome" > warm-welcome/welcome.md
|
||||
```
|
||||
|
||||
Then turn on the built-in SSH server:
|
||||
|
||||
```
|
||||
./sshww
|
||||
2022/04/03 00:11:39 warm welcome waiting on port :1312
|
||||
```
|
||||
|
||||
And from another terminal, connect with your SSH client:
|
||||
|
||||
```
|
||||
ssh -p 1312 localhost
|
||||
```
|
||||
|
||||
That's it!
|
||||
|
||||
## Configure
|
||||
|
||||
* `sshww` reads markdown files from a folder named `warm-welcome` in the
|
||||
current working directory. Any file it finds there will be included as a menu
|
||||
item in the warm welcome environment. `welcome.md` is the only required file,
|
||||
everything else is optional. Up to 10 markdown files are supported.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
* [`egonelbre/gophers`](https://github.com/egonelbre/gophers) for the rad logo
|
||||
* [`charm.sh`](https://github.com/charmbracelet) for new-wave command line glamour
|
||||
|
||||
## License
|
||||
|
||||
<a><img src="https://www.gnu.org/graphics/gplv3-or-later.png"/></a>
|
39
go.mod
Normal file
39
go.mod
Normal file
@ -0,0 +1,39 @@
|
||||
module decentral1se/sshww
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
github.com/charmbracelet/bubbletea v0.20.0
|
||||
github.com/charmbracelet/glamour v0.5.0
|
||||
github.com/charmbracelet/lipgloss v0.4.0
|
||||
github.com/charmbracelet/wish v0.3.0
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/keygen v0.2.1 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.17 // indirect
|
||||
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/yuin/goldmark v1.4.4 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
|
||||
)
|
99
go.sum
Normal file
99
go.sum
Normal file
@ -0,0 +1,99 @@
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
|
||||
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
|
||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
||||
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
|
||||
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
|
||||
github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
|
||||
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
|
||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/keygen v0.2.1 h1:H1yYTVe6qIDz+UILYXo6q+qLQNkvyXXA5KEhzyuEfzg=
|
||||
github.com/charmbracelet/keygen v0.2.1/go.mod h1:kFQ3Cvop12fXWX1K29vxDxV9x8ujG4wBSXq//GySSSk=
|
||||
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
|
||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||
github.com/charmbracelet/wish v0.3.0 h1:DsQ7wC5BfiQz07iOGiWB4GWJM53O4BPt3koO1Gqxw5w=
|
||||
github.com/charmbracelet/wish v0.3.0/go.mod h1:KB8u7Bp4a/akXmGVqy+R/2OhMxgNoeJ+3lDgSgBrog4=
|
||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
|
||||
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
||||
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
|
||||
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
|
||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
316
sshww.go
Normal file
316
sshww.go
Normal file
@ -0,0 +1,316 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/wish"
|
||||
bm "github.com/charmbracelet/wish/bubbletea"
|
||||
lm "github.com/charmbracelet/wish/logging"
|
||||
"github.com/gliderlabs/ssh"
|
||||
)
|
||||
|
||||
const help = `ssh-warm-welcome: warm welcome pages over SSH
|
||||
|
||||
Options:
|
||||
-p port for ssh server (default: 1312)
|
||||
-h output help
|
||||
`
|
||||
|
||||
var portFlag int
|
||||
var helpFlag bool
|
||||
|
||||
var (
|
||||
titleStyle = func() lipgloss.Style {
|
||||
b := lipgloss.RoundedBorder()
|
||||
b.Right = "├"
|
||||
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
|
||||
}()
|
||||
|
||||
selectedTitleStyle = func() lipgloss.Style {
|
||||
b := lipgloss.RoundedBorder()
|
||||
b.Right = "├"
|
||||
return lipgloss.NewStyle().Underline(true).BorderStyle(b).Padding(0, 1)
|
||||
}()
|
||||
|
||||
infoStyle = func() lipgloss.Style {
|
||||
b := lipgloss.RoundedBorder()
|
||||
b.Left = "┤"
|
||||
return titleStyle.Copy().BorderStyle(b)
|
||||
}()
|
||||
)
|
||||
|
||||
func main() {
|
||||
handleCliFlags()
|
||||
|
||||
if helpFlag {
|
||||
fmt.Printf(help)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if err := validatePages(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
s, err := wish.NewServer(
|
||||
wish.WithAddress(fmt.Sprintf("%s:%d", "0.0.0.0", portFlag)),
|
||||
wish.WithHostKeyPath(".ssh/term_info_ed25519"),
|
||||
wish.WithMiddleware(
|
||||
bm.Middleware(teaHandler),
|
||||
lm.Middleware(),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
done := make(chan os.Signal, 1)
|
||||
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
log.Printf("warm welcome waiting on port :%d", portFlag)
|
||||
go func() {
|
||||
if err = s.ListenAndServe(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
log.Println("turning off now...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer func() { cancel() }()
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||
pages, err := gatherPages()
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
model, err := initModel(pages)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to initialise model? (%s)", err.Error())
|
||||
}
|
||||
|
||||
return model, []tea.ProgramOption{tea.WithAltScreen()}
|
||||
}
|
||||
|
||||
func handleCliFlags() {
|
||||
flag.IntVar(&portFlag, "p", 1312, "port for ssh server")
|
||||
flag.BoolVar(&helpFlag, "h", false, "output help")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
type page struct {
|
||||
name string
|
||||
path string
|
||||
contents string
|
||||
rendered string
|
||||
}
|
||||
|
||||
type pages map[int]page
|
||||
|
||||
func sortByConvention(files []os.FileInfo) {
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
if files[i].Name() == "welcome.md" {
|
||||
return true // always top of the list
|
||||
}
|
||||
return files[i].Name() < files[j].Name()
|
||||
})
|
||||
}
|
||||
|
||||
func validatePages() error {
|
||||
warmWelcomeDir, err := filepath.Abs("warm-welcome")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(warmWelcomeDir); os.IsNotExist(err) {
|
||||
return errors.New("'warm-welcome' directory missing from current working directory?")
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(warmWelcomeDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list files in %s (%s)", warmWelcomeDir, err.Error())
|
||||
}
|
||||
|
||||
hasWelcome := false
|
||||
for _, file := range files {
|
||||
if file.Name() == "welcome.md" {
|
||||
hasWelcome = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasWelcome {
|
||||
return errors.New("welcome.md is missing from warm-welcome directory (required)?")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatherPages() (pages, error) {
|
||||
pages := make(map[int]page)
|
||||
|
||||
warmWelcomeDir, err := filepath.Abs("warm-welcome")
|
||||
if err != nil {
|
||||
return pages, err
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(warmWelcomeDir)
|
||||
if err != nil {
|
||||
return pages, fmt.Errorf("unable to list files in %s (%s)", warmWelcomeDir, err.Error())
|
||||
}
|
||||
|
||||
sortByConvention(files)
|
||||
|
||||
for idx, file := range files {
|
||||
filePath := filepath.Join(warmWelcomeDir, file.Name())
|
||||
contents, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return pages, fmt.Errorf("unable to read %s (%s)", filePath, err.Error())
|
||||
}
|
||||
|
||||
rendered, err := glamour.Render(string(contents), "dark")
|
||||
if err != nil {
|
||||
return pages, err
|
||||
}
|
||||
|
||||
pages[idx] = page{
|
||||
name: file.Name(),
|
||||
path: filePath,
|
||||
contents: string(contents),
|
||||
rendered: rendered,
|
||||
}
|
||||
}
|
||||
|
||||
return pages, nil
|
||||
}
|
||||
|
||||
type model struct {
|
||||
pages map[int]page
|
||||
pageIndex int
|
||||
viewport viewport.Model
|
||||
ready bool
|
||||
}
|
||||
|
||||
func initModel(pgs pages) (model, error) {
|
||||
return model{pages: pgs}, nil
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10":
|
||||
idx, err := strconv.Atoi(msg.String())
|
||||
if err == nil {
|
||||
if _, ok := m.pages[idx-1]; ok {
|
||||
m.pageIndex = idx - 1
|
||||
m.viewport.SetContent(m.pages[m.pageIndex].rendered)
|
||||
m.viewport.GotoTop()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
default:
|
||||
m.viewport.Update(msg)
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
headerHeight := lipgloss.Height(m.headerView())
|
||||
footerHeight := lipgloss.Height(m.footerView()) + lipgloss.Height(m.helpView())
|
||||
verticalMarginHeight := headerHeight + footerHeight
|
||||
|
||||
if !m.ready {
|
||||
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
|
||||
m.viewport.YPosition = headerHeight
|
||||
m.viewport.SetContent(m.pages[m.pageIndex].rendered)
|
||||
m.ready = true
|
||||
} else {
|
||||
m.viewport.Width = msg.Width
|
||||
m.viewport.Height = msg.Height - verticalMarginHeight
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
|
||||
return m, tea.Batch(cmd)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if !m.ready {
|
||||
return "\n initializing..."
|
||||
}
|
||||
|
||||
header := m.headerView()
|
||||
viewp := m.viewport.View()
|
||||
footer := m.footerView()
|
||||
|
||||
return fmt.Sprintf("%s\n%s\n%s\n%s", header, viewp, footer, m.helpView())
|
||||
}
|
||||
|
||||
func (m model) helpView() string {
|
||||
return "↑/↓: scroll pager • 1/2/3... choose page • q: quit"
|
||||
}
|
||||
|
||||
func (m model) headerView() string {
|
||||
indices := make([]int, 0, len(m.pages))
|
||||
for idx := range m.pages {
|
||||
indices = append(indices, idx)
|
||||
}
|
||||
sort.Ints(indices)
|
||||
|
||||
var pagesTotalWidth int
|
||||
var header []string
|
||||
for _, idx := range indices {
|
||||
var pageRender string
|
||||
if idx == m.pageIndex {
|
||||
pageRender = selectedTitleStyle.Render(m.pages[idx].name)
|
||||
} else {
|
||||
pageRender = titleStyle.Render(m.pages[idx].name)
|
||||
}
|
||||
|
||||
header = append(header, pageRender)
|
||||
pagesTotalWidth += lipgloss.Width(pageRender)
|
||||
}
|
||||
|
||||
line := strings.Repeat("─", max(0, m.viewport.Width-pagesTotalWidth))
|
||||
header = append(header, line)
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Center, header...)
|
||||
}
|
||||
|
||||
func (m model) footerView() string {
|
||||
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
|
||||
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
Reference in New Issue
Block a user