Compare commits

...

7 Commits
0.1.1 ... trunk

Author SHA1 Message Date
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
13 changed files with 255 additions and 72 deletions

1
.nanpa/grid-lid-come.kdl Normal file
View File

@ -0,0 +1 @@
patch type="fixed" "use birthtime instead of mtime"

View File

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

View File

@ -1,5 +1,18 @@
# Changelog
## [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

@ -19,9 +19,11 @@ type Config struct {
}
var configList = []string{
"pager",
"publish to html",
"publish to gopher",
"default to nopub",
"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()
@ -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

@ -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

@ -25,7 +25,7 @@ const title = ` ___ __
/ _/__ ___ / /__
/ _/ -_) -_) (_-<
/_/ \__/\__/_/___/
neofeels 0.1.1`
neofeels 0.2.0`
func NewMainMenu(index int) *MainMenu {
return &MainMenu{
@ -123,10 +123,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 +166,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

@ -28,20 +28,23 @@ func NewPublishing() *Publishing {
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,12 +67,18 @@ 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()]))
}
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(), 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

@ -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"
)
@ -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,30 @@ 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")
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")
PathUserHTMLRender = path.Join(PathUserConfig, "html")
PathUserSubs = path.Join(PathUserConfig, "subs")
)
type User struct {
@ -64,17 +66,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 +112,7 @@ type Post struct {
Words int
Author string
Nopub bool
HTML bool
}
func GetPostsForUser(user string) (posts []Post) {
@ -110,6 +120,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 +131,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 +163,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 +177,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 +335,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()

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