diff --git a/internal/net/gphttp/middleware/middlewares.go b/internal/net/gphttp/middleware/middlewares.go
index 0e98bc4..b9295f3 100644
--- a/internal/net/gphttp/middleware/middlewares.go
+++ b/internal/net/gphttp/middleware/middlewares.go
@@ -25,6 +25,7 @@ var allMiddlewares = map[string]*Middleware{
"hidexforwarded": HideXForwarded,
"modifyhtml": ModifyHTML,
+ "themed": Themed,
"errorpage": CustomErrorPage,
"customerrorpage": CustomErrorPage,
diff --git a/internal/net/gphttp/middleware/themed.go b/internal/net/gphttp/middleware/themed.go
new file mode 100644
index 0000000..175c851
--- /dev/null
+++ b/internal/net/gphttp/middleware/themed.go
@@ -0,0 +1,113 @@
+package middleware
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "text/template"
+
+ _ "embed"
+
+ "github.com/yusing/go-proxy/internal/gperr"
+)
+
+type themed struct {
+ FontURL string `json:"font_url"`
+ FontFamily string `json:"font_family"`
+ Theme Theme `json:"theme"` // predefined themes
+ CSS string `json:"css"` // css url or content
+
+ m modifyHTML
+}
+
+var Themed = NewMiddleware[themed]()
+
+type Theme string
+
+const (
+ DarkTheme Theme = "dark"
+ DarkGreyTheme Theme = "dark-grey"
+ SolarizedDarkTheme Theme = "solarized-dark"
+)
+
+var (
+ //go:embed themes/dark.css
+ darkModeCSS string
+ //go:embed themes/dark-grey.css
+ darkGreyModeCSS string
+ //go:embed themes/solarized-dark.css
+ solarizedDarkModeCSS string
+ //go:embed themes/font.css
+ fontCSS string
+)
+
+var fontCSSTemplate = template.Must(template.New("fontCSS").Parse(fontCSS))
+
+const overAllocate = 256
+
+func (m *themed) before(w http.ResponseWriter, req *http.Request) bool {
+ return m.m.before(w, req)
+}
+
+func (m *themed) modifyResponse(resp *http.Response) error {
+ return m.m.modifyResponse(resp)
+}
+
+func (m *themed) finalize() error {
+ m.m.Target = "body"
+ if m.FontURL != "" && m.FontFamily != "" {
+ buf := bytes.NewBuffer(bytePool.GetSized(len(fontCSS) + overAllocate))
+ buf.WriteString(`")
+ m.m.HTML += buf.String()
+ }
+ if m.CSS != "" && m.Theme != "" {
+ return gperr.New("css and theme are mutually exclusive")
+ }
+ // credit: https://hackcss.egoist.dev
+ if m.Theme != "" {
+ switch m.Theme {
+ case DarkTheme:
+ m.m.HTML += wrapStyleTag(darkModeCSS)
+ case DarkGreyTheme:
+ m.m.HTML += wrapStyleTag(darkGreyModeCSS)
+ case SolarizedDarkTheme:
+ m.m.HTML += wrapStyleTag(solarizedDarkModeCSS)
+ default:
+ return gperr.New("invalid theme").Subject(string(m.Theme))
+ }
+ }
+ if m.CSS != "" {
+ switch {
+ case strings.HasPrefix(m.CSS, "https://"),
+ strings.HasPrefix(m.CSS, "http://"),
+ strings.HasPrefix(m.CSS, "/"):
+ m.m.HTML += wrapStylesheetLinkTag(m.CSS)
+ case strings.HasPrefix(m.CSS, "file://"):
+ css, err := os.ReadFile(strings.TrimPrefix(m.CSS, "file://"))
+ if err != nil {
+ return err
+ }
+ m.m.HTML += wrapStyleTag(string(css))
+ default:
+ // inline css
+ m.m.HTML += wrapStyleTag(m.CSS)
+ }
+ }
+ return nil
+}
+
+func wrapStyleTag(css string) string {
+ return fmt.Sprintf(``, css)
+}
+
+func wrapStylesheetLinkTag(href string) string {
+ return fmt.Sprintf(``, href)
+}
diff --git a/internal/net/gphttp/middleware/themes/dark-grey.css b/internal/net/gphttp/middleware/themes/dark-grey.css
new file mode 100644
index 0000000..74a8df4
--- /dev/null
+++ b/internal/net/gphttp/middleware/themes/dark-grey.css
@@ -0,0 +1,116 @@
+:not(img, svg) {
+ background-color: #181818 !important;
+ color: #ccc !important;
+}
+pre {
+ background-color: #181818 !important;
+ padding: 0 !important;
+ border: none !important;
+}
+pre code {
+ color: #00bcd4 !important;
+}
+h1 a,
+h2 a,
+h3 a,
+h4 a,
+h5 a {
+ color: #ccc !important;
+}
+code,
+strong {
+ color: #fff !important;
+}
+code {
+ font-weight: 100;
+}
+table {
+ color: #ccc !important;
+}
+table td,
+table th {
+ border-color: #444 !important;
+}
+table tbody td:first-child {
+ color: #fff !important;
+}
+.form-group label {
+ color: #ccc !important;
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.form-group.form-textarea label:after {
+ background-color: #181818 !important;
+}
+.form-control {
+ color: #ccc !important;
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.form-control:focus {
+ border-color: #ccc !important;
+ color: #ccc !important;
+}
+textarea.form-control {
+ color: #ccc !important;
+}
+.card {
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.card .card-header {
+ background-color: transparent !important;
+ color: #ccc !important;
+ border-bottom: 1px solid rgba(95, 95, 95, 0.78) !important;
+}
+.btn.btn-ghost.btn-default {
+ border-color: #ababab !important;
+ color: #ababab !important;
+}
+.btn.btn-ghost.btn-default:focus,
+.btn.btn-ghost.btn-default:hover {
+ border-color: #9c9c9c !important;
+ color: #9c9c9c !important;
+ z-index: 1;
+}
+.btn.btn-ghost.btn-default:focus,
+.btn.btn-ghost.btn-default:hover {
+ border-color: #e0e0e0 !important;
+ color: #e0e0e0 !important;
+}
+.btn.btn-ghost.btn-primary:focus,
+.btn.btn-ghost.btn-primary:hover {
+ border-color: #64b5f6 !important;
+ color: #64b5f6 !important;
+}
+.btn.btn-ghost.btn-success:focus,
+.btn.btn-ghost.btn-success:hover {
+ border-color: #81c784 !important;
+ color: #81c784 !important;
+}
+.btn.btn-ghost.btn-info:focus,
+.btn.btn-ghost.btn-info:hover {
+ border-color: #4dd0e1 !important;
+ color: #4dd0e1 !important;
+}
+.btn.btn-ghost.btn-error:focus,
+.btn.btn-ghost.btn-error:hover {
+ border-color: #e57373 !important;
+ color: #e57373 !important;
+}
+.btn.btn-ghost.btn-warning:focus,
+.btn.btn-ghost.btn-warning:hover {
+ border-color: #ffb74d !important;
+ color: #ffb74d !important;
+}
+.avatarholder,
+.placeholder {
+ background-color: transparent !important;
+ border-color: #333 !important;
+}
+.menu .menu-item {
+ color: #ccc !important;
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.menu .menu-item.active,
+.menu .menu-item:hover {
+ color: #fff !important;
+ border-color: #ccc !important;
+}
diff --git a/internal/net/gphttp/middleware/themes/dark.css b/internal/net/gphttp/middleware/themes/dark.css
new file mode 100644
index 0000000..789350d
--- /dev/null
+++ b/internal/net/gphttp/middleware/themes/dark.css
@@ -0,0 +1,116 @@
+:not(img, svg) {
+ background-color: #000 !important;
+ color: #ccc !important;
+}
+pre {
+ background-color: #000 !important;
+ padding: 0;
+ border: none;
+}
+pre code {
+ color: #00bcd4 !important;
+}
+h1 a,
+h2 a,
+h3 a,
+h4 a,
+h5 a {
+ color: #ccc !important;
+}
+code,
+strong {
+ color: #fff !important;
+}
+code {
+ font-weight: 100;
+}
+table {
+ color: #ccc !important;
+}
+table td,
+table th {
+ border-color: #444 !important;
+}
+table tbody td:first-child {
+ color: #fff !important;
+}
+.form-group label {
+ color: #ccc !important;
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.form-group.form-textarea label:after {
+ background-color: #000 !important;
+}
+.form-control {
+ color: #ccc !important;
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.form-control:focus {
+ border-color: #ccc !important;
+ color: #ccc !important;
+}
+textarea.form-control {
+ color: #ccc !important;
+}
+.card {
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.card .card-header {
+ background-color: transparent !important;
+ color: #ccc !important;
+ border-bottom: 1px solid rgba(95, 95, 95, 0.78) !important;
+}
+.btn.btn-ghost.btn-default {
+ border-color: #ababab !important;
+ color: #ababab !important;
+}
+.btn.btn-ghost.btn-default:focus,
+.btn.btn-ghost.btn-default:hover {
+ border-color: #9c9c9c !important;
+ color: #9c9c9c !important;
+ z-index: 1;
+}
+.btn.btn-ghost.btn-default:focus,
+.btn.btn-ghost.btn-default:hover {
+ border-color: #e0e0e0 !important;
+ color: #e0e0e0 !important;
+}
+.btn.btn-ghost.btn-primary:focus,
+.btn.btn-ghost.btn-primary:hover {
+ border-color: #64b5f6 !important;
+ color: #64b5f6 !important;
+}
+.btn.btn-ghost.btn-success:focus,
+.btn.btn-ghost.btn-success:hover {
+ border-color: #81c784 !important;
+ color: #81c784 !important;
+}
+.btn.btn-ghost.btn-info:focus,
+.btn.btn-ghost.btn-info:hover {
+ border-color: #4dd0e1 !important;
+ color: #4dd0e1 !important;
+}
+.btn.btn-ghost.btn-error:focus,
+.btn.btn-ghost.btn-error:hover {
+ border-color: #e57373 !important;
+ color: #e57373 !important;
+}
+.btn.btn-ghost.btn-warning:focus,
+.btn.btn-ghost.btn-warning:hover {
+ border-color: #ffb74d !important;
+ color: #ffb74d !important;
+}
+.avatarholder,
+.placeholder {
+ background-color: transparent !important;
+ border-color: #333 !important;
+}
+.menu .menu-item {
+ color: #ccc !important;
+ border-color: rgba(95, 95, 95, 0.78) !important;
+}
+.menu .menu-item.active,
+.menu .menu-item:hover {
+ color: #fff !important;
+ border-color: #ccc !important;
+}
diff --git a/internal/net/gphttp/middleware/themes/font.css b/internal/net/gphttp/middleware/themes/font.css
new file mode 100644
index 0000000..31006b9
--- /dev/null
+++ b/internal/net/gphttp/middleware/themes/font.css
@@ -0,0 +1,4 @@
+@import url("{{.FontURL}}");
+body {
+ font-family: "{{.FontFamily}}", "arial", "roboto" !important;
+}
diff --git a/internal/net/gphttp/middleware/themes/solarized-dark.css b/internal/net/gphttp/middleware/themes/solarized-dark.css
new file mode 100644
index 0000000..904d236
--- /dev/null
+++ b/internal/net/gphttp/middleware/themes/solarized-dark.css
@@ -0,0 +1,149 @@
+:not(img, svg) {
+ background-color: #073642 !important;
+ color: #78909c !important;
+}
+pre {
+ background-color: #073642 !important;
+ padding: 0;
+ border: none;
+}
+pre code {
+ color: #009688 !important;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: #1e88e5 !important;
+}
+h1 a,
+h2 a,
+h3 a,
+h4 a,
+h5 a,
+h6 a {
+ color: #1e88e5 !important;
+ border-bottom-color: #1e88e5 !important;
+}
+h1 a:hover,
+h2 a:hover,
+h3 a:hover,
+h4 a:hover,
+h5 a:hover,
+h6 a:hover {
+ background-color: #1e88e5 !important;
+ color: #fff !important;
+}
+h1 a,
+h2 a,
+h3 a,
+h4 a,
+h5 a {
+ color: #78909c !important;
+}
+code,
+strong {
+ color: #90a4ae !important;
+}
+code {
+ font-weight: 100;
+}
+.progress-bar-filled {
+ background-color: #558b2f !important;
+}
+.progress-bar-filled:after,
+.progress-bar-filled:before {
+ color: #90a4ae !important;
+}
+table {
+ color: #78909c !important;
+}
+table td,
+table th {
+ border-color: #b0bec5 !important;
+}
+table tbody td:first-child {
+ color: #b0bec5 !important;
+}
+.form-group label {
+ color: #78909c !important;
+ border-color: #90a4ae !important;
+}
+.form-group.form-textarea label:after {
+ background-color: #073642 !important;
+}
+.form-control {
+ color: #78909c !important;
+ border-color: #90a4ae !important;
+}
+.form-control:focus {
+ border-color: #cfd8dc !important;
+ color: #cfd8dc !important;
+}
+textarea.form-control {
+ color: #78909c !important;
+}
+.card {
+ border-color: #90a4ae !important;
+}
+.card .card-header {
+ background-color: transparent !important;
+ color: #78909c !important;
+ border-bottom: 1px solid #90a4ae !important;
+}
+.btn.btn-ghost.btn-default {
+ border-color: #607d8b !important;
+ color: #607d8b !important;
+}
+.btn.btn-ghost.btn-default:focus,
+.btn.btn-ghost.btn-default:hover {
+ border-color: #90a4ae !important;
+ color: #90a4ae !important;
+ z-index: 1;
+}
+.btn.btn-ghost.btn-default:focus,
+.btn.btn-ghost.btn-default:hover {
+ border-color: #e0e0e0 !important;
+ color: #e0e0e0 !important;
+}
+.btn.btn-ghost.btn-primary:focus,
+.btn.btn-ghost.btn-primary:hover {
+ border-color: #64b5f6 !important;
+ color: #64b5f6 !important;
+}
+.btn.btn-ghost.btn-success:focus,
+.btn.btn-ghost.btn-success:hover {
+ border-color: #81c784 !important;
+ color: #81c784 !important;
+}
+.btn.btn-ghost.btn-info:focus,
+.btn.btn-ghost.btn-info:hover {
+ border-color: #4dd0e1 !important;
+ color: #4dd0e1 !important;
+}
+.btn.btn-ghost.btn-error:focus,
+.btn.btn-ghost.btn-error:hover {
+ border-color: #e57373 !important;
+ color: #e57373 !important;
+}
+.btn.btn-ghost.btn-warning:focus,
+.btn.btn-ghost.btn-warning:hover {
+ border-color: #ffb74d !important;
+ color: #ffb74d !important;
+}
+.avatarholder,
+.placeholder {
+ background-color: transparent !important;
+ border-color: #90a4ae !important;
+}
+.menu .menu-item {
+ color: #78909c !important;
+ border-color: #90a4ae !important;
+}
+.menu .menu-item.active,
+.menu .menu-item:hover {
+ color: #fff !important;
+ border-color: #78909c !important;
+}