You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

503 lines
11 KiB

package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/atotto/clipboard"
"github.com/gdamore/tcell/v2"
"github.com/gen2brain/beeep"
"github.com/icza/gox/timex"
"github.com/mattn/go-mastodon"
"github.com/microcosm-cc/bluemonday"
"github.com/rivo/tview"
"golang.org/x/net/html"
)
type URL struct {
Text string
URL string
Classes []string
}
//Runs commands prefixed !CMD!
func CmdToString(cmd string) (string, error) {
cmd = strings.TrimPrefix(cmd, "!CMD!")
parts := strings.Split(cmd, " ")
s, err := exec.Command(parts[0], parts[1:]...).CombinedOutput()
return string(s), err
}
func getURLs(text string) []URL {
doc := html.NewTokenizer(strings.NewReader(text))
var urls []URL
for {
n := doc.Next()
switch n {
case html.ErrorToken:
return urls
case html.StartTagToken:
token := doc.Token()
if token.Data == "a" {
url := URL{}
var appendUrl = true
for _, a := range token.Attr {
switch a.Key {
case "href":
url.URL = a.Val
url.Text = a.Val
case "class":
url.Classes = strings.Split(a.Val, " ")
if strings.Contains(a.Val, "hashtag") {
appendUrl = false
}
}
}
if appendUrl {
urls = append(urls, url)
}
}
}
}
}
func cleanTootHTML(content string) (string, []URL) {
stripped := bluemonday.NewPolicy().AllowElements("p", "br").AllowAttrs("href", "class").OnElements("a").Sanitize(content)
urls := getURLs(stripped)
stripped = bluemonday.NewPolicy().AllowElements("p", "br").Sanitize(content)
stripped = strings.ReplaceAll(stripped, "<br>", "\n")
stripped = strings.ReplaceAll(stripped, "<br/>", "\n")
stripped = strings.ReplaceAll(stripped, "<p>", "")
stripped = strings.ReplaceAll(stripped, "</p>", "\n\n")
stripped = strings.TrimSpace(stripped)
stripped = html.UnescapeString(stripped)
return stripped, urls
}
func openEditor(app *tview.Application, content string) (string, error) {
editor, exists := os.LookupEnv("EDITOR")
if !exists || editor == "" {
editor = "vi"
}
args := []string{}
parts := strings.Split(editor, " ")
if len(parts) > 1 {
args = append(args, parts[1:]...)
editor = parts[0]
}
f, err := ioutil.TempFile("", "tut")
if err != nil {
return "", err
}
if content != "" {
_, err = f.WriteString(content)
if err != nil {
return "", err
}
}
args = append(args, f.Name())
cmd := exec.Command(editor, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
var text []byte
app.Suspend(func() {
err = cmd.Run()
if err != nil {
log.Fatalln(err)
}
f.Seek(0, 0)
text, err = ioutil.ReadAll(f)
})
f.Close()
if err != nil {
return "", err
}
return strings.TrimSpace(string(text)), nil
}
func copyToClipboard(text string) bool {
if clipboard.Unsupported {
return false
}
clipboard.WriteAll(text)
return true
}
func openCustom(app *tview.Application, program string, args []string, terminal bool, url string) {
args = append(args, url)
if terminal {
openInTerminal(app, program, args...)
} else {
exec.Command(program, args...).Start()
}
}
func openURL(app *tview.Application, conf MediaConfig, pc OpenPatternConfig, url string) {
for _, m := range pc.Patterns {
if m.Compiled.Match(url) {
args := append(m.Args, url)
if m.Terminal {
openInTerminal(app, m.Program, args...)
} else {
exec.Command(m.Program, args...).Start()
}
return
}
}
args := append(conf.LinkArgs, url)
if conf.LinkTerminal {
openInTerminal(app, conf.LinkViewer, args...)
} else {
exec.Command(conf.LinkViewer, args...).Start()
}
}
func reverseFiles(filenames []string) []string {
if len(filenames) == 0 {
return filenames
}
var f []string
for i := len(filenames) - 1; i >= 0; i-- {
f = append(f, filenames[i])
}
return f
}
type runProgram struct {
Name string
Args []string
Terminal bool
}
func newRunProgram(name string, args ...string) runProgram {
return runProgram{
Name: name,
Args: args,
}
}
func openMediaType(app *tview.Application, conf MediaConfig, filenames []string, mediaType string) {
terminal := []runProgram{}
external := []runProgram{}
switch mediaType {
case "image":
if conf.ImageReverse {
filenames = reverseFiles(filenames)
}
if conf.ImageSingle {
for _, f := range filenames {
args := append(conf.ImageArgs, f)
c := newRunProgram(conf.ImageViewer, args...)
if conf.ImageTerminal {
terminal = append(terminal, c)
} else {
external = append(external, c)
}
}
} else {
args := append(conf.ImageArgs, filenames...)
c := newRunProgram(conf.ImageViewer, args...)
if conf.ImageTerminal {
terminal = append(terminal, c)
} else {
external = append(external, c)
}
}
case "video", "gifv":
if conf.VideoReverse {
filenames = reverseFiles(filenames)
}
if conf.VideoSingle {
for _, f := range filenames {
args := append(conf.VideoArgs, f)
c := newRunProgram(conf.VideoViewer, args...)
if conf.VideoTerminal {
terminal = append(terminal, c)
} else {
external = append(external, c)
}
}
} else {
args := append(conf.VideoArgs, filenames...)
c := newRunProgram(conf.VideoViewer, args...)
if conf.VideoTerminal {
terminal = append(terminal, c)
} else {
external = append(external, c)
}
}
case "audio":
if conf.AudioReverse {
filenames = reverseFiles(filenames)
}
if conf.AudioSingle {
for _, f := range filenames {
args := append(conf.AudioArgs, f)
c := newRunProgram(conf.AudioViewer, args...)
if conf.AudioTerminal {
terminal = append(terminal, c)
} else {
external = append(external, c)
}
}
} else {
args := append(conf.AudioArgs, filenames...)
c := newRunProgram(conf.AudioViewer, args...)
if conf.AudioTerminal {
terminal = append(terminal, c)
} else {
external = append(external, c)
}
}
}
go func() {
for _, ext := range external {
exec.Command(ext.Name, ext.Args...).Run()
}
}()
for _, term := range terminal {
openInTerminal(app, term.Name, term.Args...)
}
}
func openInTerminal(app *tview.Application, command string, args ...string) error {
cmd := exec.Command(command, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
var err error
app.Suspend(func() {
err = cmd.Run()
if err != nil {
log.Fatalln(err)
}
})
return err
}
func downloadFile(url string) (string, error) {
f, err := ioutil.TempFile("", "tutfile")
if err != nil {
return "", err
}
defer f.Close()
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
_, err = io.Copy(f, resp.Body)
if err != nil {
return "", nil
}
return f.Name(), nil
}
func getConfigDir() string {
home, _ := os.LookupEnv("HOME")
xdgConfig, exists := os.LookupEnv("XDG_CONFIG_HOME")
if !exists {
xdgConfig = home + "/.config"
}
xdgConfig += "/tut"
return xdgConfig
}
func testConfigPath(name string) (string, error) {
xdgConfig := getConfigDir()
path := xdgConfig + "/" + name
_, err := os.Stat(path)
if os.IsNotExist(err) {
return "", err
}
if err != nil {
return "", err
}
return path, nil
}
func GetAccountsPath() (string, error) {
return testConfigPath("accounts.yaml")
}
func GetConfigPath() (string, error) {
return testConfigPath("config.ini")
}
func CheckPath(input string, inclHidden bool) (string, bool) {
info, err := os.Stat(input)
if err != nil {
return "", false
}
if !inclHidden && strings.HasPrefix(info.Name(), ".") {
return "", false
}
if info.IsDir() {
if input == "/" {
return input, true
}
return input + "/", true
}
return input, true
}
func IsDir(input string) bool {
info, err := os.Stat(input)
if err != nil {
return false
}
return info.IsDir()
}
func FindFiles(s string) []string {
input := filepath.Clean(s)
if len(s) > 2 && s[len(s)-2:] == "/." {
input += "/."
}
var files []string
path, exists := CheckPath(input, true)
if exists {
files = append(files, path)
}
base := filepath.Base(input)
inclHidden := strings.HasPrefix(base, ".") || (len(input) > 1 && input[len(input)-2:] == "/.")
matches, _ := filepath.Glob(input + "*")
if strings.HasSuffix(path, "/") {
matchesDir, _ := filepath.Glob(path + "*")
matches = append(matches, matchesDir...)
}
for _, f := range matches {
p, exists := CheckPath(f, inclHidden)
if exists && p != path {
files = append(files, p)
}
}
return files
}
func ColorKey(c *Config, pre, key, end string) string {
color := ColorMark(c.Style.TextSpecial2)
normal := ColorMark(c.Style.Text)
key = TextFlags("b") + key + TextFlags("-")
if c.General.ShortHints {
pre = ""
end = ""
}
text := fmt.Sprintf("%s%s%s%s%s%s", normal, pre, color, key, normal, end)
return text
}
func TextFlags(s string) string {
return fmt.Sprintf("[::%s]", s)
}
func ColorMark(color tcell.Color) string {
return fmt.Sprintf("[#%06x]", color.Hex())
}
func FormatUsername(a mastodon.Account) string {
if a.DisplayName != "" {
return fmt.Sprintf("%s (%s)", a.DisplayName, a.Acct)
}
return a.Acct
}
func SublteText(style StyleConfig, text string) string {
subtle := ColorMark(style.Subtle)
return fmt.Sprintf("%s%s", subtle, text)
}
func FloorDate(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
func OutputDate(status time.Time, today time.Time, long, short string, relativeDate int) string {
ty, tm, td := today.Date()
sy, sm, sd := status.Date()
format := long
sameDay := false
displayRelative := false
if ty == sy && tm == sm && td == sd {
format = short
sameDay = true
}
todayFloor := FloorDate(today)
statusFloor := FloorDate(status)
if relativeDate > -1 && !sameDay {
days := int(todayFloor.Sub(statusFloor).Hours() / 24)
if relativeDate == 0 || days <= relativeDate {
displayRelative = true
}
}
var dateOutput string
if displayRelative {
y, m, d, _, _, _ := timex.Diff(statusFloor, todayFloor)
if y > 0 {
dateOutput = fmt.Sprintf("%s%dy", dateOutput, y)
}
if dateOutput != "" || m > 0 {
dateOutput = fmt.Sprintf("%s%dm", dateOutput, m)
}
if dateOutput != "" || d > 0 {
dateOutput = fmt.Sprintf("%s%dd", dateOutput, d)
}
} else {
dateOutput = status.Format(format)
}
return dateOutput
}
func Notify(nc NotificationConfig, t NotificationType, title string, body string) {
switch t {
case NotificationFollower:
if !nc.NotificationFollower {
return
}
case NotificationFavorite:
if !nc.NotificationFavorite {
return
}
case NotificationMention:
if !nc.NotificationMention {
return
}
case NotificationBoost:
if !nc.NotificationBoost {
return
}
case NotificationPoll:
if !nc.NotificationPoll {
return
}
case NotificationPost:
if !nc.NotificationPost {
return
}
default:
return
}
beeep.Notify(title, body, "")
}