honk/admin.go

300 lines
5.9 KiB
Go

//
// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package main
/*
#include <termios.h>
void
clearecho(struct termios *tio)
{
tio->c_lflag = tio->c_lflag & ~(ECHO|ICANON);
}
*/
import "C"
import (
"bufio"
"fmt"
"os"
"os/signal"
"strings"
"humungus.tedunangst.com/r/webs/log"
)
func adminscreen() {
log.Init(log.Options{Progname: "honk", Alllogname: "null"})
stdout := bufio.NewWriter(os.Stdout)
esc := "\x1b"
smcup := esc + "[?1049h"
rmcup := esc + "[?1049l"
var avatarColors string
getconfig("avatarcolors", &avatarColors)
loadLingo()
type adminfield struct {
name string
label string
text string
oneline bool
}
messages := []*adminfield{
{
name: "servermsg",
label: "server",
text: string(serverMsg),
},
{
name: "aboutmsg",
label: "about",
text: string(aboutMsg),
},
{
name: "loginmsg",
label: "login",
text: string(loginMsg),
},
{
name: "avatarcolors",
label: "avatar colors (4 RGBA hex numbers)",
text: string(avatarColors),
oneline: true,
},
}
for _, l := range []string{"honked", "bonked", "honked back", "qonked", "evented"} {
messages = append(messages, &adminfield{
name: "lingo-" + strings.ReplaceAll(l, " ", ""),
label: "lingo for " + l,
text: relingo[l],
oneline: true,
})
}
cursel := 0
hidecursor := func() {
stdout.WriteString(esc + "[?25l")
}
showcursor := func() {
stdout.WriteString(esc + "[?12;25h")
}
movecursor := func(x, y int) {
stdout.WriteString(fmt.Sprintf(esc+"[%d;%dH", y, x))
}
moveleft := func() {
stdout.WriteString(esc + "[1D")
}
clearscreen := func() {
stdout.WriteString(esc + "[2J")
}
//clearline := func() { stdout.WriteString(esc + "[2K") }
colorfn := func(code int) func(string) string {
return func(s string) string {
return fmt.Sprintf(esc+"[%dm"+"%s"+esc+"[0m", code, s)
}
}
reverse := colorfn(7)
magenta := colorfn(35)
readchar := func() byte {
var buf [1]byte
os.Stdin.Read(buf[:])
c := buf[0]
return c
}
savedtio := new(C.struct_termios)
C.tcgetattr(1, savedtio)
restore := func() {
stdout.WriteString(rmcup)
showcursor()
stdout.Flush()
C.tcsetattr(1, C.TCSAFLUSH, savedtio)
}
defer restore()
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
restore()
os.Exit(0)
}()
init := func() {
tio := new(C.struct_termios)
C.tcgetattr(1, tio)
C.clearecho(tio)
C.tcsetattr(1, C.TCSADRAIN, tio)
hidecursor()
stdout.WriteString(smcup)
clearscreen()
movecursor(1, 1)
stdout.Flush()
}
editing := false
linecount := func(s string) int {
lines := 1
for i := range s {
if s[i] == '\n' {
lines++
}
}
return lines
}
msglineno := func(idx int) int {
off := 1
if idx == -1 {
return off
}
for i, m := range messages {
off += 1
if i == idx {
return off
}
if !m.oneline {
off += 1
off += linecount(m.text)
}
}
off += 2
return off
}
forscreen := func(s string) string {
return strings.Replace(s, "\n", "\n ", -1)
}
drawmessage := func(idx int) {
line := msglineno(idx)
movecursor(4, line)
label := messages[idx].label
if idx == cursel {
label = reverse(label)
}
label = magenta(label)
text := forscreen(messages[idx].text)
if messages[idx].oneline {
stdout.WriteString(fmt.Sprintf("%s\t %s", label, text))
} else {
stdout.WriteString(fmt.Sprintf("%s\n %s", label, text))
}
}
drawscreen := func() {
clearscreen()
movecursor(4, msglineno(-1))
stdout.WriteString(magenta(serverName + " admin panel"))
for i := range messages {
if !editing || i != cursel {
drawmessage(i)
}
}
movecursor(4, msglineno(len(messages)))
dir := "j/k to move - q to quit - enter to edit"
if editing {
dir = "esc to end"
}
stdout.WriteString(magenta(dir))
if editing {
drawmessage(cursel)
}
stdout.Flush()
}
selectnext := func() {
if cursel < len(messages)-1 {
movecursor(4, msglineno(cursel))
stdout.WriteString(magenta(messages[cursel].label))
cursel++
movecursor(4, msglineno(cursel))
stdout.WriteString(reverse(magenta(messages[cursel].label)))
stdout.Flush()
}
}
selectprev := func() {
if cursel > 0 {
movecursor(4, msglineno(cursel))
stdout.WriteString(magenta(messages[cursel].label))
cursel--
movecursor(4, msglineno(cursel))
stdout.WriteString(reverse(magenta(messages[cursel].label)))
stdout.Flush()
}
}
editsel := func() {
editing = true
showcursor()
drawscreen()
m := messages[cursel]
loop:
for {
c := readchar()
switch c {
case '\x1b':
break loop
case '\n':
if m.oneline {
break loop
}
m.text += "\n"
drawscreen()
case 127:
if len(m.text) > 0 {
last := m.text[len(m.text)-1]
m.text = m.text[:len(m.text)-1]
if last == '\n' {
drawscreen()
} else {
moveleft()
stdout.WriteString(" ")
moveleft()
}
}
default:
m.text += string(c)
stdout.WriteString(string(c))
}
stdout.Flush()
}
editing = false
setconfig(m.name, m.text)
hidecursor()
drawscreen()
}
init()
drawscreen()
for {
c := readchar()
switch c {
case 'q':
return
case 'j':
selectnext()
case 'k':
selectprev()
case '\n':
editsel()
default:
}
}
}