Skip to content

Commit 91376b0

Browse files
authored
Initial support for FidoNet NodeList (#215)
* Added initial Nodelist support for new messages To address field * Updated Nodelist table style * Removed ZIPed nodelist from the testdata * Don't fail on Nodelist errors * Imporved header fields switching flow * Removed used of depricated ioutil from nodelist reader
1 parent 33303bc commit 91376b0

File tree

8 files changed

+2349
-3
lines changed

8 files changed

+2349
-3
lines changed

gossiped.example.yml

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ sorting:
2424
statusbar:
2525
clock: true
2626
citypath: ./city.yml
27+
nodelistpath: ''

pkg/config/config.go

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/askovpen/gossiped/pkg/types"
12+
"github.com/askovpen/gossiped/pkg/nodelist"
1213
"github.com/gdamore/tcell/v2"
1314
"gopkg.in/yaml.v3"
1415
)
@@ -45,6 +46,7 @@ type (
4546
Sorting SortTypeMap
4647
Colors map[string]ColorMap
4748
CityPath string
49+
NodelistPath string
4850
}
4951
)
5052

@@ -110,6 +112,8 @@ func Read(fn string) error {
110112
}
111113
Config.CityPath = tryPath(rootPath, Config.CityPath)
112114
err = readCity()
115+
Config.NodelistPath = tryPath(rootPath, Config.NodelistPath)
116+
nodelist.Read(Config.NodelistPath)
113117
if err != nil {
114118
return err
115119
}

pkg/nodelist/nodelist.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package nodelist
2+
3+
import (
4+
"github.com/askovpen/gossiped/pkg/types"
5+
"bufio"
6+
"io"
7+
"os"
8+
"regexp"
9+
"strings"
10+
)
11+
12+
type (
13+
nodelineS struct {
14+
Address types.FidoAddr
15+
BBS string
16+
City string
17+
Sysop string
18+
}
19+
)
20+
21+
// Nodelist contains NodeList
22+
var Nodelist []nodelineS
23+
24+
//Read reads NodeList from the file
25+
func Read(fn string) error {
26+
file, err := os.Open(fn)
27+
if err != nil {
28+
return err
29+
}
30+
defer file.Close()
31+
b, err := io.ReadAll(file)
32+
if err != nil {
33+
return err
34+
}
35+
scanner := bufio.NewScanner(strings.NewReader(string(b)))
36+
re := regexp.MustCompile(",")
37+
var z, n, f string
38+
for scanner.Scan() {
39+
if scanner.Text()[0] == ';' {
40+
continue
41+
}
42+
res := re.Split(scanner.Text(), -1)
43+
if len(res) < 5 {
44+
continue
45+
}
46+
switch strings.ToLower(res[0]) {
47+
case "zone":
48+
z = res[1]
49+
n = "0"
50+
f = "0"
51+
case "region":
52+
n = res[1]
53+
f = "0"
54+
case "host":
55+
n = res[1]
56+
f = "0"
57+
default:
58+
f = res[1]
59+
}
60+
address := types.AddrFromString(z+":"+n+"/"+f)
61+
node := nodelineS {
62+
Address: *address,
63+
BBS: res[2],
64+
City: res[3],
65+
Sysop: res[4],
66+
}
67+
Nodelist = append(Nodelist, node)
68+
}
69+
return nil
70+
}

