Compare commits

...

10 Commits
0.1.1 ... trunk

Author SHA1 Message Date
46439d2488
0.3.0 2025-04-29 05:57:46 +03:00
c29a7c1594
add gopher publishing 2025-04-29 05:57:26 +03:00
350727cd58
add hotkey menu navigation
Closes: #13
2025-04-29 05:56:42 +03:00
9d4053da0b
use birthtime instead of mtime 2025-04-01 01:27:07 +03:00
6dd7d573b8
0.2.0 2025-01-20 14:43:22 +02:00
462d18850e
remove stray unpublishes from html dir 2025-01-20 14:40:16 +02:00
5a7db89b47
ui: clean up doubling 2025-01-20 14:36:14 +02:00
e8125b419f
add symlink to html 2025-01-12 14:48:39 +02:00
50cb60cdc4
add configurable pager 2025-01-12 14:26:30 +02:00
7e4456450c
add html rendering 2025-01-10 21:10:45 +02:00
21 changed files with 476 additions and 131 deletions

View File

@ -1,2 +1,2 @@
name neofeels
version 0.1.1
version 0.3.0

View File

@ -1,5 +1,29 @@
# Changelog
## [0.3.0] - 2025-04-29
### Added
- add gopher support
- shortcuts to list items in menu and management
### Fixed
- use birthtime instead of mtime
## [0.2.0] - 2025-01-20
### Added
- add html rendering
- add configurable pager
### Fixed
- fix hacky workaround for doubled inputs
- symlink html directory
- remove stray unpublishes from html dir
## [0.1.1] - 2025-01-09
- use 755 for config dir

View File

