add cover image support (#7)

This commit is contained in:
T.v.Dein
2025-10-19 20:39:04 +02:00
committed by GitHub
parent c2abc4ba4d
commit 2c6e81a2c8
7 changed files with 110 additions and 33 deletions

View File

@@ -32,7 +32,7 @@ import (
)
const (
Version string = `v0.0.4`
Version string = `v0.0.5`
Usage string = `This is epuppy, a terminal ui ebook viewer.
Usage: epuppy [options] <epub file>
@@ -42,6 +42,7 @@ Options:
-s --store-progress remember reading position
-n --line-numbers add line numbers
-c --config <file> use config <file>
-i --cover-image display cover image
-t --txt dump readable content to STDOUT
-x --xml dump source xml to STDOUT
-N --no-color disable colors (or use $NO_COLOR env var)
@@ -63,6 +64,7 @@ type Config struct {
ColorDark ColorSetting `koanf:"colordark"` // comes from config file only
ColorLight ColorSetting `koanf:"colorlight"` // comes from config file only
ShowHelp bool `koanf:"help"`
ShowCover bool `koanf:"cover-image"` // -i
Colors Colors // generated from user config file or internal defaults, respects dark mode
Document string
InitialProgress int // lines
@@ -89,6 +91,7 @@ func InitConfig(output io.Writer) (*Config, error) {
flagset.BoolP("txt", "t", false, "dump readable content to STDOUT")
flagset.BoolP("xml", "x", false, "dump xml to STDOUT")
flagset.BoolP("no-color", "N", false, "disable colors")
flagset.BoolP("cover-image", "i", false, "show cover image")
flagset.BoolP("help", "h", false, "show help")
flagset.StringP("config", "c", "", "read config from file")

View File

@@ -21,10 +21,13 @@ package cmd
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager
import (
"bytes"
"fmt"
"image"
"os"
"strings"
"github.com/blacktop/go-termimg"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
@@ -132,6 +135,7 @@ type Doc struct {
hideUi bool
meta *Meta
config *Config
Cover *termimg.ImageWidget
keys keyMap
help help.Model
@@ -220,7 +224,12 @@ func (m *Doc) Rewrap() {
// wrapping has changed, update viewport and line count
if content != "" {
m.viewport.SetContent(content)
if m.Cover != nil {
cover, _ := m.Cover.Render()
m.viewport.SetContent(cover + "\n\n" + content)
} else {
m.viewport.SetContent(content)
}
m.meta.lines = len(strings.Split(content, "\n"))
}
@@ -270,7 +279,15 @@ func max(a, b int) int {
return b
}
func Pager(conf *Config, title, message string) (int, error) {
type Ebook struct {
Config *Config
Title string
Body string
Cover []byte
MediaType string
}
func Pager(book *Ebook) (int, error) {
width := 80
scrollto := 0
@@ -281,32 +298,44 @@ func Pager(conf *Config, title, message string) (int, error) {
}
}
if conf.StoreProgress {
scrollto = conf.InitialProgress
if book.Config.StoreProgress {
scrollto = book.Config.InitialProgress
}
if conf.LineNumbers {
if book.Config.LineNumbers {
catn := ""
for idx, line := range strings.Split(message, "\n") {
for idx, line := range strings.Split(book.Body, "\n") {
catn += fmt.Sprintf("%4d: %s\n", idx, line)
}
message = catn
book.Body = catn
}
meta := Meta{
initialprogress: scrollto,
lines: len(strings.Split(message, "\n")),
lines: len(strings.Split(book.Body, "\n")),
}
doc := Doc{
content: book.Body,
title: book.Title,
initialwidth: width,
meta: &meta,
config: book.Config,
keys: keys,
}
if book.MediaType != "" && book.Config.ShowCover {
img, _, err := image.Decode(bytes.NewReader(book.Cover))
if err != nil {
return 0, fmt.Errorf("failed to load cover image: %w", err)
}
doc.Cover = termimg.NewImageWidgetFromImage(img)
doc.Cover.SetSize(width, width).SetProtocol(termimg.Auto)
}
p := tea.NewProgram(
Doc{
content: message,
title: title,
initialwidth: width,
meta: &meta,
config: conf,
keys: keys,
},
doc,
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
)
@@ -315,7 +344,7 @@ func Pager(conf *Config, title, message string) (int, error) {
return 0, fmt.Errorf("could not run pager: %w", err)
}
if conf.Debug {
if book.Config.Debug {
fmt.Printf("scrollto: %d, last: %d, diff: %d\n",
scrollto, meta.currentline, scrollto-meta.currentline)
}

View File

@@ -44,7 +44,11 @@ func ViewText(conf *Config) (int, error) {
return fmt.Println(string(data))
}
return Pager(conf, conf.Document, string(data))
return Pager(&Ebook{
Config: conf,
Title: conf.Document,
Body: string(data),
})
}
func ViewEpub(conf *Config) (int, error) {
@@ -74,11 +78,15 @@ func ViewEpub(conf *Config) (int, error) {
return fmt.Println(buf.String())
}
return Pager(conf, head.String(), buf.String())
return Pager(&Ebook{
Config: conf,
Title: head.String(),
Body: buf.String(),
Cover: book.CoverImage,
MediaType: book.CoverMediaType,
})
}
// FIXME: since the switch to book.Files() in epub.Open() this
// returns invalid chapter numbering
func fetchByContent(conf *Config, buf *strings.Builder, book *epub.Book) bool {
chapter := 1
var gotbody bool

7
go.mod
View File

@@ -41,14 +41,21 @@ require (
require (
github.com/antchfx/xpath v1.3.5 // indirect
github.com/blacktop/go-termimg v0.1.20 // indirect
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/makeworld-the-better-one/dither/v2 v2.4.0 // indirect
github.com/mattn/go-sixel v0.0.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/soniakeys/quant v1.0.0 // indirect
golang.org/x/image v0.25.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/tools v0.37.0 // indirect
)

19
go.sum
View File

@@ -8,6 +8,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/blacktop/go-termimg v0.1.20 h1:+EAUc3c9hwE/fUYaqRV1BSLvAlOuLySgLTEBzxGbYK4=
github.com/blacktop/go-termimg v0.1.20/go.mod h1:nwxrOjfFcBjtS358oIGBLfscSLnCpNdRlMVRxsnZwMU=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
@@ -22,8 +24,11 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff h1:OVBKPzoa0k5ZVMoor27BReRZxER1IEDtLHXkRjaHElg=
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:5qLP4S++M5quSc/xbvWWW8vKkgKwOqOT/IVhAas26XI=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -47,6 +52,8 @@ github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -54,6 +61,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sixel v0.0.5 h1:55w2FR5ncuhKhXrM5ly1eiqMQfZsnAHIpYNGZX03Cv8=
github.com/mattn/go-sixel v0.0.5/go.mod h1:h2Sss+DiUEHy0pUqcIB6PFXo5Cy8sTQEFr3a9/5ZLNw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -66,6 +75,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -76,8 +87,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y=
github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -91,6 +106,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -161,5 +178,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -11,12 +11,15 @@ import (
// Book epub book
type Book struct {
Ncx Ncx `json:"ncx"`
Opf Opf `json:"opf"`
Container Container `json:"-"`
Mimetype string `json:"-"`
Content []Content
fd *zip.ReadCloser
Ncx Ncx `json:"ncx"`
Opf Opf `json:"opf"`
Container Container `json:"-"`
Mimetype string `json:"-"`
Content []Content
fd *zip.ReadCloser
CoverImage []byte
CoverFile string
CoverMediaType string
}
// Open open resource file

View File

@@ -45,8 +45,11 @@ func Open(fn string, dumpxml bool) (*Book, error) {
if err != nil {
return &bk, err
}
}
break
if mf.ID == "cover-image" {
bk.CoverFile = mf.Href
bk.CoverMediaType = mf.MediaType
}
}
@@ -61,13 +64,18 @@ func Open(fn string, dumpxml bool) (*Book, error) {
if err := ct.String(content); err != nil {
return &bk, err
}
bk.Content = append(bk.Content, ct)
if dumpxml {
fmt.Println(string(ct.XML))
}
}
bk.Content = append(bk.Content, ct)
if dumpxml {
fmt.Println(string(ct.XML))
if strings.Contains(file, bk.CoverFile) {
bk.CoverImage = content
}
}
if dumpxml {