pkg/nodelist/nodelist_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package nodelist
2+
3+
import (
4+
"github.com/askovpen/gossiped/pkg/types"
5+
. "github.com/franela/goblin"
6+
"testing"
7+
)
8+
9+
func TestNodelist(t *testing.T) {
10+
g := Goblin(t)
11+
g.Describe("Check Nodelist", func() {
12+
g.It("check nodelist.Read()", func() {
13+
Read("../../testdata/NODELIST.299")
14+
g.Assert(len(Nodelist)).Equal(1216)
15+
g.Assert(Nodelist[0].Address).Equal(*types.AddrFromString("1:0/0"))
16+
g.Assert(Nodelist[0].Sysop).Equal("Nick_Andre")
17+
g.Assert(Nodelist[0].BBS).Equal("North_America_(298)")
18+
g.Assert(Nodelist[0].City).Equal("Toronto")
19+
g.Assert(Nodelist[len(Nodelist)-1].Address).Equal(*types.AddrFromString("4:920/69"))
20+
g.Assert(Nodelist[len(Nodelist)-1].Sysop).Equal("John_Dovey")
21+
g.Assert(Nodelist[len(Nodelist)-1].BBS).Equal("El_Gato_De_Fuego_BBS_II")
22+
g.Assert(Nodelist[len(Nodelist)-1].City).Equal("Pedasi_Panama")
23+
})
24+
})
25+
}

pkg/ui/editheader.go

+27-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ui
33
import (
44
"github.com/askovpen/gossiped/pkg/config"
55
"github.com/askovpen/gossiped/pkg/msgapi"
6+
"github.com/askovpen/gossiped/pkg/nodelist"
67
"github.com/gdamore/tcell/v2"
78
"github.com/rivo/tview"
89
)
@@ -22,10 +23,11 @@ type EditHeader struct {
2223
sCoords [5]coords
2324
done func([5][]rune)
2425
msg *msgapi.Message
26+
app *App
2527
}
2628