@ -4,7 +4,7 @@ SCDOC ?= scdoc
all: neofeels doc
neofeels:
neofeels: **/*.go
$(GO) build $(GOFLAGS) .
ifeq (, $(shell which $(SCDOC) 2>/dev/null))
@ -18,6 +18,3 @@ endif
clean:
rm neofeels doc/neofeels.1
.PHONY: all neofeels doc clean

View File

@ -25,14 +25,14 @@ type Backups struct {
func NewBackups() *Backups {
os.MkdirAll(ttbp.PathUserBackups, 0700)
backups, _ := os.ReadDir(ttbp.PathUserBackups)
list := []string{}
list := [][]vaxis.Segment{}
for _, backup := range backups {
timestamp, err := time.Parse("feels-backup-20060102-150405.tar.gz", backup.Name())
if err != nil {
continue
}
// XXX: for some reason this gets fucked up with timezones???
list = append(list, humanize.Time(timestamp.Local()))
list = append(list, []vaxis.Segment{{Text: humanize.Time(timestamp.Local())}})
}
return &Backups{

View File

@ -23,14 +23,14 @@ func NewBrowse() *Browse {
posts = append(posts, ttbp.GetPostsForUser(user.Name)...)
}
posts = ttbp.SortPostsByRecent(posts)
list := []string{}
list := [][]vaxis.Segment{}
for _, post := range posts {
list = append(list, fmt.Sprintf(
list = append(list, []vaxis.Segment{{Text: fmt.Sprintf(
"~%-15s %s (%d words)",
post.Author,
post.LastEdited.Format("2006-01-02 15:04"),
post.Words,
))
)}})
}
return &Browse{

View File

@ -23,13 +23,13 @@ type Bury struct {
func NewBury() *Bury {
user, _ := user.Current()
posts := ttbp.GetPostsForUser(user.Username)
list := []string{}
list := [][]vaxis.Segment{}
for _, post := range posts {
list = append(list, fmt.Sprintf(
list = append(list, []vaxis.Segment{{Text: fmt.Sprintf(
"%s (%d words)",
post.Date.Format("2006-01-02"),
post.Words,
))
)}})
}
return &Bury{

View File

@ -18,10 +18,12 @@ type Config struct {
descriptions []string
}
var configList = []string{
"publish to html",
"publish to gopher",
"default to nopub",
var configList = [][]vaxis.Segment{
[]vaxis.Segment{{Text: "pager"}},
[]vaxis.Segment{{Text: "publish to html"}},
[]vaxis.Segment{{Text: "publish to gopher"}},
[]vaxis.Segment{{Text: "default to nopub"}},
[]vaxis.Segment{{Text: "default to html"}},
}
func NewConfig() *Config {
@ -35,6 +37,10 @@ func NewConfig() *Config {
"↑↓/kj move ↵ enter q return",
cfg,
[]string{
`which pager do you want to use to display feels?
here you can add additional pre-processing to your pager. leaving this option
unset defaults to $PAGER, and if that isn't set, neofeels falls back to less.`,
`do you want to publish your feels online?
if yes, your feels will be published to a directory of your choice in your
@ -52,6 +58,10 @@ visible from your gopherhole, and will be purged from your gophermap on your
next entry update.`,
`should your posts automatically show up on your world-visible pages, such as
html and gopher? you can change this behaviour on a per-post basis after the
fact. changes to this setting will not be made retroactively.`,
`should your posts automatically be parsed as html in the neofeels reader?
this is not related to the html publishing option, and only applies to the
terminal reader. you can change this behaviour on a per-post basis after the
fact. changes to this setting will not be made retroactively.`,
`in which directory under public_html should your blog reside? for example,
"blog" will make it visible under https://tilde.town/~you/blog.`,
@ -75,23 +85,21 @@ func (config *Config) Event(state *ui.State, event vaxis.Event) (processed bool)
case "0", "1":
i, _ := strconv.Atoi(key.String())
config.list.SetIndex(i)
case "2", "3":
if config.config.Publishing {
i, _ := strconv.Atoi(key.String())
config.list.SetIndex(i)
}
case "q", "h", "Left":
ui.ViewChange <- NewMainMenu(6)
case "Enter", "l", "Right", "Space":
switch config.list.Index() {
case 0:
config.config.Publishing = !config.config.Publishing
config.config.Pager = config.changePager(state)
case 1:
config.config.Gopher = !config.config.Gopher
config.config.Publishing = !config.config.Publishing
case 2:
config.config.Nopub = !config.config.Nopub
config.config.Gopher = !config.config.Gopher
case 3:
config.config.Nopub = !config.config.Nopub
case 4:
config.config.HTML = !config.config.HTML
case 5:
config.config.PublishDir = config.changePublishDir(state)
}
config.config.Write()
@ -104,7 +112,7 @@ func (config *Config) Event(state *ui.State, event vaxis.Event) (processed bool)
func (config *Config) Draw(state *ui.State) {
if config.config.Publishing {
config.list.SetItems(append(configList, "html publish directory"))
config.list.SetItems(append(configList, []vaxis.Segment{{Text: "html publish directory"}}))
} else {
config.list.SetItems(configList)
}
@ -116,17 +124,19 @@ func (config *Config) Draw(state *ui.State) {
Column: win.Width/2 - 21,
Row: win.Height/2 - 2,
Width: 28,
Height: 4,
Height: 6,
})
win.New(win.Width/2-40, win.Height/2+3, 80, 10).Print(vaxis.Segment{Text: config.descriptions[config.list.Index()]})
win.New(win.Width/2-15, win.Height/2+13, 30, 1).Print(vaxis.Segment{Text: config.help})
win.New(win.Width/2-40, win.Height/2+5, 80, 10).Print(vaxis.Segment{Text: config.descriptions[config.list.Index()]})
win.New(win.Width/2-15, win.Height/2+15, 30, 1).Print(vaxis.Segment{Text: config.help})
// drawing the current selected options
win.New(win.Width/2+7, win.Height/2-2, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.Publishing)})
win.New(win.Width/2+7, win.Height/2-1, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.Gopher)})
win.New(win.Width/2+7, win.Height/2, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.Nopub)})
win.New(win.Width/2+7, win.Height/2-2, 40, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.Pager)})
win.New(win.Width/2+7, win.Height/2-1, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.Publishing)})
win.New(win.Width/2+7, win.Height/2, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.Gopher)})
win.New(win.Width/2+7, win.Height/2+1, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.Nopub)})
win.New(win.Width/2+7, win.Height/2+2, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.HTML)})
if config.config.Publishing {
win.New(win.Width/2+7, win.Height/2+1, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.PublishDir)})
win.New(win.Width/2+7, win.Height/2+3, 14, 1).Print(vaxis.Segment{Text: fmt.Sprintf(" %-12v", config.config.PublishDir)})
}
}
@ -139,7 +149,7 @@ func (config *Config) changePublishDir(state *ui.State) string {
Vx: win.Vx,
Parent: &win,
Column: win.Width/2 + 9,
Row: win.Height/2 + 1,
Row: win.Height/2 + 2,
Width: 12,
Height: 1,
})
@ -158,7 +168,7 @@ func (config *Config) changePublishDir(state *ui.State) string {
Vx: win.Vx,
Parent: nil,
Column: win.Width/2 + 9,
Row: win.Height/2 + 1,
Row: win.Height/2 + 2,
Width: 12,
Height: 1,
})
@ -167,3 +177,41 @@ func (config *Config) changePublishDir(state *ui.State) string {
return ""
}
func (config *Config) changePager(state *ui.State) string {
ti := textinput.New()
ti.SetContent(config.config.Pager)
config.Draw(state)
win := state.Window()
ti.Draw(vaxis.Window{
Vx: win.Vx,
Parent: &win,
Column: win.Width/2 + 9,
Row: win.Height/2 - 2,
Width: 40,
Height: 1,
})
for ev := range win.Vx.Events() {
switch ev := ev.(type) {
case vaxis.Key:
switch ev.String() {
case "Ctrl+c", "Esc", "Enter":
state.HideCursor()
return ti.String()
}
}
ti.Update(ev)
config.Draw(state)
ti.Draw(vaxis.Window{
Vx: win.Vx,
Parent: nil,
Column: win.Width/2 + 9,
Row: win.Height/2 - 2,
Width: 40,
Height: 1,
})
state.Render()
}
return ""
}

View File

@ -22,13 +22,13 @@ type Delete struct {
func NewDelete() *Delete {
user, _ := user.Current()
posts := ttbp.GetPostsForUser(user.Username)
list := []string{}
list := [][]vaxis.Segment{}
for _, post := range posts {
list = append(list, fmt.Sprintf(
list = append(list, []vaxis.Segment{{Text: fmt.Sprintf(
"%s (%d words)",
post.Date.Format("2006-01-02"),
post.Words,
))
)}})
}
return &Delete{

View File

@ -57,7 +57,7 @@ func (graffiti *Graffiti) Event(state *ui.State, event vaxis.Event) (processed b
}
win := state.Window()
win.New(win.Width/2-10, win.Height/2-8, 20, 5).Print(vaxis.Segment{Text: graffiti.title})
win.New(win.Width/2-40, win.Height/2-2, 80, 9).Print(vaxis.Segment{Text: graffiti.content})
win.New(win.Width/2-40, win.Height/2-2, 81, 9).Print(vaxis.Segment{Text: graffiti.content})
win.New(win.Width/2-9, win.Height/2+8, 18, 1).Print(vaxis.Segment{Text: graffiti.help})
return
}
@ -97,9 +97,6 @@ func editGraffiti(state *ui.State) {
continue
}
// for some reason vaxis doubles all events for Press/Release so this just ignores releases
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
vt.Update(ev)
}
}

View File

@ -23,15 +23,40 @@ type Management struct {
func NewManagement(index int) *Management {
return &Management{
title,
ui.NewList([]string{
"read over feels",
"modify feels publishing",
"backup your feels",
"import a feels backup",
"bury some feels",
"delete feels by day",
"purge all feels",
"wipe feels account",
ui.NewList([][]vaxis.Segment{
{
{Text: "r", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "ead over feels"},
},
{
{Text: "m", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "odify feels publishing"},
},
{
{Text: "b", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "ackup your feels"},
},
{
{Text: "i", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "mport a feels backup"},
},
{
{Text: "b"},
{Text: "u", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "ry some feels"},
},
{
{Text: "d", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "elete feels by day"},
},
{
{Text: "p", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "urge all feels"},
},
{
{Text: "w", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "ipe feels account"},
},
}, index),
"↑↓/kj move ↵ enter q return",
}
@ -50,6 +75,22 @@ func (management *Management) Event(state *ui.State, event vaxis.Event) (process
management.list.End()
case "Home", "g":
management.list.Home()
case "r":
management.list.SetIndex(0)
case "m":
management.list.SetIndex(1)
case "b":
management.list.SetIndex(2)
case "i":
management.list.SetIndex(3)
case "u":
management.list.SetIndex(4)
case "d":
management.list.SetIndex(5)
case "p":
management.list.SetIndex(6)
case "w":
management.list.SetIndex(7)
case "0", "1", "2", "3", "4", "5", "6", "7":
i, _ := strconv.Atoi(key.String())
management.list.SetIndex(i)

View File

@ -25,21 +25,53 @@ const title = ` ___ __
/ _/__ ___ / /__
/ _/ -_) -_) (_-<
/_/ \__/\__/_/___/
neofeels 0.1.1`
neofeels 0.3.0`
func NewMainMenu(index int) *MainMenu {
return &MainMenu{
title,
ui.NewList([]string{
"record some feels",
"manage your feels",
"check out your neighbors",
"browse global feels",
"visit your subscriptions",
"scribble some graffiti",
"change your settings",
"see credits",
"read documentation",
ui.NewList([][]vaxis.Segment{
{
{Text: "r", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "ecord some feels"},
},
{
{Text: "m", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "anage your feels"},
},
{
{Text: "check out your "},
{Text: "n", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "eighbors"},
},
{
{Text: "b", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "rowse global feels"},
},
{
{Text: "visit your "},
{Text: "s", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "ubscriptions"},
},
{
{Text: "scribble some gra"},
{Text: "f", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "fiti"},
},
{
{Text: "c", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "hange your settings"},
},
{
{Text: "see cr"},
{Text: "e", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "dits"},
},
{
{Text: "read "},
{Text: "d", Style: vaxis.Style{Attribute: vaxis.AttrReverse}},
{Text: "ocumentation"},
},
}, index),
"↑↓/kj move ↵ enter q exit",
}
@ -58,6 +90,24 @@ func (menu *MainMenu) Event(state *ui.State, event vaxis.Event) (processed bool)
menu.list.End()
case "Home", "g":
menu.list.Home()
case "r":
menu.list.SetIndex(0)
case "m":
menu.list.SetIndex(1)
case "n":
menu.list.SetIndex(2)
case "b":
menu.list.SetIndex(3)
case "s":
menu.list.SetIndex(4)
case "f":
menu.list.SetIndex(5)
case "c":
menu.list.SetIndex(6)
case "e":
menu.list.SetIndex(7)
case "d":
menu.list.SetIndex(8)
case "0", "1", "2", "3", "4", "5", "6", "7", "8":
i, _ := strconv.Atoi(key.String())
menu.list.SetIndex(i)
@ -123,10 +173,7 @@ func showManpage(state *ui.State) {
continue
}
// for some reason vaxis doubles all events for Press/Release so this just ignores releases
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
vt.Update(ev)
}
}
@ -169,9 +216,6 @@ func newFeels(state *ui.State) {
continue
}
// for some reason vaxis doubles all events for Press/Release so this just ignores releases
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
vt.Update(ev)
}
}

View File

@ -21,9 +21,9 @@ type Neighbors struct {
func NewNeighbors(index int) *Neighbors {
users := ttbp.SortUsersByRecent(ttbp.GetUsers())
subscriptions := ttbp.GetSubscriptions()
list := []string{}
list := [][]vaxis.Segment{}
for _, user := range users {
list = append(list, formatNeighbor(user, subscriptions))
list = append(list, []vaxis.Segment{{Text: formatNeighbor(user, subscriptions)}})
}
return &Neighbors{
@ -79,7 +79,7 @@ func (neighbors *Neighbors) Event(state *ui.State, event vaxis.Event) (processed
neighbors.subscriptions.Subscribe(user)
}
neighbors.subscriptions.Write()
neighbors.list.SetItem(neighbors.list.Index(), formatNeighbor(user, neighbors.subscriptions))
neighbors.list.SetItem(neighbors.list.Index(), []vaxis.Segment{{Text: formatNeighbor(user, neighbors.subscriptions)}})
case "Enter", "l", "Right":
ui.ViewChange <- NewUserPage(neighbors.neighbors[neighbors.list.Index()].Name, false, neighbors.list.Index())
}

View File

@ -20,28 +20,31 @@ type Publishing struct {
func NewPublishing() *Publishing {
user, _ := user.Current()
posts := ttbp.GetPostsForUser(user.Username)
list := []string{}
list := [][]vaxis.Segment{}
for _, post := range posts {
list = append(list, formatPublishing(post))
list = append(list, []vaxis.Segment{{Text: formatPublishing(post)}})
}
return &Publishing{
title,
ui.NewList(list, 0),
"↑↓/kj move ↵ enter q return",
"↑↓/kj move n nopub m html q return",
posts,
}
}
func formatPublishing(post ttbp.Post) string {
nopub := ""
status := ""
if post.Nopub {
nopub = "(nopub)"
status += "(nopub) "
}
if post.HTML {
status += "(html)"
}
return fmt.Sprintf(
"%s %s",
post.Date.Format("2006-01-02"),
nopub,
status,
)
}
@ -64,11 +67,17 @@ func (publishing *Publishing) Event(state *ui.State, event vaxis.Event) (process
case "q", "h", "Left":
ttbp.Publish()
ui.ViewChange <- NewManagement(1)
case "Enter", "l", "Right":
case "n":
if len(publishing.list.Items()) > 0 {
publishing.posts[publishing.list.Index()].Nopub = !publishing.posts[publishing.list.Index()].Nopub
ttbp.ToggleNopub(publishing.posts[publishing.list.Index()].Date)
publishing.list.SetItem(publishing.list.Index(), formatPublishing(publishing.posts[publishing.list.Index()]))
publishing.list.SetItem(publishing.list.Index(), []vaxis.Segment{{Text: formatPublishing(publishing.posts[publishing.list.Index()])}})
}
case "m":
if len(publishing.list.Items()) > 0 {
publishing.posts[publishing.list.Index()].HTML = !publishing.posts[publishing.list.Index()].HTML
ttbp.ToggleHTML(publishing.posts[publishing.list.Index()].Date)
publishing.list.SetItem(publishing.list.Index(), []vaxis.Segment{{Text: formatPublishing(publishing.posts[publishing.list.Index()])}})
}
}
processed = true
@ -83,10 +92,10 @@ func (publishing *Publishing) Draw(state *ui.State) {
publishing.list.Draw(vaxis.Window{
Vx: win.Vx,
Parent: nil,
Column: win.Width/2 - 14,
Column: win.Width/2 - 15,
Row: win.Height/2 - 2,
Width: 28,
Width: 30,
Height: 10,
})
win.New(win.Width/2-15, win.Height/2+9, 30, 1).Print(vaxis.Segment{Text: publishing.help})
win.New(win.Width/2-19, win.Height/2+9, 38, 1).Print(vaxis.Segment{Text: publishing.help})
}

View File

@ -19,11 +19,11 @@ type Subscriptions struct {
func NewSubscriptions(index int) *Subscriptions {
users := ttbp.SortUsersByRecent(ttbp.GetUsers())
subscriptions := ttbp.GetSubscriptions()
list := []string{}
list := [][]vaxis.Segment{}
neighbors := []ttbp.User{}
for _, user := range users {
if subscriptions.IsSubscribed(user) {
list = append(list, formatNeighbor(user, subscriptions))
list = append(list, []vaxis.Segment{{Text: formatNeighbor(user, subscriptions)}})
neighbors = append(neighbors, user)
}
}
@ -64,7 +64,7 @@ func (subscriptions *Subscriptions) Event(state *ui.State, event vaxis.Event) (p
} else {
subscriptions.subscriptions.Subscribe(user)
}
subscriptions.list.SetItem(subscriptions.list.Index(), formatNeighbor(user, subscriptions.subscriptions))
subscriptions.list.SetItem(subscriptions.list.Index(), []vaxis.Segment{{Text: formatNeighbor(user, subscriptions.subscriptions)}})
}
case "Enter", "l", "Right":
if len(subscriptions.list.Items()) > 0 {

View File

@ -9,6 +9,7 @@ import (
"git.sr.ht/~rockorager/vaxis"
"git.sr.ht/~rockorager/vaxis/widgets/term"
"git.tilde.town/nbsp/neofeels/config"
"git.tilde.town/nbsp/neofeels/ttbp"
"git.tilde.town/nbsp/neofeels/ui"
)
@ -24,13 +25,13 @@ type UserPage struct {
func NewUserPage(user string, self bool, index int) *UserPage {
posts := ttbp.GetPostsForUser(user)
list := []string{}
list := [][]vaxis.Segment{}
for _, post := range posts {
list = append(list, fmt.Sprintf(
list = append(list, []vaxis.Segment{{Text: fmt.Sprintf(
"%s (%d words)",
post.Date.Format("2006-01-02"),
post.Words,
))
)}})
}
return &UserPage{
@ -94,11 +95,27 @@ func showPost(state *ui.State, post ttbp.Post) {
vt.TERM = os.Getenv("TERM")
vt.Attach(state.PostEvent())
vt.Focus()
pager := os.ExpandEnv(os.Getenv("PAGER"))
cfg, err := config.Read()
if err != nil {
panic(err)
}
pager := os.ExpandEnv(cfg.Pager)
if cfg.Pager == "" {
pager = os.ExpandEnv(os.Getenv("PAGER"))
}
if pager == "" {
pager = "less"
}
err := vt.Start(exec.Command(pager, path.Join("/home", post.Author, ".ttbp/entries", post.Date.Format("20060102")+".txt")))
prepro := "cat"
if post.HTML {
prepro = "lynx -dump -force_html"
}
cmd := fmt.Sprintf("%s %s | %s", prepro, path.Join("/home", post.Author, ".ttbp/entries", post.Date.Format("20060102")+".txt"), pager)
err = vt.Start(exec.Command("sh", "-c", cmd))
if err != nil {
panic(err)
}
@ -116,9 +133,6 @@ func showPost(state *ui.State, post ttbp.Post) {
continue
}
// for some reason vaxis doubles all events for Press/Release so this just ignores releases
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
vt.Update(ev)
}
}

View File

@ -13,6 +13,8 @@ type Config struct {
PublishDir string `json:"publish dir"`
Publishing bool `json:"publishing"`
Rainbows bool `json:"rainbows"` // we don't care about this
HTML bool `json:"html"`
Pager string `json:"pager"`
}
var Default = &Config{
@ -22,6 +24,8 @@ var Default = &Config{
PublishDir: "blog",
Publishing: false,
Rainbows: false,
HTML: false,
Pager: "less",
}
func Read() (config *Config, err error) {

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.23.4
require (
git.sr.ht/~rockorager/vaxis v0.11.0
github.com/djherbis/times v1.6.0
github.com/dustin/go-humanize v1.0.1
github.com/yuin/goldmark v1.4.13
)

3
go.sum
View File

@ -6,6 +6,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
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/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
@ -43,6 +45,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=

View File

@ -13,28 +13,32 @@ import (
"time"
"git.tilde.town/nbsp/neofeels/config"
"github.com/djherbis/times"
"github.com/yuin/goldmark"
)
var (
PathVar = "/var/global/ttbp"
PathVarWWW = path.Join(PathVar, "www")
PathLive = "https://tilde.town/~"
PathUserFile = path.Join(PathVar, "users.txt")
PathGraff = path.Join(PathVar, "graffiti")
PathWall = path.Join(PathGraff, "wall.txt")
PathWallLock = path.Join(PathGraff, ".lock")
PathUser = os.Getenv("HOME")
PathUserFeels = path.Join(PathUser, ".ttbp")
PathUserHTML = path.Join(PathUser, "public_html")
PathUserConfig = path.Join(PathUserFeels, "config")
PathUserEntries = path.Join(PathUserFeels, "entries")
PathUserBuried = path.Join(PathUserFeels, "buried")
PathUserBackups = path.Join(PathUserFeels, "backups")
PathUserWWW = path.Join(PathUserFeels, "www")
PathUserRc = path.Join(PathUserConfig, "ttbprc")
PathUserNopub = path.Join(PathUserConfig, "nopub")
PathUserSubs = path.Join(PathUserConfig, "subs")
PathVar = "/var/global/ttbp"
PathVarWWW = path.Join(PathVar, "www")
PathLive = "https://tilde.town/~"
PathUserFile = path.Join(PathVar, "users.txt")
PathGraff = path.Join(PathVar, "graffiti")
PathWall = path.Join(PathGraff, "wall.txt")
PathWallLock = path.Join(PathGraff, ".lock")
PathUser = os.Getenv("HOME")
PathUserFeels = path.Join(PathUser, ".ttbp")
PathUserHTML = path.Join(PathUser, "public_html")
PathUserGopherhole = path.Join(PathUser, "public_gopher", "feels")
PathUserConfig = path.Join(PathUserFeels, "config")
PathUserEntries = path.Join(PathUserFeels, "entries")
PathUserBuried = path.Join(PathUserFeels, "buried")
PathUserBackups = path.Join(PathUserFeels, "backups")
PathUserWWW = path.Join(PathUserFeels, "www")
PathUserGopher = path.Join(PathUserFeels, "gopher")
PathUserRc = path.Join(PathUserConfig, "ttbprc")
PathUserNopub = path.Join(PathUserConfig, "nopub")
PathUserHTMLRender = path.Join(PathUserConfig, "html")
PathUserSubs = path.Join(PathUserConfig, "subs")
)
type User struct {
@ -64,17 +68,24 @@ func GetUsers() (users []User) {
}
// get last published file
entries, err := os.ReadDir(path.Join("/home", user.Name(), ".ttbp/entries"))
entriesDir := path.Join("/home", user.Name(), ".ttbp/entries")
entries, err := os.ReadDir(entriesDir)
if err != nil {
continue
}
var lastPublished time.Time = *new(time.Time)
if len(entries) > 0 {
info, err := entries[len(entries)-1].Info()
file, err := os.Open(path.Join(entriesDir, entries[len(entries)-1].Name()))
if err != nil {
continue
}
lastPublished = info.ModTime()
defer file.Close()
timespec, err := times.StatFile(file)
if err != nil {
continue
}
lastPublished = timespec.BirthTime()
}
users = append(users, User{
@ -103,6 +114,7 @@ type Post struct {
Words int
Author string
Nopub bool
HTML bool
}
func GetPostsForUser(user string) (posts []Post) {
@ -110,6 +122,7 @@ func GetPostsForUser(user string) (posts []Post) {
if err != nil {
return
}
nopubFile, err := os.OpenFile(PathUserNopub, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil {
return
@ -120,6 +133,18 @@ func GetPostsForUser(user string) (posts []Post) {
for nopubScanner.Scan() {
nopubs = append(nopubs, nopubScanner.Text())
}
htmlFile, err := os.OpenFile(PathUserHTMLRender, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil {
return
}
defer htmlFile.Close()
var htmls []string
htmlScanner := bufio.NewScanner(htmlFile)
for htmlScanner.Scan() {
htmls = append(htmls, htmlScanner.Text())
}
for _, post := range postFiles {
// retrieve date of file
// assume file ends in .txt
@ -140,7 +165,7 @@ func GetPostsForUser(user string) (posts []Post) {
}
// get modtime of file
stat, err := file.Stat()
timespec, err := times.StatFile(file)
if err != nil {
continue
}
@ -154,12 +179,22 @@ func GetPostsForUser(user string) (posts []Post) {
}
}
// see if file is in html
html := false
for _, name := range htmls {
if name == post.Name() {
html = true
break
}
}
posts = append([]Post{Post{
Author: user,
Date: fileDate,
LastEdited: stat.ModTime(),
LastEdited: timespec.BirthTime(),
Words: count,
Nopub: nopub,
HTML: html,
}}, posts...)
}
}
@ -302,15 +337,82 @@ func ToggleNopub(t time.Time) {
writer.Flush()
}
func NewHTML(t time.Time) {
cfg, err := config.Read()
if err != nil || (!cfg.HTML) {
return
}
dateString := t.Format("20060102.txt")
file, err := os.OpenFile(PathUserHTMLRender, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if scanner.Text() == dateString {
return
}
}
writer := bufio.NewWriter(file)
writer.WriteString(dateString)
writer.Flush()
}
func ToggleHTML(t time.Time) {
dateString := t.Format("20060102.txt")
htmls, err := os.ReadFile(PathUserHTMLRender)
if err != nil {
return
}
lines := strings.Split(strings.TrimSpace(string(htmls)), "\n")
newLines := []string{}
exists := false
for _, line := range lines {
if line == dateString {
exists = true
} else {
newLines = append(newLines, line)
}
}
if !exists {
newLines = append(newLines, dateString)
}
file, err := os.Create(PathUserHTMLRender)
if err != nil {
return
}
defer file.Close()
writer := bufio.NewWriter(file)
for _, line := range newLines {
if line == "" {
continue
}
_, err := writer.WriteString(line + "\n")
if err != nil {
return
}
}
writer.Flush()
}
func Publish() {
cfg, err := config.Read()
if err != nil {
return // TODO: expose this error to the user
}
if cfg.Publishing {
os.RemoveAll(PathUserWWW) // remove all post and start over
if _, err := os.Stat(PathUserWWW); os.IsNotExist(err) {
os.MkdirAll(PathUserWWW, 0700)
os.Symlink(path.Join(PathUserConfig, "style.css"), path.Join(PathUserWWW, "style.css"))
os.Symlink(PathUserWWW, path.Join(PathUserHTML, cfg.PublishDir))
}
file, err := os.Create(path.Join(PathUserWWW, "index.html"))
defer file.Close()
@ -336,7 +438,49 @@ func Publish() {
writer.WriteString(string(footer))
writer.Flush()
}
// TODO: gopher
if cfg.Gopher {
os.RemoveAll(PathUserGopher) // remove all posts and start over
if _, err := os.Stat(PathUserGopher); os.IsNotExist(err) {
os.MkdirAll(PathUserGopher, 0755)
os.Symlink(PathUserGopher, PathUserGopherhole)
}
file, err := os.Create(path.Join(PathUserGopher, "gophermap"))
defer file.Close()
if err != nil {
return
}
writer := bufio.NewWriter(file)
user, _ := user.Current()
writer.WriteString(fmt.Sprintf(`
welcome to %s's gopherfeels.
.:: .::
.: .::
.:.: .: .:: .:: .:: .::::
.:: .: .:: .: .:: .::.::
.:: .::::: .::.::::: .:: .:: .:::
.:: .: .: .:: .::
.:: .:::: .:::: .:::.:: .::
this file is automatically generated by ttbp.
0(about ttbp)%s/~endorphant/ttbp.txt%stilde.town%s70
1(back to user's home)%s/~%s
entries:
`, user.Username, "\t", "\t", "\t", "\t", user.Username))
for _, post := range GetPostsForUser(user.Username) {
if !post.Nopub && post.Words > 0 {
dateString := post.Date.Format("20060102.txt")
os.Symlink(path.Join(PathUserEntries, dateString), path.Join(PathUserGopher, dateString))
writer.WriteString(fmt.Sprintf("0%s\t%s\n", post.Date.Format("2006-01-02"), dateString))
}
}
writer.Flush()
}
}
func Unpublish() {
@ -348,6 +492,10 @@ func Unpublish() {
os.RemoveAll(PathUserWWW)
os.RemoveAll(path.Join(PathUserHTML, cfg.PublishDir))
}
if cfg.Gopher {
os.RemoveAll(PathUserGopher)
os.RemoveAll(PathUserGopherhole)
}
}
func writePage(post Post, header, footer []byte) {

View File

@ -13,11 +13,11 @@ import (
type List struct {
index int
items []string
items [][]vaxis.Segment
offset int
}
func NewList(items []string, index int) List {
func NewList(items [][]vaxis.Segment, index int) List {
return List{
items: items,
index: index,
@ -32,7 +32,7 @@ func (m *List) Draw(win vaxis.Window) {
m.offset = m.index
}
defaultStyle := vaxis.Style{}
defaultStyle := vaxis.Style{Attribute: vaxis.AttrNone}
selectedStyle := vaxis.Style{Attribute: vaxis.AttrReverse}
index := m.index - m.offset
@ -43,7 +43,15 @@ func (m *List) Draw(win vaxis.Window) {
} else {
style = defaultStyle
}
win.Println(i, vaxis.Segment{Text: fmt.Sprintf(" %-"+strconv.Itoa(win.Width-2)+"s", subject), Style: style})
var styledSubject []vaxis.Segment
length := 0
for _, chunk := range subject {
length += len(chunk.Text)
styledSubject = append(styledSubject, vaxis.Segment{Text: chunk.Text, Style: vaxis.Style{Attribute: style.Attribute | chunk.Style.Attribute}})
}
styledSubject = append([]vaxis.Segment{{Text: " ", Style: style}}, styledSubject...)
styledSubject = append(styledSubject, vaxis.Segment{Text: fmt.Sprintf("%"+strconv.Itoa(win.Width-length-2)+"s", ""), Style: style})
win.Println(i, styledSubject...)
}
}
@ -83,15 +91,23 @@ func (m *List) PageUp(win vaxis.Window) {
}
func (m *List) Items() []string {
return m.items
var items []string
for _, item := range m.items {
var text string
for _, chunk := range item {
text += chunk.Text
}
items = append(items, text)
}
return items
}
func (m *List) SetItems(items []string) {
func (m *List) SetItems(items [][]vaxis.Segment) {
m.items = items
m.index = min(len(items)-1, m.index)
}
func (m *List) SetItem(index int, item string) {
func (m *List) SetItem(index int, item []vaxis.Segment) {
m.items[index] = item
}

View File

@ -25,8 +25,7 @@ type State struct {
func New(view View) (state State, err error) {
vx, err := vaxis.New(vaxis.Options{
DisableMouse: false,
CSIuBitMask: vaxis.CSIuDisambiguate | vaxis.CSIuReportEvents | vaxis.CSIuAlternateKeys | vaxis.CSIuAllKeys | vaxis.CSIuAssociatedText,
DisableMouse: true,
})
if err != nil {
return