Compare commits

...

11 Commits
0.1.0 ... 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
5868ae2f13
0.1.1 2025-01-09 05:05:55 +02:00
251c250a8d
default to nano and warn if $EDITOR not set 2025-01-09 05:04:00 +02:00
491adc5290
use 755 for config dir for ttbprc readability 2025-01-09 04:58:02 +02:00
0e00aeaf45
save position of list 2025-01-09 04:55:06 +02:00
24 changed files with 339 additions and 113 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 name neofeels
version 0.1.0 version 0.2.0

View File

@ -1,5 +1,24 @@
# Changelog # 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
- save position of previous list when going back
- default to nano and warn if no $EDITOR set
## [0.1.0] - 2025-01-08 ## [0.1.0] - 2025-01-08
initial release: fully supports everything in ttbp, in a backwards-compatible initial release: fully supports everything in ttbp, in a backwards-compatible

View File

@ -37,7 +37,7 @@ func NewBackups() *Backups {
return &Backups{ return &Backups{
title, title,
ui.NewList(list), ui.NewList(list, 0),
"↑↓/kj move ↵ enter q return", "↑↓/kj move ↵ enter q return",
"choose a backup to restore", "choose a backup to restore",
backups, backups,
@ -61,7 +61,7 @@ func (backups *Backups) Event(state *ui.State, event vaxis.Event) (processed boo
i, _ := strconv.Atoi(key.String()) i, _ := strconv.Atoi(key.String())
backups.list.SetIndex(i) backups.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewManagement() ui.ViewChange <- NewManagement(3)
case "Enter", "l", "Right": case "Enter", "l", "Right":
if len(backups.list.Items()) > 0 { if len(backups.list.Items()) > 0 {
backups.LoadBackup(state) backups.LoadBackup(state)

View File

@ -35,7 +35,7 @@ func NewBrowse() *Browse {
return &Browse{ return &Browse{
title, title,
ui.NewList(list), ui.NewList(list, 0),
"↑↓/kj move ↵ enter q return", "↑↓/kj move ↵ enter q return",
posts, posts,
} }
@ -58,7 +58,7 @@ func (browse *Browse) Event(state *ui.State, event vaxis.Event) (processed bool)
i, _ := strconv.Atoi(key.String()) i, _ := strconv.Atoi(key.String())
browse.list.SetIndex(i) browse.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(3)
case "Enter", "l", "Right": case "Enter", "l", "Right":
showPost(state, browse.posts[browse.list.Index()]) showPost(state, browse.posts[browse.list.Index()])
} }

View File

@ -34,7 +34,7 @@ func NewBury() *Bury {
return &Bury{ return &Bury{
title, title,
ui.NewList(list), ui.NewList(list, 0),
"↑↓/kj move ↵ enter q return", "↑↓/kj move ↵ enter q return",
posts, posts,
} }
@ -57,7 +57,7 @@ func (bury *Bury) Event(state *ui.State, event vaxis.Event) (processed bool) {
i, _ := strconv.Atoi(key.String()) i, _ := strconv.Atoi(key.String())
bury.list.SetIndex(i) bury.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewManagement() ui.ViewChange <- NewManagement(4)
case "Enter", "l", "Right": case "Enter", "l", "Right":
if len(bury.list.Items()) > 0 { if len(bury.list.Items()) > 0 {
bury.Confirmation(state, func() { bury.Confirmation(state, func() {

View File

@ -19,9 +19,11 @@ type Config struct {
} }
var configList = []string{ var configList = []string{
"pager",
"publish to html", "publish to html",
"publish to gopher", "publish to gopher",
"default to nopub", "default to nopub",
"default to html",
} }
func NewConfig() *Config { func NewConfig() *Config {
@ -31,10 +33,14 @@ func NewConfig() *Config {
} }
return &Config{ return &Config{
title, title,
ui.NewList(configList), ui.NewList(configList, 0),
"↑↓/kj move ↵ enter q return", "↑↓/kj move ↵ enter q return",
cfg, cfg,
[]string{ []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? `do you want to publish your feels online?
if yes, your feels will be published to a directory of your choice in your 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.`, next entry update.`,
`should your posts automatically show up on your world-visible pages, such as `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 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.`, fact. changes to this setting will not be made retroactively.`,
`in which directory under public_html should your blog reside? for example, `in which directory under public_html should your blog reside? for example,
"blog" will make it visible under https://tilde.town/~you/blog.`, "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": case "0", "1":
i, _ := strconv.Atoi(key.String()) i, _ := strconv.Atoi(key.String())
config.list.SetIndex(i) config.list.SetIndex(i)
case "2", "3":
if config.config.Publishing {
i, _ := strconv.Atoi(key.String())
config.list.SetIndex(i)
}
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(6)
case "Enter", "l", "Right", "Space": case "Enter", "l", "Right", "Space":
switch config.list.Index() { switch config.list.Index() {
case 0: case 0:
config.config.Publishing = !config.config.Publishing config.config.Pager = config.changePager(state)
case 1: case 1:
config.config.Gopher = !config.config.Gopher config.config.Publishing = !config.config.Publishing
case 2: case 2:
config.config.Nopub = !config.config.Nopub config.config.Gopher = !config.config.Gopher
case 3: 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.PublishDir = config.changePublishDir(state)
} }
config.config.Write() config.config.Write()
@ -116,17 +124,19 @@ func (config *Config) Draw(state *ui.State) {
Column: win.Width/2 - 21, Column: win.Width/2 - 21,
Row: win.Height/2 - 2, Row: win.Height/2 - 2,
Width: 28, 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-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+13, 30, 1).Print(vaxis.Segment{Text: config.help}) win.New(win.Width/2-15, win.Height/2+15, 30, 1).Print(vaxis.Segment{Text: config.help})
// drawing the current selected options // 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-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.Gopher)}) 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.Nopub)}) 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 { 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, Vx: win.Vx,
Parent: &win, Parent: &win,
Column: win.Width/2 + 9, Column: win.Width/2 + 9,
Row: win.Height/2 + 1, Row: win.Height/2 + 2,
Width: 12, Width: 12,
Height: 1, Height: 1,
}) })
@ -158,7 +168,7 @@ func (config *Config) changePublishDir(state *ui.State) string {
Vx: win.Vx, Vx: win.Vx,
Parent: nil, Parent: nil,
Column: win.Width/2 + 9, Column: win.Width/2 + 9,
Row: win.Height/2 + 1, Row: win.Height/2 + 2,
Width: 12, Width: 12,
Height: 1, Height: 1,
}) })
@ -167,3 +177,41 @@ func (config *Config) changePublishDir(state *ui.State) string {
return "" 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

@ -31,7 +31,7 @@ func (credits *Credits) Event(state *ui.State, event vaxis.Event) (processed boo
case "Ctrl+c", "Ctrl+d": case "Ctrl+c", "Ctrl+d":
close(ui.Quit) close(ui.Quit)
case "Enter", "q", "h", "l", "Left", "Right": case "Enter", "q", "h", "l", "Left", "Right":
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(7)
} }
processed = true processed = true
} }

View File

@ -33,7 +33,7 @@ func NewDelete() *Delete {
return &Delete{ return &Delete{
title, title,
ui.NewList(list), ui.NewList(list, 0),
"↑↓/kj move ↵ enter q return", "↑↓/kj move ↵ enter q return",
posts, posts,
} }
@ -56,7 +56,7 @@ func (delete *Delete) Event(state *ui.State, event vaxis.Event) (processed bool)
i, _ := strconv.Atoi(key.String()) i, _ := strconv.Atoi(key.String())
delete.list.SetIndex(i) delete.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewManagement() ui.ViewChange <- NewManagement(5)
case "Enter", "l", "Right": case "Enter", "l", "Right":
if len(delete.list.Items()) > 0 { if len(delete.list.Items()) > 0 {
delete.Confirmation(state, func() { delete.Confirmation(state, func() {

View File

@ -1,6 +1,8 @@
package app package app
import ( import (
"bufio"
"fmt"
"os" "os"
"os/exec" "os/exec"
@ -42,25 +44,36 @@ func (graffiti *Graffiti) Event(state *ui.State, event vaxis.Event) (processed b
case "Ctrl+c", "Ctrl+d": case "Ctrl+c", "Ctrl+d":
close(ui.Quit) close(ui.Quit)
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(5)
case "Enter", "l", "Right": case "Enter", "l", "Right":
if ttbp.GraffitiFree() { if ttbp.GraffitiFree() {
os.Create(ttbp.PathWallLock) os.Create(ttbp.PathWallLock)
editGraffiti(state) editGraffiti(state)
os.Remove(ttbp.PathWallLock) os.Remove(ttbp.PathWallLock)
} }
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(5)
} }
processed = true processed = true
} }
win := state.Window() 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-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}) win.New(win.Width/2-9, win.Height/2+8, 18, 1).Print(vaxis.Segment{Text: graffiti.help})
return return
} }
func editGraffiti(state *ui.State) { func editGraffiti(state *ui.State) {
// if $EDITOR isn't set, warn about it, and use nano
editor := os.ExpandEnv(os.Getenv("EDITOR"))
if editor == "" {
editor = "nano"
state.Suspend()
fmt.Print("$EDITOR not found, using nano. press ↵ to continue")
input := bufio.NewScanner(os.Stdin)
input.Scan()
state.Resume()
}
state.HideCursor() state.HideCursor()
vt := term.New() vt := term.New()
vt.TERM = os.Getenv("TERM") vt.TERM = os.Getenv("TERM")
@ -84,9 +97,6 @@ func editGraffiti(state *ui.State) {
continue continue
} }
// for some reason vaxis doubles all events for Press/Release so this just ignores releases vt.Update(ev)
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
} }
} }

View File

@ -20,7 +20,7 @@ type Management struct {
help string help string
} }
func NewManagement() *Management { func NewManagement(index int) *Management {
return &Management{ return &Management{
title, title,
ui.NewList([]string{ ui.NewList([]string{
@ -32,7 +32,7 @@ func NewManagement() *Management {
"delete feels by day", "delete feels by day",
"purge all feels", "purge all feels",
"wipe feels account", "wipe feels account",
}), }, index),
"↑↓/kj move ↵ enter q return", "↑↓/kj move ↵ enter q return",
} }
} }
@ -54,12 +54,12 @@ func (management *Management) Event(state *ui.State, event vaxis.Event) (process
i, _ := strconv.Atoi(key.String()) i, _ := strconv.Atoi(key.String())
management.list.SetIndex(i) management.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(1)
case "Enter", "l", "Right": case "Enter", "l", "Right":
switch management.list.Index() { switch management.list.Index() {
case 0: case 0:
user, _ := user.Current() user, _ := user.Current()
ui.ViewChange <- NewUserPage(user.Username, true) ui.ViewChange <- NewUserPage(user.Username, true, 0)
case 1: case 1:
ui.ViewChange <- NewPublishing() ui.ViewChange <- NewPublishing()
case 2: case 2:

View File

@ -1,6 +1,8 @@
package app package app
import ( import (
"bufio"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@ -23,9 +25,9 @@ const title = ` ___ __
/ _/__ ___ / /__ / _/__ ___ / /__
/ _/ -_) -_) (_-< / _/ -_) -_) (_-<
/_/ \__/\__/_/___/ /_/ \__/\__/_/___/
neofeels 0.1.0` neofeels 0.2.0`
func NewMainMenu() *MainMenu { func NewMainMenu(index int) *MainMenu {
return &MainMenu{ return &MainMenu{
title, title,
ui.NewList([]string{ ui.NewList([]string{
@ -38,7 +40,7 @@ func NewMainMenu() *MainMenu {
"change your settings", "change your settings",
"see credits", "see credits",
"read documentation", "read documentation",
}), }, index),
"↑↓/kj move ↵ enter q exit", "↑↓/kj move ↵ enter q exit",
} }
} }
@ -64,13 +66,13 @@ func (menu *MainMenu) Event(state *ui.State, event vaxis.Event) (processed bool)
case 0: case 0:
newFeels(state) newFeels(state)
case 1: case 1:
ui.ViewChange <- NewManagement() ui.ViewChange <- NewManagement(0)
case 2: case 2:
ui.ViewChange <- NewNeighbors() ui.ViewChange <- NewNeighbors(0)
case 3: case 3:
ui.ViewChange <- NewBrowse() ui.ViewChange <- NewBrowse()
case 4: case 4:
ui.ViewChange <- NewSubscriptions() ui.ViewChange <- NewSubscriptions(0)
case 5: case 5:
ui.ViewChange <- NewGraffiti() ui.ViewChange <- NewGraffiti()
case 6: case 6:
@ -121,21 +123,29 @@ func showManpage(state *ui.State) {
continue continue
} }
// for some reason vaxis doubles all events for Press/Release so this just ignores releases vt.Update(ev)
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
} }
} }
func newFeels(state *ui.State) { func newFeels(state *ui.State) {
// if $EDITOR isn't set, warn about it, and use nano
editor := os.ExpandEnv(os.Getenv("EDITOR"))
if editor == "" {
editor = "nano"
state.Suspend()
fmt.Print("$EDITOR not found, using nano. press ↵ to continue")
input := bufio.NewScanner(os.Stdin)
input.Scan()
state.Resume()
}
state.HideCursor() state.HideCursor()
vt := term.New() vt := term.New()
vt.TERM = os.Getenv("TERM") vt.TERM = os.Getenv("TERM")
vt.Attach(state.PostEvent()) vt.Attach(state.PostEvent())
vt.Focus() vt.Focus()
now := time.Now() now := time.Now()
err := vt.Start(exec.Command(os.ExpandEnv(os.Getenv("EDITOR")), path.Join(ttbp.PathUserEntries, now.Format("20060102")+".txt"))) err := vt.Start(exec.Command(editor, path.Join(ttbp.PathUserEntries, now.Format("20060102")+".txt")))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -156,9 +166,6 @@ func newFeels(state *ui.State) {
continue continue
} }
// for some reason vaxis doubles all events for Press/Release so this just ignores releases vt.Update(ev)
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
} }
} }

View File

@ -18,7 +18,7 @@ type Neighbors struct {
subscriptions *ttbp.Subscriptions subscriptions *ttbp.Subscriptions
} }
func NewNeighbors() *Neighbors { func NewNeighbors(index int) *Neighbors {
users := ttbp.SortUsersByRecent(ttbp.GetUsers()) users := ttbp.SortUsersByRecent(ttbp.GetUsers())
subscriptions := ttbp.GetSubscriptions() subscriptions := ttbp.GetSubscriptions()
list := []string{} list := []string{}
@ -28,7 +28,7 @@ func NewNeighbors() *Neighbors {
return &Neighbors{ return &Neighbors{
title, title,
ui.NewList(list), ui.NewList(list, index),
"↑↓/kj move ↵ enter s subscribe q return", "↑↓/kj move ↵ enter s subscribe q return",
users, users,
subscriptions, subscriptions,
@ -70,7 +70,7 @@ func (neighbors *Neighbors) Event(state *ui.State, event vaxis.Event) (processed
i, _ := strconv.Atoi(key.String()) i, _ := strconv.Atoi(key.String())
neighbors.list.SetIndex(i) neighbors.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(2)
case "s": case "s":
user := neighbors.neighbors[neighbors.list.Index()] user := neighbors.neighbors[neighbors.list.Index()]
if neighbors.subscriptions.IsSubscribed(user) { if neighbors.subscriptions.IsSubscribed(user) {
@ -81,7 +81,7 @@ func (neighbors *Neighbors) Event(state *ui.State, event vaxis.Event) (processed
neighbors.subscriptions.Write() neighbors.subscriptions.Write()
neighbors.list.SetItem(neighbors.list.Index(), formatNeighbor(user, neighbors.subscriptions)) neighbors.list.SetItem(neighbors.list.Index(), formatNeighbor(user, neighbors.subscriptions))
case "Enter", "l", "Right": case "Enter", "l", "Right":
ui.ViewChange <- NewUserPage(neighbors.neighbors[neighbors.list.Index()].Name, false) ui.ViewChange <- NewUserPage(neighbors.neighbors[neighbors.list.Index()].Name, false, neighbors.list.Index())
} }
processed = true processed = true
} }

View File

@ -40,7 +40,7 @@ func (posted *Posted) Event(state *ui.State, event vaxis.Event) (processed bool)
case "Ctrl+c", "Ctrl+d": case "Ctrl+c", "Ctrl+d":
close(ui.Quit) close(ui.Quit)
case "Enter", "q", "h", "l", "Left", "Right": case "Enter", "q", "h", "l", "Left", "Right":
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(0)
} }
processed = true processed = true
} }

View File

@ -27,21 +27,24 @@ func NewPublishing() *Publishing {
return &Publishing{ return &Publishing{
title, title,
ui.NewList(list), ui.NewList(list, 0),
"↑↓/kj move ↵ enter q return", "↑↓/kj move n nopub m html q return",
posts, posts,
} }
} }
func formatPublishing(post ttbp.Post) string { func formatPublishing(post ttbp.Post) string {
nopub := "" status := ""
if post.Nopub { if post.Nopub {
nopub = "(nopub)" status += "(nopub) "
}
if post.HTML {
status += "(html)"
} }
return fmt.Sprintf( return fmt.Sprintf(
"%s %s", "%s %s",
post.Date.Format("2006-01-02"), post.Date.Format("2006-01-02"),
nopub, status,
) )
} }
@ -63,13 +66,19 @@ func (publishing *Publishing) Event(state *ui.State, event vaxis.Event) (process
publishing.list.SetIndex(i) publishing.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
ttbp.Publish() ttbp.Publish()
ui.ViewChange <- NewManagement() ui.ViewChange <- NewManagement(1)
case "Enter", "l", "Right": case "n":
if len(publishing.list.Items()) > 0 { if len(publishing.list.Items()) > 0 {
publishing.posts[publishing.list.Index()].Nopub = !publishing.posts[publishing.list.Index()].Nopub publishing.posts[publishing.list.Index()].Nopub = !publishing.posts[publishing.list.Index()].Nopub
ttbp.ToggleNopub(publishing.posts[publishing.list.Index()].Date) 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(), 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 processed = true
} }
@ -83,10 +92,10 @@ func (publishing *Publishing) Draw(state *ui.State) {
publishing.list.Draw(vaxis.Window{ publishing.list.Draw(vaxis.Window{
Vx: win.Vx, Vx: win.Vx,
Parent: nil, Parent: nil,
Column: win.Width/2 - 14, Column: win.Width/2 - 15,
Row: win.Height/2 - 2, Row: win.Height/2 - 2,
Width: 28, Width: 30,
Height: 10, 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

@ -16,7 +16,7 @@ type Subscriptions struct {
subscriptions *ttbp.Subscriptions subscriptions *ttbp.Subscriptions
} }
func NewSubscriptions() *Subscriptions { func NewSubscriptions(index int) *Subscriptions {
users := ttbp.SortUsersByRecent(ttbp.GetUsers()) users := ttbp.SortUsersByRecent(ttbp.GetUsers())
subscriptions := ttbp.GetSubscriptions() subscriptions := ttbp.GetSubscriptions()
list := []string{} list := []string{}
@ -30,7 +30,7 @@ func NewSubscriptions() *Subscriptions {
return &Subscriptions{ return &Subscriptions{
title, title,
ui.NewList(list), ui.NewList(list, index),
"↑↓/kj move ↵ enter s subscribe q return", "↑↓/kj move ↵ enter s subscribe q return",
neighbors, neighbors,
subscriptions, subscriptions,
@ -55,7 +55,7 @@ func (subscriptions *Subscriptions) Event(state *ui.State, event vaxis.Event) (p
subscriptions.list.SetIndex(i) subscriptions.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
subscriptions.subscriptions.Write() subscriptions.subscriptions.Write()
ui.ViewChange <- NewMainMenu() ui.ViewChange <- NewMainMenu(4)
case "s": case "s":
if len(subscriptions.list.Items()) > 0 { if len(subscriptions.list.Items()) > 0 {
user := subscriptions.neighbors[subscriptions.list.Index()] user := subscriptions.neighbors[subscriptions.list.Index()]
@ -69,7 +69,7 @@ func (subscriptions *Subscriptions) Event(state *ui.State, event vaxis.Event) (p
case "Enter", "l", "Right": case "Enter", "l", "Right":
if len(subscriptions.list.Items()) > 0 { if len(subscriptions.list.Items()) > 0 {
subscriptions.subscriptions.Write() subscriptions.subscriptions.Write()
ui.ViewChange <- NewUserPage(subscriptions.neighbors[subscriptions.list.Index()].Name, false) ui.ViewChange <- NewUserPage(subscriptions.neighbors[subscriptions.list.Index()].Name, false, subscriptions.list.Index())
} }
} }
processed = true processed = true

View File

@ -9,6 +9,7 @@ import (
"git.sr.ht/~rockorager/vaxis" "git.sr.ht/~rockorager/vaxis"
"git.sr.ht/~rockorager/vaxis/widgets/term" "git.sr.ht/~rockorager/vaxis/widgets/term"
"git.tilde.town/nbsp/neofeels/config"
"git.tilde.town/nbsp/neofeels/ttbp" "git.tilde.town/nbsp/neofeels/ttbp"
"git.tilde.town/nbsp/neofeels/ui" "git.tilde.town/nbsp/neofeels/ui"
) )
@ -19,9 +20,10 @@ type UserPage struct {
help string help string
posts []ttbp.Post posts []ttbp.Post
self bool self bool
index int // which index on the previous page to return to?
} }
func NewUserPage(user string, self bool) *UserPage { func NewUserPage(user string, self bool, index int) *UserPage {
posts := ttbp.GetPostsForUser(user) posts := ttbp.GetPostsForUser(user)
list := []string{} list := []string{}
for _, post := range posts { for _, post := range posts {
@ -34,10 +36,11 @@ func NewUserPage(user string, self bool) *UserPage {
return &UserPage{ return &UserPage{
title, title,
ui.NewList(list), ui.NewList(list, 0),
"↑↓/kj move ↵ enter q return", "↑↓/kj move ↵ enter q return",
posts, posts,
self, self,
index,
} }
} }
@ -59,9 +62,9 @@ func (user *UserPage) Event(state *ui.State, event vaxis.Event) (processed bool)
user.list.SetIndex(i) user.list.SetIndex(i)
case "q", "h", "Left": case "q", "h", "Left":
if user.self { if user.self {
ui.ViewChange <- NewManagement() ui.ViewChange <- NewManagement(0)
} else { } else {
ui.ViewChange <- NewNeighbors() ui.ViewChange <- NewNeighbors(user.index)
} }
case "Enter", "l", "Right": case "Enter", "l", "Right":
showPost(state, user.posts[user.list.Index()]) showPost(state, user.posts[user.list.Index()])
@ -92,11 +95,27 @@ func showPost(state *ui.State, post ttbp.Post) {
vt.TERM = os.Getenv("TERM") vt.TERM = os.Getenv("TERM")
vt.Attach(state.PostEvent()) vt.Attach(state.PostEvent())
vt.Focus() 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 == "" { if pager == "" {
pager = "less" 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 { if err != nil {
panic(err) panic(err)
} }
@ -114,9 +133,6 @@ func showPost(state *ui.State, post ttbp.Post) {
continue continue
} }
// for some reason vaxis doubles all events for Press/Release so this just ignores releases vt.Update(ev)
if key, ok := ev.(vaxis.Key); ok && key.EventType == vaxis.EventPress {
vt.Update(ev)
}
} }
} }

View File

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

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.23.4
require ( require (
git.sr.ht/~rockorager/vaxis v0.11.0 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/dustin/go-humanize v1.0.1
github.com/yuin/goldmark v1.4.13 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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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= 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-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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=

View File

@ -39,7 +39,7 @@ press ↵ to set up an account, or Ctrl+c to quit. you can always come back late
user, _ := user.Current() user, _ := user.Current()
header := strings.ReplaceAll(header, "%USER%", user.Username) header := strings.ReplaceAll(header, "%USER%", user.Username)
header = strings.ReplaceAll(header, "%DATETIME%", time.Now().Format(time.DateTime)) header = strings.ReplaceAll(header, "%DATETIME%", time.Now().Format(time.DateTime))
os.MkdirAll(ttbp.PathUserConfig, 0700) os.MkdirAll(ttbp.PathUserConfig, 0755)
os.MkdirAll(ttbp.PathUserEntries, 0700) os.MkdirAll(ttbp.PathUserEntries, 0700)
os.WriteFile(path.Join(ttbp.PathUserConfig, "header.txt"), []byte(header), 0644) os.WriteFile(path.Join(ttbp.PathUserConfig, "header.txt"), []byte(header), 0644)
os.WriteFile(path.Join(ttbp.PathUserConfig, "footer.txt"), footer, 0644) os.WriteFile(path.Join(ttbp.PathUserConfig, "footer.txt"), footer, 0644)
@ -53,7 +53,7 @@ func main() {
initializePrompt() initializePrompt()
} }
state, err := ui.New(app.NewMainMenu()) state, err := ui.New(app.NewMainMenu(0))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -13,28 +13,30 @@ import (
"time" "time"
"git.tilde.town/nbsp/neofeels/config" "git.tilde.town/nbsp/neofeels/config"
"github.com/djherbis/times"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
) )
var ( var (
PathVar = "/var/global/ttbp" PathVar = "/var/global/ttbp"
PathVarWWW = path.Join(PathVar, "www") PathVarWWW = path.Join(PathVar, "www")
PathLive = "https://tilde.town/~" PathLive = "https://tilde.town/~"
PathUserFile = path.Join(PathVar, "users.txt") PathUserFile = path.Join(PathVar, "users.txt")
PathGraff = path.Join(PathVar, "graffiti") PathGraff = path.Join(PathVar, "graffiti")
PathWall = path.Join(PathGraff, "wall.txt") PathWall = path.Join(PathGraff, "wall.txt")
PathWallLock = path.Join(PathGraff, ".lock") PathWallLock = path.Join(PathGraff, ".lock")
PathUser = os.Getenv("HOME") PathUser = os.Getenv("HOME")
PathUserFeels = path.Join(PathUser, ".ttbp") PathUserFeels = path.Join(PathUser, ".ttbp")
PathUserHTML = path.Join(PathUser, "public_html") PathUserHTML = path.Join(PathUser, "public_html")
PathUserConfig = path.Join(PathUserFeels, "config") PathUserConfig = path.Join(PathUserFeels, "config")
PathUserEntries = path.Join(PathUserFeels, "entries") PathUserEntries = path.Join(PathUserFeels, "entries")
PathUserBuried = path.Join(PathUserFeels, "buried") PathUserBuried = path.Join(PathUserFeels, "buried")
PathUserBackups = path.Join(PathUserFeels, "backups") PathUserBackups = path.Join(PathUserFeels, "backups")
PathUserWWW = path.Join(PathUserFeels, "www") PathUserWWW = path.Join(PathUserFeels, "www")
PathUserRc = path.Join(PathUserConfig, "ttbprc") PathUserRc = path.Join(PathUserConfig, "ttbprc")
PathUserNopub = path.Join(PathUserConfig, "nopub") PathUserNopub = path.Join(PathUserConfig, "nopub")
PathUserSubs = path.Join(PathUserConfig, "subs") PathUserHTMLRender = path.Join(PathUserConfig, "html")
PathUserSubs = path.Join(PathUserConfig, "subs")
) )
type User struct { type User struct {
@ -64,17 +66,24 @@ func GetUsers() (users []User) {
} }
// get last published file // 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 { if err != nil {
continue continue
} }
var lastPublished time.Time = *new(time.Time) var lastPublished time.Time = *new(time.Time)
if len(entries) > 0 { 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 { if err != nil {
continue continue
} }
lastPublished = info.ModTime() defer file.Close()
timespec, err := times.StatFile(file)
if err != nil {
continue
}
lastPublished = timespec.BirthTime()
} }
users = append(users, User{ users = append(users, User{
@ -103,6 +112,7 @@ type Post struct {
Words int Words int
Author string Author string
Nopub bool Nopub bool
HTML bool
} }
func GetPostsForUser(user string) (posts []Post) { func GetPostsForUser(user string) (posts []Post) {
@ -110,6 +120,7 @@ func GetPostsForUser(user string) (posts []Post) {
if err != nil { if err != nil {
return return
} }
nopubFile, err := os.OpenFile(PathUserNopub, os.O_RDONLY|os.O_CREATE, 0644) nopubFile, err := os.OpenFile(PathUserNopub, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil { if err != nil {
return return
@ -120,6 +131,18 @@ func GetPostsForUser(user string) (posts []Post) {
for nopubScanner.Scan() { for nopubScanner.Scan() {
nopubs = append(nopubs, nopubScanner.Text()) 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 { for _, post := range postFiles {
// retrieve date of file // retrieve date of file
// assume file ends in .txt // assume file ends in .txt
@ -140,7 +163,7 @@ func GetPostsForUser(user string) (posts []Post) {
} }
// get modtime of file // get modtime of file
stat, err := file.Stat() timespec, err := times.StatFile(file)
if err != nil { if err != nil {
continue 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{ posts = append([]Post{Post{
Author: user, Author: user,
Date: fileDate, Date: fileDate,
LastEdited: stat.ModTime(), LastEdited: timespec.BirthTime(),
Words: count, Words: count,
Nopub: nopub, Nopub: nopub,
HTML: html,
}}, posts...) }}, posts...)
} }
} }
@ -302,15 +335,82 @@ func ToggleNopub(t time.Time) {
writer.Flush() 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() { func Publish() {
cfg, err := config.Read() cfg, err := config.Read()
if err != nil { if err != nil {
return // TODO: expose this error to the user return // TODO: expose this error to the user
} }
if cfg.Publishing { if cfg.Publishing {
os.RemoveAll(PathUserWWW) // remove all post and start over
if _, err := os.Stat(PathUserWWW); os.IsNotExist(err) { if _, err := os.Stat(PathUserWWW); os.IsNotExist(err) {
os.MkdirAll(PathUserWWW, 0700) os.MkdirAll(PathUserWWW, 0700)
os.Symlink(path.Join(PathUserConfig, "style.css"), path.Join(PathUserWWW, "style.css")) 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")) file, err := os.Create(path.Join(PathUserWWW, "index.html"))
defer file.Close() defer file.Close()

View File

@ -17,9 +17,10 @@ type List struct {
offset int offset int
} }
func NewList(items []string) List { func NewList(items []string, index int) List {
return List{ return List{
items: items, items: items,
index: index,
} }
} }

View File

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