From 108bc44411162b0f5625f33b95d50a6d05f496ff Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Wed, 24 Apr 2024 19:09:17 -0600 Subject: [PATCH] Making client GUI with fake data --- cmd/client/main.go | 369 ++++++++++++++++++++++++++++++++++++--------- go-ncurses | 2 +- server.go | 16 +- 3 files changed, 307 insertions(+), 80 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 040f2ca..5d82062 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,26 +1,26 @@ package main import ( - "crypto/ed25519" - "crypto/rand" - "crypto/x509" - "encoding/binary" - "encoding/pem" - "flag" - "fmt" - "os" - "os/signal" - "syscall" - "time" - - "git.metznet.ca/MetzNet/go-ncurses" - "git.metznet.ca/MetzNet/pnyx" - "github.com/gen2brain/malgo" - "github.com/google/uuid" - "github.com/hraban/opus" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/binary" + "encoding/pem" + "flag" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "git.metznet.ca/MetzNet/go-ncurses" + "git.metznet.ca/MetzNet/pnyx" + "github.com/gen2brain/malgo" + "github.com/google/uuid" + "github.com/hraban/opus" ) -var decoders = map[pnyx.PeerID]chan[]byte{} +var decoders = map[pnyx.PeerID]chan []byte{} var encoder *opus.Encoder var sample_rate int = 0 @@ -33,18 +33,18 @@ func set_sample_rate(audio_data chan []int16, new_sample_rate int) error { return err } - for peer_id, decoder_chan := range(decoders) { + for peer_id, decoder_chan := range decoders { if decoder_chan != nil { decoder_chan <- nil } - new_chan := make(chan[]byte, 1000) + new_chan := make(chan []byte, 1000) decoders[peer_id] = new_chan go handle_peer_decode(audio_data, peer_id, decoders[peer_id], sample_rate) } return nil } -func handle_peer_decode(audio_data chan []int16, peer_id pnyx.PeerID, decode_chan chan[]byte, sample_rate int){ +func handle_peer_decode(audio_data chan []int16, peer_id pnyx.PeerID, decode_chan chan []byte, sample_rate int) { decoder, err := opus.NewDecoder(sample_rate, 1) if err != nil { panic(err) @@ -54,11 +54,11 @@ func handle_peer_decode(audio_data chan []int16, peer_id pnyx.PeerID, decode_cha missed := 0 for running { select { - case <-time.After(20*time.Millisecond): + case <-time.After(20 * time.Millisecond): missed += 1 if missed > 100 { - decode_chan <- <- decode_chan + decode_chan <- <-decode_chan } pcm := make([]int16, sample_rate/50) @@ -88,11 +88,11 @@ func mixer(data_chan chan []int16, speaker_chan chan []int16) { var samples []int16 = nil for true { if samples == nil { - samples = <- data_chan + samples = <-data_chan } else { select { - case new_samples := <- data_chan: - for i, sample := range(new_samples) { + case new_samples := <-data_chan: + for i, sample := range new_samples { samples[i] += sample } case speaker_chan <- samples: @@ -108,13 +108,13 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext panic(err) } - infos, err := ctx.Devices(malgo.Playback) - if err != nil { + infos, err := ctx.Devices(malgo.Playback) + if err != nil { panic(err) - } + } var playback_device *malgo.DeviceInfo = nil - for _, info := range infos { + for _, info := range infos { if info.IsDefault != 0 { full, err := ctx.DeviceInfo(malgo.Playback, info.ID, malgo.Shared) if err != nil { @@ -122,19 +122,19 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext } playback_device = &full } - } + } if playback_device == nil { panic("No default playback device") } - infos, err = ctx.Devices(malgo.Capture) - if err != nil { + infos, err = ctx.Devices(malgo.Capture) + if err != nil { panic(err) - } + } var capture_device *malgo.DeviceInfo = nil - for _, info := range infos { + for _, info := range infos { if info.IsDefault != 0 { full, err := ctx.DeviceInfo(malgo.Capture, info.ID, malgo.Shared) if err != nil { @@ -142,33 +142,33 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext } capture_device = &full } - } + } if capture_device == nil { panic("No default capture device") } inDeviceConfig := malgo.DefaultDeviceConfig(malgo.Capture) - inDeviceConfig.Capture.Format = malgo.FormatS16 - inDeviceConfig.Capture.Channels = 1 + inDeviceConfig.Capture.Format = malgo.FormatS16 + inDeviceConfig.Capture.Channels = 1 inDeviceConfig.Capture.DeviceID = capture_device.ID.Pointer() - inDeviceConfig.SampleRate = 48000 + inDeviceConfig.SampleRate = 48000 inDeviceConfig.PeriodSizeInFrames = 960 - inDeviceConfig.Alsa.NoMMap = 1 + inDeviceConfig.Alsa.NoMMap = 1 inDeviceConfig.Capture.ShareMode = malgo.Shared outDeviceConfig := malgo.DefaultDeviceConfig(malgo.Playback) - outDeviceConfig.Playback.Format = malgo.FormatS16 - outDeviceConfig.Playback.Channels = 1 + outDeviceConfig.Playback.Format = malgo.FormatS16 + outDeviceConfig.Playback.Channels = 1 outDeviceConfig.Playback.DeviceID = playback_device.ID.Pointer() - outDeviceConfig.SampleRate = 48000 + outDeviceConfig.SampleRate = 48000 outDeviceConfig.PeriodSizeInFrames = 960 - outDeviceConfig.Alsa.NoMMap = 1 + outDeviceConfig.Alsa.NoMMap = 1 outDeviceConfig.Playback.ShareMode = malgo.Shared onSendFrames := func(output_samples []byte, input_samples []byte, framecount uint32) { select { - case pcm := <- speaker: + case pcm := <-speaker: for i := 0; i < sample_rate/50; i++ { binary.LittleEndian.PutUint16(output_samples[i*2:], uint16(pcm[i])) } @@ -176,9 +176,9 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext } } - playbackCallbacks := malgo.DeviceCallbacks{ - Data: onSendFrames, - } + playbackCallbacks := malgo.DeviceCallbacks{ + Data: onSendFrames, + } outDevice, err := malgo.InitDevice(ctx.Context, outDeviceConfig, playbackCallbacks) if err != nil { @@ -190,7 +190,7 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext panic(err) } - onRecvFrames := func(output_samples []byte, input_samples []byte, framecount uint32) { + onRecvFrames := func(output_samples []byte, input_samples []byte, framecount uint32) { if encoder != nil { pcm := make([]int16, len(input_samples)/2) for i := 0; i < len(input_samples)/2; i++ { @@ -207,21 +207,21 @@ func setup_audio(mic chan []byte, speaker chan []int16) (*malgo.AllocatedContext default: } } - } + } - captureCallbacks := malgo.DeviceCallbacks{ - Data: onRecvFrames, - } + captureCallbacks := malgo.DeviceCallbacks{ + Data: onRecvFrames, + } - inDevice, err := malgo.InitDevice(ctx.Context, inDeviceConfig, captureCallbacks) - if err != nil { + inDevice, err := malgo.InitDevice(ctx.Context, inDeviceConfig, captureCallbacks) + if err != nil { panic(err) - } + } - err = inDevice.Start() - if err != nil { + err = inDevice.Start() + if err != nil { panic(err) - } + } return ctx, outDevice, inDevice } @@ -257,7 +257,7 @@ func get_private_key(path string, generate bool) ed25519.PrivateKey { } key_pem := pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", + Type: "PRIVATE KEY", Bytes: key_pkcs8, }) @@ -271,21 +271,21 @@ func get_private_key(path string, generate bool) ed25519.PrivateKey { } } -func print_decoration(window, title, channels, body ncurses.Window, top_left string) { +func print_decoration(window, title, channels, body ncurses.Window, top_left string) { max_y := ncurses.GetMaxY(*ncurses.CurScr) max_x := ncurses.GetMaxX(*ncurses.CurScr) ncurses.WResize(window, max_y, max_x) ncurses.WResize(title, 1, max_x/4-2) ncurses.MvWin(title, 1, 1) - ncurses.WResize(channels, max_y - 4, (max_x / 4) - 2) + ncurses.WResize(channels, max_y-4, (max_x/4)-1) ncurses.MvWin(channels, 3, 1) - ncurses.WResize(body, max_y - 2, max_x - (max_x / 4) - 2) - ncurses.MvWin(body, 1, (max_x / 4) + 1) + ncurses.WResize(body, max_y-2, max_x-(max_x/4)-2) + ncurses.MvWin(body, 1, (max_x/4)+1) ncurses.MvWAddStr(title, 0, 0, top_left) - for i := 1; i < max_x - 1; i++ { + for i := 1; i < max_x-1; i++ { ncurses.MvWAddStr(window, 0, i, "═") } @@ -306,7 +306,7 @@ func print_decoration(window, title, channels, body ncurses.Window, top_left str } for i := 1; i < max_x-1; i++ { - ncurses.MvWAddStr(window, max_y - 1, i, "═") + ncurses.MvWAddStr(window, max_y-1, i, "═") } ncurses.MvWAddStr(window, 0, 0, "╔") @@ -324,12 +324,231 @@ func print_decoration(window, title, channels, body ncurses.Window, top_left str ncurses.WRefresh(body) } +type ChannelState struct { + ID pnyx.ChannelID + Name string + Members []pnyx.PeerID + Modes []pnyx.ModeID +} + +type PeerState struct { + Name string +} + +type UIState struct { + SelectedBody int + SelectedPeer int + SelectedChannel int + SelectedArea int + Channels []ChannelState + Peers map[pnyx.PeerID]PeerState +} + +const ( + AREA_CHANNELS = 0 + AREA_BODY = 1 + + BODY_CHANNEL = 0 + BODY_DETAIL = 1 + BODY_QUIT = 2 +) + +func handle_back(state *UIState) { + if state.SelectedArea == AREA_BODY { + if state.SelectedBody == BODY_QUIT { + state.SelectedBody = BODY_CHANNEL + } else if state.SelectedBody == BODY_DETAIL { + state.SelectedBody = BODY_CHANNEL + } + state.SelectedArea = AREA_CHANNELS + } +} + +func handle_up(state *UIState) { + if state.SelectedArea == AREA_CHANNELS { + if state.SelectedPeer > 0 { + state.SelectedPeer -= 1 + } else if state.SelectedChannel > 0 { + state.SelectedChannel -= 1 + state.SelectedPeer = len(state.Channels[state.SelectedChannel].Members) + } + } +} + +func handle_down(state *UIState) { + if state.SelectedArea == AREA_CHANNELS { + if state.SelectedPeer < len(state.Channels[state.SelectedChannel].Members) { + state.SelectedPeer += 1 + } else if state.SelectedChannel < len(state.Channels) - 1 { + state.SelectedChannel += 1 + state.SelectedPeer = 0 + } + } +} + +func handle_select(state *UIState) { + if state.SelectedArea == AREA_CHANNELS { + state.SelectedArea = AREA_BODY + state.SelectedBody = BODY_CHANNEL + } +} + +func handle_edit(state *UIState) { + if state.SelectedArea == AREA_CHANNELS { + state.SelectedArea = AREA_BODY + state.SelectedBody = BODY_DETAIL + } +} + +func handle_quit(state *UIState) { + state.SelectedArea = AREA_BODY + state.SelectedBody = BODY_QUIT +} + +func handle_input(state *UIState, char []byte) { + switch char[0] { + case 'q': + handle_quit(state) + case 'e': + handle_edit(state) + case '\r': + handle_select(state) + case 0x1B: + if len(char) == 1 { + handle_back(state) + } else { + switch char[1] { + case '[': + if len(char) == 2 { + } else { + switch char[2] { + case 'A': // Up + handle_up(state) + case 'B': // Down + handle_down(state) + case 'C': // Right + handle_select(state) + case 'D': // Left + handle_back(state) + } + } + } + } + } +} + +func draw_channels(state *UIState, channels ncurses.Window) { + channel_highlight_color := 2 + if state.SelectedArea == AREA_CHANNELS { + channel_highlight_color = 1 + } + + ncurses.WErase(channels) + + ncurses.WMove(channels, 0, 0) + for i, channel_state := range(state.Channels) { + if i == state.SelectedChannel && state.SelectedPeer == 0 { + ncurses.WAttrOn(channels, ncurses.COLOR_PAIR(channel_highlight_color)) + } + + ncurses.WAddStr(channels, channel_state.Name) + ncurses.WAddCh(channels, '\n') + for j, peer_id := range(channel_state.Members) { + ncurses.WAddStr(channels, " ↳") + peer_state, found := state.Peers[peer_id] + if state.SelectedChannel == i && state.SelectedPeer == (j+1) { + ncurses.WAttrOn(channels, ncurses.COLOR_PAIR(channel_highlight_color)) + } + if found { + ncurses.WAddStr(channels, peer_state.Name) + } else { + ncurses.WAddStr(channels, fmt.Sprintf("%s", peer_id)) + } + if state.SelectedChannel == i && state.SelectedPeer == (j+1) { + ncurses.WAttrOff(channels, ncurses.COLOR_PAIR(channel_highlight_color)) + } + ncurses.WAddCh(channels, '\n') + } + + if i == state.SelectedChannel && state.SelectedPeer == 0 { + ncurses.WAttrOff(channels, ncurses.COLOR_PAIR(channel_highlight_color)) + } + } + + ncurses.WRefresh(channels) +} + +func draw_body_channel(state *UIState, body ncurses.Window) { + ncurses.WErase(body) + ncurses.MvWAddStr(body, 0, 0, "channel") + ncurses.WRefresh(body) +} + +func draw_body_detail(state *UIState, body ncurses.Window) { + ncurses.WErase(body) + ncurses.MvWAddStr(body, 0, 0, "detail") + ncurses.WRefresh(body) +} + +func draw_body_quit(state *UIState, body ncurses.Window) { + ncurses.WErase(body) + ncurses.MvWAddStr(body, 0, 0, "quit") + ncurses.WRefresh(body) +} + +func draw_gui(state *UIState, channels, body ncurses.Window) { + draw_channels(state, channels) + switch state.SelectedBody { + case BODY_CHANNEL: + draw_body_channel(state, body) + case BODY_DETAIL: + draw_body_detail(state, body) + case BODY_QUIT: + draw_body_quit(state, body) + } +} + func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Window, packet_chan chan pnyx.Payload, user_chan chan []byte, sigwinch_channel chan os.Signal) { channels := ncurses.NewWin(0, 0, 0, 0) body := ncurses.NewWin(0, 0, 0, 0) title := ncurses.NewWin(0, 0, 0, 0) + state := &UIState{ + Peers: map[pnyx.PeerID]PeerState{ + [32]byte{}: { + Name: "Test User 0", + }, + [32]byte{0x01}: { + Name: "Test User 1", + }, + [32]byte{0x02}: { + Name: "Test User 2", + }, + }, + Channels: []ChannelState{ + { + ID: 0, + Name: "Channel 0", + Members: []pnyx.PeerID{[32]byte{}, [32]byte{0x01}}, + }, + { + ID: 1, + Name: "Channel 1", + Members: []pnyx.PeerID{[32]byte{0x02}}, + }, + }, + } + + ncurses.InitColor(1, 1000, 1000, 1000) + ncurses.InitColor(2, 300, 300, 300) + ncurses.InitColor(3, 150, 150, 150) + ncurses.InitColor(4, 150, 150, 300) + ncurses.InitPair(1, 1, 2) + ncurses.InitPair(2, 1, 3) + ncurses.InitPair(3, 1, 4) + ncurses.ScrollOk(body, true) print_decoration(window, title, channels, body, client.Remote()) + draw_gui(state, channels, body) for client.Active() { select { @@ -338,6 +557,7 @@ func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Wind ncurses.WRefresh(window) ncurses.WClear(window) print_decoration(window, title, channels, body, client.Remote()) + draw_gui(state, channels, body) case packet := <-packet_chan: switch packet := packet.(type) { case pnyx.ChannelCommandPacket: @@ -366,7 +586,7 @@ func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Wind decode_chan, exists := decoders[packet.Peer] if exists == false { if sample_rate != 0 { - decode_chan = make(chan[]byte, 1000) + decode_chan = make(chan []byte, 1000) decoders[packet.Peer] = decode_chan go handle_peer_decode(audio_data, packet.Peer, decoders[packet.Peer], sample_rate) decode_chan <- packet.Data @@ -379,15 +599,15 @@ func main_loop(client *pnyx.Client, audio_data chan []int16, window ncurses.Wind } } case char := <-user_chan: - ncurses.WAddStr(body, string(char)) - ncurses.WRefresh(body) + handle_input(state, char) + draw_gui(state, channels, body) } } } func process_mic(client *pnyx.Client, mic chan []byte) { for true { - data := <- mic + data := <-mic err := client.Send(pnyx.NewDataPacket(pnyx.ChannelID(0), pnyx.MODE_AUDIO, data)) if err != nil { panic(err) @@ -415,7 +635,6 @@ func main() { defer inDevice.Uninit() defer inDevice.Stop() - user_chan := make(chan []byte, 1024) packet_chan := make(chan pnyx.Payload, 1024) @@ -431,7 +650,7 @@ func main() { if err != nil { panic(err) } - + join_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_CHANNEL, pnyx.CHANNEL_COMMAND_JOIN, nil) err = client.Send(join_packet) if err != nil { @@ -446,11 +665,11 @@ func main() { go process_mic(client, mic) - locale := ncurses.SetLocale(0, "") - fmt.Printf("locale: %s\n", locale) - + ncurses.SetLocale(0, "") + user_chan, stdin_active := ncurses.UTF8Listener(100, os.Stdin) window := ncurses.InitScr() + ncurses.CursSet(0) ret := ncurses.StartColor() if ret != 0 { panic(ret) diff --git a/go-ncurses b/go-ncurses index 6b121ac..14b1c31 160000 --- a/go-ncurses +++ b/go-ncurses @@ -1 +1 @@ -Subproject commit 6b121ac061f1182cf5adf8d540cf3d5d6aae8c0b +Subproject commit 14b1c310e749554c5e290108e50190cbe71b2d23 diff --git a/server.go b/server.go index f196838..161c370 100644 --- a/server.go +++ b/server.go @@ -36,11 +36,16 @@ func(session *ServerSession) Send(payload Payload) { } } +type SessionPayload struct { + Session *ServerSession + Payload Payload +} + type Server struct { key ed25519.PrivateKey active atomic.Bool connection *net.UDPConn - commands chan Payload + commands chan SessionPayload threads sync.WaitGroup @@ -80,7 +85,7 @@ func NewServer(listen string, key ed25519.PrivateKey, channels map[ChannelID]*Ch key: key, connection: connection, active: atomic.Bool{}, - commands: make(chan Payload, SERVER_COMMAND_BUFFER_SIZE), + commands: make(chan SessionPayload, SERVER_COMMAND_BUFFER_SIZE), sessions: map[SessionID]*ServerSession{}, channels: atomic.Value{}, @@ -198,7 +203,10 @@ func handle_session_incoming(session *ServerSession, server *Server) { switch packet := packet.(type) { case CommandPacket: - server.commands<-packet + server.commands<-SessionPayload{ + Session: session, + Payload: packet, + } case ChannelCommandPacket: channels := server.channels.Load().(map[ChannelID]*Channel) channel, exists := channels[packet.Channel] @@ -380,7 +388,7 @@ func(server *Server) update_state() { for server.active.Load() { select { case command := <-server.commands: - if command == nil { + if command.Payload == nil { break } server.Log("Incoming server command %+v", command)