2729
// NewEditHeader create new EditHeader
28-
func NewEditHeader(msg *msgapi.Message) *EditHeader {
30+
func NewEditHeader(a *App, msg *msgapi.Message) *EditHeader {
2931
eh := &EditHeader{
3032
Box: tview.NewBox().SetBackgroundColor(tcell.ColorDefault),
3133
sCoords: [5]coords{
@@ -45,6 +47,7 @@ func NewEditHeader(msg *msgapi.Message) *EditHeader {
4547
sPosition: [5]int{stringWidth(msg.From), stringWidth(msg.FromAddr.String()), stringWidth(msg.To), stringWidth(msg.ToAddr.String()), stringWidth(msg.Subject)},
4648
sIndex: 0,
4749
msg: msg,
50+
app: a,
4851
}
4952
return eh
5053
}
@@ -90,7 +93,12 @@ func (e *EditHeader) InputHandler() func(event *tcell.EventKey, setFocus func(p
9093
}
9194
switch key := event.Key(); key {
9295
case tcell.KeyTab:
93-
e.sIndex++
96+
if (e.sIndex == 2 || e.sIndex == 3) {
97+
e.app.Pages.AddPage(e.showNodeList())
98+
e.app.Pages.ShowPage("NodeListModal")
99+
} else {
100+
e.sIndex++
101+
}
94102
if e.sIndex == 5 {
95103
e.sIndex = 0
96104
} else if (*e.msg.AreaObject).GetType() != msgapi.EchoAreaTypeNetmail && e.sIndex == 3 {
@@ -137,3 +145,20 @@ func (e *EditHeader) SetDoneFunc(handler func([5][]rune)) *EditHeader {
137145
e.done = handler
138146
return e
139147
}
148+
149+
func (e *EditHeader) showNodeList() (string, tview.Primitive, bool, bool) {
150+
modal := NewModalNodeList().
151+
SetDoneFunc(func(buttonIndex int) {
152+
if (buttonIndex > 0) && (len(nodelist.Nodelist) > 0) {
153+
e.sInputs[2] = []rune(nodelist.Nodelist[buttonIndex-1].Sysop)
154+
if (*e.msg.AreaObject).GetType() == msgapi.EchoAreaTypeNetmail {
155+
e.sInputs[3] = []rune(nodelist.Nodelist[buttonIndex-1].Address.String())
156+
}
157+
e.sIndex = 4
158+
}
159+
e.app.Pages.HidePage("NodeListModal")
160+
e.app.Pages.RemovePage("NodeListModal")
161+
e.app.App.SetFocus(e.app.Pages)
162+
})
163+
return "NodeListModal", modal, true, true
164+
}

pkg/ui/insertmsg.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (a *App) InsertMsg(area *msgapi.AreaPrimitive, msgType int) (string, tview.
9191
}
9292
_, boxBg, _ := config.GetElementStyle(config.ColorAreaMessageHeader, config.ColorElementWindow).Decompose()
9393
mhStyle := config.GetElementStyle(config.ColorAreaMessageHeader, config.ColorElementTitle)
94-
a.im.eh = NewEditHeader(a.im.newMsg)
94+
a.im.eh = NewEditHeader(a, a.im.newMsg)
9595
a.im.eh.SetBackgroundColor(boxBg)
9696
a.im.eh.SetBorder(true).
9797
SetTitle(config.FormatTextWithStyle(" "+(*a.im.postArea).GetName()+" ", mhStyle)).

pkg/ui/modalnodelist.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package ui
2+
3+
import (
4+
"github.com/askovpen/gossiped/pkg/config"
5+
"github.com/askovpen/gossiped/pkg/nodelist"
6+
"github.com/gdamore/tcell/v2"
7+
"github.com/rivo/tview"
8+
)
9+
10+
// ModalNodeList is a centered message window used to inform the user or prompt them
11+
type ModalNodeList struct {
12+
*tview.Box
13+
table *tview.Table
14+
frame *tview.Frame
15+
textColor tcell.Color
16+
title string
17+
done func(buttonIndex int)
18+
}
19+
20+
// NewModalNodeList returns a new modal message window.
21+
func NewModalNodeList() *ModalNodeList {
22+
defFg, defBg, _ := config.StyleDefault.Decompose()
23+
m := &ModalNodeList{
24+
Box: tview.NewBox().SetBackgroundColor(defBg),
25+
textColor: defFg,
26+
}
27+
borderFg, _, borderAttr := config.GetElementStyle(config.ColorAreaAreaListModal, config.ColorElementBorder).Decompose()
28+
headerStyle := config.GetElementStyle(config.ColorAreaAreaListModal, config.ColorElementHeader)
29+
selectionStyle := config.GetElementStyle(config.ColorAreaAreaListModal, config.ColorElementSelection)
30+
itemStyle := config.GetElementStyle(config.ColorAreaAreaListModal, config.ColorElementItem)
31+
fgItem, bgItem, attrItem := itemStyle.Decompose()
32+
fgHeader, bgHeader, attrHeader := headerStyle.Decompose()
33+
m.table = tview.NewTable().
34+
SetFixed(1, 0).
35+
SetBordersColor(borderFg).
36+
SetSelectable(true, false).
37+
SetSelectedStyle(selectionStyle).
38+
SetSelectedFunc(func(row int, column int) {
39+
m.done(row)
40+
})
41+
m.frame = tview.NewFrame(m.table).SetBorders(0, 0, 1, 0, 0, 0)
42+
m.frame.SetBackgroundColor(defBg)
43+
m.table.SetBackgroundColor(defBg)
44+
m.frame.SetBorder(true).
45+
SetTitleAlign(tview.AlignLeft).
46+
SetBorderAttributes(borderAttr).
47+
SetBorderColor(borderFg).
48+
SetBorderPadding(0, 0, 1, 1)
49+
m.table.SetCell(
50+
0, 0, tview.NewTableCell(" Address").
51+
SetTextColor(fgHeader).SetBackgroundColor(bgHeader).SetAttributes(attrHeader).
52+
SetSelectable(false))
53+
m.table.SetCell(
54+
0, 1, tview.NewTableCell("Sysop").
55+
SetTextColor(fgHeader).SetBackgroundColor(bgHeader).SetAttributes(attrHeader).
56+
SetSelectable(false))
57+
m.table.SetCell(
58+
0, 2, tview.NewTableCell("City").
59+
SetTextColor(fgHeader).SetBackgroundColor(bgHeader).SetAttributes(attrHeader).
60+
SetSelectable(false))
61+
m.table.SetCell(
62+
0, 3, tview.NewTableCell("BBS").
63+
SetTextColor(fgHeader).SetBackgroundColor(bgHeader).SetAttributes(attrHeader).
64+
SetExpansion(1).
65+
SetSelectable(false))
66+
for i, node := range nodelist.Nodelist {
67+
m.table.SetCell(i+1, 0, tview.NewTableCell(node.Address.String()).
68+
SetTextColor(fgItem).SetBackgroundColor(bgItem).SetAttributes(attrItem))
69+
m.table.SetCell(i+1, 1, tview.NewTableCell(node.Sysop).
70+
SetTextColor(fgItem).SetBackgroundColor(bgItem).SetAttributes(attrItem))
71+
m.table.SetCell(i+1, 2, tview.NewTableCell(node.City).
72+
SetTextColor(fgItem).SetBackgroundColor(bgItem).SetAttributes(attrItem))
73+
m.table.SetCell(i+1, 3, tview.NewTableCell(node.BBS).
74+
SetTextColor(fgItem).SetBackgroundColor(bgItem).SetAttributes(attrItem))
75+
}
76+
return m
77+
}
78+
79+
// SetTextColor sets the color of the message text.
80+
func (m *ModalNodeList) SetTextColor(color tcell.Color) *ModalNodeList {
81+
m.textColor = color
82+
return m
83+
}
84+
85+
// SetDoneFunc sets a handler which is called when one of the buttons was
86+
// pressed. It receives the index of the button as well as its label text. The
87+
// handler is also called when the user presses the Escape key. The index will
88+
// then be negative and the label text an emptry string.
89+
func (m *ModalNodeList) SetDoneFunc(handler func(buttonIndex int)) *ModalNodeList {
90+
m.done = handler
91+
return m
92+
}
93+
94+
// SetText sets the message text of the window. The text may contain line
95+
// breaks. Note that words are wrapped, too, based on the final size of the
96+
// window.
97+
func (m *ModalNodeList) SetText(text string) *ModalNodeList {
98+
m.title = text
99+
style := config.GetElementStyle(config.ColorAreaAreaListModal, config.ColorElementTitle)
100+
m.frame.SetTitle(config.FormatTextWithStyle(text, style))
101+
return m
102+
}
103+
104+
// AddButtons adds buttons to the window. There must be at least one button and
105+
// a "done" handler so the window can be closed again.
106+
107+
// Focus is called when this primitive receives focus.
108+
func (m *ModalNodeList) Focus(delegate func(p tview.Primitive)) {
109+
//delegate(m.form)
110+
delegate(m.table)
111+
}
112+
113+
// HasFocus returns whether or not this primitive has focus.
114+
func (m *ModalNodeList) HasFocus() bool {
115+
//return m.form.HasFocus()
116+
return m.table.HasFocus()
117+
}
118+
119+
// Draw draws this primitive onto the screen.
120+
func (m *ModalNodeList) Draw(screen tcell.Screen) {
121+
width, height := screen.Size()
122+
height -= 7
123+
m.frame.Clear()
124+
x := 0
125+
y := 6
126+
m.SetRect(x, y, width, height)
127+
128+
// Draw the frame.
129+
m.frame.SetRect(x, y, width, height)
130+
m.frame.Draw(screen)
131+
}
132+
133+
// InputHandler handle input
134+
func (m *ModalNodeList) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
135+
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
136+
if m.HasFocus() {
137+
switch event.Key() {
138+
case tcell.KeyEscape:
139+
m.done(-1)
140+
}
141+
if handler := m.table.InputHandler(); handler != nil {
142+
handler(event, setFocus)
143+
}
144+
return
145+
}
146+
})
147+
}

0 commit comments

Comments
 (0)