10 Commits

Author SHA1 Message Date
2c2dc57453 WIP broken stage 2026-01-06 15:40:30 -05:00
85bd564164 release 0.3.0 2026-01-04 16:27:49 -05:00
0a0b1fa8c3 large changes 2026-01-04 16:27:39 -05:00
608a083861 patch: fix the back option for archives 2026-01-03 16:51:16 -05:00
50a45b6a82 update archive example 2026-01-03 16:50:33 -05:00
19752a0c89 change logging format 2026-01-03 09:53:07 -05:00
5192919645 update gitignore 2026-01-02 10:49:04 -05:00
f763f57001 rewrite Dockerfile 2026-01-02 10:06:27 -05:00
629fd06be0 exit beta 2026-01-01 23:06:30 -05:00
bedcfe781d updated logging 2026-01-01 23:05:29 -05:00
42 changed files with 440 additions and 180 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
fes fes
*.tar.gz
/stuff/

View File

@@ -4,24 +4,17 @@ WORKDIR /src
RUN apk add --no-cache git build-base RUN apk add --no-cache git build-base
COPY go.mod go.sum ./
RUN go mod download
COPY . . COPY . .
ENV CGO_ENABLED=0 RUN make
ENV GOOS=linux
ENV GOARCH=amd64
RUN go build -ldflags="-X fes/modules/version.gitCommit=$(git rev-parse --short HEAD) -s -w" -o fes FROM alpine:3.19
FROM scratch COPY --from=builder /src/fes /usr/local/bin/fes
COPY --from=builder /src/fes /fes
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 3000
ENTRYPOINT ["/fes"] ENTRYPOINT ["/usr/local/bin/fes"]
CMD ["run", "/app"] CMD ["run", "/app"]

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,22 @@
Pinnipeds [2] are the seals and their relatives, a group of semi-aquatic marine
mammals. The Pinnipedia is in the Order Carnivora. There are three seal
families: Odobenidae (walruses), Otariidae (eared seals, including sea lions
and fur seals), and Phocidae (true seals).[3]
Seals are sleek-bodied and barrel-shaped. Their bodies are well adapted to the
aquatic habitat where they spend most of their lives. Pinnipeds have flippers
for hands, big bulky bodies, doggish faces, and big eyes. Unlike cetaceans,
pinnipeds have their noses on their faces, and each nostril of the nose closes
when the pinniped goes underwater. Like cetaceans, pinnipeds have a thick layer
of blubber (fat) just under their skin: this blubber keeps them warm in cold
waters and keeps them fed during times when food is not easily found. When they
cannot find food, they live off the fat in the blubber.
Pinnipeds are carnivorous. This means they eat only meat (such as fish or
squid) and not plants. However, almost all pinnipeds can be eaten by polar
bears, sharks and killer whales.
Seals are often trained in zoos or aquariums to put on shows. However, in
Sweden, it is illegal to train a seal to balance a ball on its nose.[4]
From [Pinniped Wikipedia](https://simple.wikipedia.org/wiki/Pinniped)

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -0,0 +1,5 @@
[app]
name = "extentions"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -0,0 +1,33 @@
# extentions
```
fes new extentions
```
> **Know what you are doing?** Delete this file. Have fun!
## Project Structure
Inside your Fes project, you'll see the following directories and files:
```
.
├── Fes.toml
├── README.md
└── www
└── index.lua
```
Fes looks for `.lua` files in the `www/` directory. Each file is exposed as a route based on its file name.
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `fes run .` | Runs the project at `.` |
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).

View File

@@ -0,0 +1,12 @@
local fes = require("fes")
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev/", "fSD")
site:extend("myext", {
shout = function(self, str) return self:g(str:upper()) end
})
site.myext:shout("hello world")
return site

View File

@@ -1,4 +1,5 @@
local std = require("lib.std") local std = require("lib.std")
local symbol = require("lib.symbol")
local M = {} local M = {}
M.__index = M M.__index = M
@@ -318,11 +319,30 @@ em, i { font-style: italic; }
return setmetatable(self, M) return setmetatable(self, M)
end end
function M:custom(str) function M:g(str)
table.insert(self.parts, str) table.insert(self.parts, str)
return self return self
end end
function M:extend(name, tbl)
if type(name) ~= "string" then
error("First argument to extend must be a string (namespace name)")
end
if type(tbl) ~= "table" then
error("Second argument to extend must be a table of functions")
end
self[name] = {}
for k, v in pairs(tbl) do
if type(v) ~= "function" then
error("Extension values must be functions, got " .. type(v) .. " for key " .. k)
end
self[name][k] = function(...)
return v(self, ...)
end
end
return self
end
for name, func in pairs(std) do for name, func in pairs(std) do
if type(func) == "function" then if type(func) == "function" then
M[name] = function(self, ...) M[name] = function(self, ...)
@@ -340,9 +360,11 @@ function M:build()
header = header:gsub( header = header:gsub(
"{{FAVICON}}", "{{FAVICON}}",
favicon_html favicon_html
or [[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]] or
[[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]]
) )
local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder") local footer = self.footer:gsub("{{COPYRIGHT}}",
self.copyright or symbol.legal.copyright .. "The Copyright Holder")
return header .. table.concat(self.parts, "\n") .. footer return header .. table.concat(self.parts, "\n") .. footer
end end

27
lib/site.lua Normal file
View File

@@ -0,0 +1,27 @@
local M = {}
function M.name()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.name then
return fes_mod.config.site.name
end
return ""
end
function M.version()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.version then
return fes_mod.config.site.version
end
return ""
end
function M.authors()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.authors then
return fes_mod.config.site.authors
end
return {}
end
return M

View File

@@ -1,155 +1,163 @@
local M = {} local M = {}
function M.fes_version() function M.element(tag, attrs, content)
local fes_mod = package.loaded.fes local out = { "<", tag }
if fes_mod and fes_mod.config and fes_mod.config.fes and fes_mod.config.fes.version then
return fes_mod.config.fes.version
end
return ""
end
function M.site_version() if attrs then
local fes_mod = package.loaded.fes for k, v in pairs(attrs) do
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.version then if v ~= false and v ~= nil then
return fes_mod.config.site.version if v == true then
out[#out + 1] = " " .. k
else
out[#out + 1] = " " .. k .. "=\"" .. tostring(v) .. "\""
end
end
end
end end
return ""
if content == nil then
out[#out + 1] = " />"
return table.concat(out)
end
out[#out + 1] = ">"
out[#out + 1] = tostring(content)
out[#out + 1] = "</"
out[#out + 1] = tag
out[#out + 1] = ">"
return table.concat(out)
end end
function M.a(link, str) function M.a(link, str)
link = link or "https://example.com" link = link or "https://example.com"
str = str or link str = str or link
return '<a href="' .. link .. '">' .. str .. "</a>" return M.element("a", { href = link }, str)
end
function M.download(link, str, downloadName)
link = link or "."
str = str or link
return M.element("a", { href = link, download = downloadName }, str)
end end
function M.ha(link, str) function M.ha(link, str)
link = link or "https://example.com" link = link or "https://example.com"
str = str or link str = str or link
return '<a class="hidden" href="' .. link .. '">' .. str .. "</a>" return M.element("a", { href = link, class = "hidden" }, str)
end end
function M.external(link, str) function M.external(link, str)
return '<a target="_blank" href="' .. link .. '">' .. str .. "</a>" return M.element("a", { href = link, target = "_blank" }, str)
end end
function M.note(str) function M.note(str)
return '<div class="note">' .. str .. "</div>" return M.element("div", { class = "note" }, str)
end end
function M.muted(str) function M.muted(str)
return '<div class="muted">' .. str .. "</div>" return M.element("div", { class = "muted" }, str)
end end
function M.callout(str) function M.callout(str)
return '<div class="callout">' .. str .. "</div>" return M.element("div", { class = "callout" }, str)
end end
function M.h1(str) function M.h1(str)
return "<h1>" .. (str or "") .. "</h1>" return M.element("h1", nil, str or "")
end end
function M.h2(str) function M.h2(str)
return "<h2>" .. (str or "") .. "</h2>" return M.element("h2", nil, str or "")
end end
function M.h3(str) function M.h3(str)
return "<h3>" .. (str or "") .. "</h3>" return M.element("h3", nil, str or "")
end end
function M.h4(str) function M.h4(str)
return "<h4>" .. (str or "") .. "</h4>" return M.element("h4", nil, str or "")
end end
function M.h5(str) function M.h5(str)
return "<h5>" .. (str or "") .. "</h5>" return M.element("h5", nil, str or "")
end end
function M.h6(str) function M.h6(str)
return "<h6>" .. (str or "") .. "</h6>" return M.element("h6", nil, str or "")
end end
function M.p(str) function M.p(str)
return "<p>" .. (str or "") .. "</p>" return M.element("p", nil, str or "")
end end
function M.pre(str) function M.pre(str)
return "<pre>" .. (str or "") .. "</pre>" return M.element("pre", nil, str or "")
end end
function M.code(str) function M.code(str)
return "<pre><code>" .. (str or "") .. "</code></pre>" return M.element("pre", nil, M.element("code", nil, str or ""))
end end
function M.ul(items) function M.ul(items)
items = items or {} items = items or {}
local html = "<ul>" local out = {}
for _, item in ipairs(items) do for _, item in ipairs(items) do
html = html .. "<li>" .. tostring(item) .. "</li>" out[#out + 1] = M.element("li", nil, item)
end end
html = html .. "</ul>" return M.element("ul", nil, table.concat(out))
return html
end end
function M.ol(items) function M.ol(items)
items = items or {} items = items or {}
local html = "<ol>" local out = {}
for _, item in ipairs(items) do for _, item in ipairs(items) do
html = html .. "<li>" .. tostring(item) .. "</li>" out[#out + 1] = M.element("li", nil, item)
end end
html = html .. "</ol>" return M.element("ol", nil, table.concat(out))
return html
end end
function M.tl(items) function M.tl(items)
items = items or {} items = items or {}
local html = '<ul class="tl">' local out = {}
for _, item in ipairs(items) do for _, item in ipairs(items) do
html = html .. "<li>" .. tostring(item) .. "</li>" out[#out + 1] = M.element("li", nil, item)
end end
html = html .. "</ul>" return M.element("ul", { class = "tl" }, table.concat(out))
return html
end end
function M.blockquote(str) function M.blockquote(str)
return "<blockquote>" .. (str or "") .. "</blockquote>" return M.element("blockquote", nil, str or "")
end end
function M.hr() function M.hr()
return "<hr>" return M.element("hr")
end end
function M.img(src, alt) function M.img(src, alt)
src = src or "" return M.element("img", { src = src or "", alt = alt or "" })
alt = alt or ""
return '<img src="' .. src .. '" alt="' .. alt .. '">'
end end
function M.strong(str) function M.strong(str)
return "<strong>" .. (str or "") .. "</strong>" return M.element("strong", nil, str or "")
end end
function M.em(str) function M.em(str)
return "<em>" .. (str or "") .. "</em>" return M.element("em", nil, str or "")
end end
function M.br() function M.br()
return "<br>" return M.element("br")
end end
function M.div(content, class) function M.div(content, class)
content = content or "" return M.element("div", class and { class = class } or nil, content or "")
class = class or ""
local class_attr = class ~= "" and (' class="' .. class .. '"') or ""
return "<div" .. class_attr .. ">" .. content .. "</div>"
end end
function M.span(content, class) function M.span(content, class)
content = content or "" return M.element("span", class and { class = class } or nil, content or "")
class = class or ""
local class_attr = class ~= "" and (' class="' .. class .. '"') or ""
return "<span" .. class_attr .. ">" .. content .. "</span>"
end end
-- HTML escaping utility
function M.escape(str) function M.escape(str)
str = tostring(str or "") str = tostring(str or "")
str = str:gsub("&", "&amp;") str = str:gsub("&", "&amp;")
@@ -160,55 +168,28 @@ function M.escape(str)
return str return str
end end
-- Get site name from config
function M.site_name()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.name then
return fes_mod.config.site.name
end
return ""
end
-- Get site title from config
function M.site_title()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.title then
return fes_mod.config.site.title
end
return ""
end
-- Get site authors from config
function M.site_authors()
local fes_mod = package.loaded.fes
if fes_mod and fes_mod.config and fes_mod.config.site and fes_mod.config.site.authors then
return fes_mod.config.site.authors
end
return {}
end
function M.highlight(str) function M.highlight(str)
return '<span class="highlight">' .. (str or "") .. "</span>" return M.element("span", { class = "highlight" }, str or "")
end end
function M.banner(str) function M.banner(str)
return '<div class="banner">' .. (str or "") .. "</div>" return M.element("div", { class = "banner" }, str or "")
end end
function M.center(str) function M.center(str)
return '<div class="center">' .. (str or "") .. "</div>" return M.element("div", { class = "center" }, str or "")
end end
function M.nav(link, str) function M.nav(link, str)
link = link or "example.com" link = link or "example.com"
str = str or link str = str or link
return '<a class="nav" href="' .. link .. '">' .. str .. "</a>" return M.element("a", { href = link, class = "nav" }, str)
end end
function M.rl(r, l) function M.rl(r, l)
r = r or "" return
l = l or "" M.element("span", { class = "left" }, r or "") ..
return string.format('<span class="left">%s</span><span class="right">%s</span>', r, l) M.element("span", { class = "right" }, l or "")
end end
return M return M

View File

@@ -1,7 +1,75 @@
local M = {} local M = {}
M.copyright = "&#169;" local function get(s)
M.registered_trademark = "&#174;" return "&" .. (s or "") .. ";"
M.trademark = "&#8482;" end
M.legal = {
copyright = get("copy"),
registered_trademark = get("reg"),
trademark = get("trade"),
}
M.currency = {
euro = get("euro"),
pound = get("pound"),
yen = get("yen"),
cent = get("cent"),
dollar = "$",
}
M.math = {
plus_minus = get("plusmn"),
multiply = get("times"),
divide = get("divide"),
not_equal = get("ne"),
less_equal = get("le"),
greater_equal = get("ge"),
infinity = get("infin"),
approx = get("asymp"),
}
M.arrows = {
left = get("larr"),
right = get("rarr"),
up = get("uarr"),
down = get("darr"),
left_right = get("harr"),
}
M.punctuation = {
left_double_quote = get("ldquo"),
right_double_quote = get("rdquo"),
left_single_quote = get("lsquo"),
right_single_quote = get("rsquo"),
ellipsis = get("hellip"),
em_dash = get("mdash"),
en_dash = get("ndash"),
}
M.whitespace = {
non_breaking = get("nbsp"),
thin = get("thinsp"),
}
M.symbols = {
degree = get("deg"),
micro = get("micro"),
section = get("sect"),
paragraph = get("para"),
check = get("check"),
cross = get("cross"),
bullet = get("bull"),
middle_dot = get("middot"),
broken_bar = get("brvbar"),
}
M.html = {
less_than = get("lt"),
greater_than = get("gt"),
ampersand = get("amp"),
double_quote = get("quot"),
single_quote = get("apos"),
}
return M return M

View File

@@ -3,12 +3,33 @@ local symbol = require("lib.symbol")
local M = {} local M = {}
function M.cc(tbl) function M.cc(tbl, sep)
return table.concat(tbl) return table.concat(tbl, sep or "")
end
function M.year(y)
return y or os.date("%Y")
end end
function M.copyright(link, holder) function M.copyright(link, holder)
return symbol.copyright .. " " .. std.external(link, holder) return symbol.legal.copyright .. " " .. std.external(link, holder)
end
function M.license(name)
return symbol.legal.registered .. " " .. name
end
function M.ls(dir)
local p = io.popen('ls -A -1 -- ' .. string.format('%q', dir))
if not p then
return nil
end
local t = {}
for line in p:lines() do
t[#t + 1] = line
end
p:close()
return t
end end
return M return M

26
main.go
View File

@@ -6,6 +6,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"runtime"
"github.com/fatih/color" "github.com/fatih/color"
@@ -13,6 +14,7 @@ import (
"fes/modules/doc" "fes/modules/doc"
"fes/modules/new" "fes/modules/new"
"fes/modules/server" "fes/modules/server"
"fes/modules/ui"
"fes/modules/version" "fes/modules/version"
) )
@@ -29,18 +31,21 @@ func init() {
config.Docker = flag.Bool("docker", false, "Create a docker project") config.Docker = flag.Bool("docker", false, "Create a docker project")
config.Lib = lib config.Lib = lib
config.Doc = documentation config.Doc = documentation
config.Verbose = flag.Bool("verbose", false, "Enable verbose logging")
} }
func main() { func main() {
var m runtime.MemStats
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <command> <project_dir>\n", os.Args[0]) fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <command> <project_dir>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Commands:") fmt.Fprintln(flag.CommandLine.Output(), "Commands:")
fmt.Fprintf(os.Stderr, " new <project_dir> Create a new project") fmt.Fprintln(flag.CommandLine.Output(), " new <project_dir> Create a new project")
fmt.Fprintf(os.Stderr, " doc Open documentation") fmt.Fprintln(flag.CommandLine.Output(), " doc Open documentation")
fmt.Fprintf(os.Stderr, " run <project_dir> Start the server") fmt.Fprintln(flag.CommandLine.Output(), " run <project_dir> Start the server")
fmt.Fprintf(os.Stderr, "Options:") fmt.Fprintln(flag.CommandLine.Output(), "Options:")
flag.PrintDefaults() flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.") fmt.Fprintln(flag.CommandLine.Output(), "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.")
} }
showVersion := flag.Bool("version", false, "Show version and exit") showVersion := flag.Bool("version", false, "Show version and exit")
@@ -89,6 +94,15 @@ func main() {
os.Exit(1) os.Exit(1)
} }
case "run": case "run":
if *config.Port == 3000 {
ui.WARNING("Using default port, this may lead to conflicts with other services")
}
ui.Log("Fes is starting")
ui.Log("Fes version=%s, commit=%s, just started", version.VERSION, version.GetCommit())
runtime.ReadMemStats(&m)
ui.Log("FRE memory usage when created %v Mb", m.TotalAlloc/1024/1024)
if err := server.Start(dir); err != nil { if err := server.Start(dir); err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "%s does not exist\n", dir) fmt.Fprintf(os.Stderr, "%s does not exist\n", dir)

View File

@@ -11,6 +11,7 @@ var Port *int
var Color *bool var Color *bool
var Static *bool var Static *bool
var Docker *bool var Docker *bool
var Verbose *bool
type AppConfig struct { type AppConfig struct {
App struct { App struct {

View File

@@ -118,11 +118,12 @@ Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).`, "$$", "`"),
ui.Hint("you can run this with `fes run %s`", dir) ui.Hint("you can run this with `fes run %s`", dir)
fmt.Println("Created new Fes project at", func() string { fmt.Println("Created new Fes project at", func () string {
if res, err := filepath.Abs(dir); err == nil { if cwd, err := os.Getwd(); err != nil {
return res return dir
} else {
return cwd
} }
return dir
}()) }())
return nil return nil

View File

@@ -1,11 +1,10 @@
package server package server
import ( import (
"errors"
"fes/modules/config" "fes/modules/config"
"fes/modules/ui" "fes/modules/ui"
"fmt" "fmt"
"github.com/pelletier/go-toml/v2"
lua "github.com/yuin/gopher-lua"
"html/template" "html/template"
"io/fs" "io/fs"
"net/http" "net/http"
@@ -15,6 +14,9 @@ import (
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/pelletier/go-toml/v2"
lua "github.com/yuin/gopher-lua"
) )
/* this is the request data we pass over the bus to the application, via the fes.bus interface */ /* this is the request data we pass over the bus to the application, via the fes.bus interface */
@@ -284,23 +286,25 @@ func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
urlPath = basePath(strings.TrimPrefix(urlPath, "/archive")) urlPath = basePath(strings.TrimPrefix(urlPath, "/archive"))
var b strings.Builder var b strings.Builder
b.WriteString("<html>\n<head><title>Index of ") b.WriteString("<html>\n<head><title>Index of ")
b.WriteString(template.HTMLEscapeString(urlPath)) b.WriteString(template.HTMLEscapeString(urlPath))
b.WriteString("</title></head>\n<body>\n<h1>Index of ") b.WriteString("</title></head>\n<body>\n<h1>Index of ")
b.WriteString(template.HTMLEscapeString(urlPath)) b.WriteString(template.HTMLEscapeString(urlPath))
b.WriteString("</h1><hr><pre>") b.WriteString("</h1><hr><pre>")
if urlPath != "/archive" && urlPath != "/archive/" {
up := path.Dir(urlPath) if urlPath != "/" {
if up == "." { b.WriteString(
up = "/archive" `<a href="/archive` +
} template.HTMLEscapeString(path.Dir(strings.TrimSuffix(urlPath, "/"))) +
if !strings.HasSuffix(up, "/") { `">../</a>` + "\n",
up = "/archive" + filepath.Dir(up) + "/" )
}
b.WriteString(`<a href="` + template.HTMLEscapeString(up) + `">../</a>` + "\n")
} else { } else {
b.WriteString(`<a href="../">../</a>` + "\n") b.WriteString(
`<a href="/">../</a>` + "\n",
)
} }
nameCol := 50 nameCol := 50
for _, ei := range list { for _, ei := range list {
escapedName := template.HTMLEscapeString(ei.name) escapedName := template.HTMLEscapeString(ei.name)
@@ -378,18 +382,26 @@ func loadDirs() map[string]string {
/* helper to parse the Fes.toml and generate config */ /* helper to parse the Fes.toml and generate config */
func parseConfig() config.AppConfig { func parseConfig() config.AppConfig {
defaultCfg := config.AppConfig{}
defaultCfg.App.Authors = []string{"unknown"}
defaultCfg.App.Name = "unknown"
defaultCfg.App.Version = "unknown"
tomlDocument, err := os.ReadFile("Fes.toml") tomlDocument, err := os.ReadFile("Fes.toml")
if err != nil { if err != nil {
ui.Error("failed to read Fes.toml", err) if errors.Is(err, os.ErrNotExist) {
os.Exit(1) ui.WARN("no config file found, using the default config. In order to specify a config file write to Fes.toml")
return defaultCfg
} else {
ui.Error("failed to read Fes.toml", err)
os.Exit(1)
}
} }
docStr := fixMalformedToml(string(tomlDocument)) docStr := fixMalformedToml(string(tomlDocument))
var cfg config.AppConfig var cfg config.AppConfig
if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil { if err := toml.Unmarshal([]byte(docStr), &cfg); err != nil {
ui.Warning("failed to parse Fes.toml", err) ui.Warning("failed to parse Fes.toml", err)
cfg.App.Authors = []string{"unknown"} cfg = defaultCfg
cfg.App.Name = "unknown"
cfg.App.Version = "unknown"
} }
return cfg return cfg
} }
@@ -414,6 +426,8 @@ func Start(dir string) error {
return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err) return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err)
} }
ui.Log("Running root=%s, port=%d.", filepath.Clean(dir), *config.Port)
cfg := parseConfig() cfg := parseConfig()
notFoundData := generateNotFoundData(&cfg) notFoundData := generateNotFoundData(&cfg)
routes := loadDirs() routes := loadDirs()
@@ -465,7 +479,8 @@ func Start(dir string) error {
w.Write(data) w.Write(data)
}) })
ui.Log("Server initialized")
fmt.Printf("Server is running on http://localhost:%d\n", *config.Port) ui.Log("Ready to accept connections tcp")
return http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil) return http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil)
} }

View File

@@ -4,75 +4,114 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
"fes/modules/config" "fes/modules/config"
"fes/modules/version"
"github.com/fatih/color" "github.com/fatih/color"
) )
const ( const (
hint_color = 0xbda02a hintColor = 0xbda02a
) )
/* print out the current path (route) and relevant error */ func formatTimestamp() string {
func Path(path string, err error) { return time.Now().Format("02 Jan 2006 15:04")
path = strings.TrimPrefix(path, "/") }
if path == "" { func logMessage(prefix string, msg string, args ...any) {
path = "(null)" formatted := fmt.Sprintf(msg, args...)
} if prefix == "" {
fmt.Printf("%s * %s\n", formatTimestamp(), formatted)
fmt.Printf(" > %s ", path)
if err == nil {
OK("ok")
return
} else if errors.Is(err, config.ErrRouteMiss) {
WARN(config.ErrRouteMiss.Error())
} else { } else {
ERROR("bad") fmt.Printf("%s * %s: %s\n", formatTimestamp(), prefix, formatted)
} }
} }
/* print general system warning */ // Generic log
func Warning(msg string, err error) error { func Log(msg string, args ...any) {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.MagentaString("warning"), err) logMessage("", msg, args...)
return err
} }
/* print general system error */ // OK message (green)
func Error(msg string, err error) error { func OK(msg string, args ...any) {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("error"), err) formatted := fmt.Sprintf(msg, args...)
return err color.Green("%s * %s\n", formatTimestamp(), formatted)
} }
/* print fatality and panic */ // Warning message (magenta)
func Fatal(msg string, err error) error { func WARN(msg string, args ...any) {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("fatal"), err) formatted := fmt.Sprintf(msg, args...)
panic(err) color.Magenta("%s # %s\n", formatTimestamp(), formatted)
} }
/* print a useful hint to the user */ // Warning message (magenta)
func Hint(format string, args ...any) { func WARNING(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Magenta("%s # WARNING %s\n", formatTimestamp(), formatted)
}
// Error message (red)
func ERROR(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Red("%s ! %s\n", formatTimestamp(), formatted)
}
// Fatal message and panic
func FATAL(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.Red("%s % %s\n", formatTimestamp(), formatted)
panic(formatted)
}
// Hint message (custom color)
func Hint(msg string, args ...any) {
formatted := fmt.Sprintf(msg, args...)
color.RGB(func(hex int) (r, g, b int) { color.RGB(func(hex int) (r, g, b int) {
r = (hex >> 16) & 0xFF r = (hex >> 16) & 0xFF
g = (hex >> 8) & 0xFF g = (hex >> 8) & 0xFF
b = hex & 0xFF b = hex & 0xFF
return return
}(hint_color)).Printf("hint: %s\n", fmt.Sprintf(format, args...)) }(hintColor)).Printf("hint: %s\n", formatted)
} }
/* print message using the ok status color */ // Path logging: prints route and status
func OK(msg string) { func Path(path string, err error) {
color.Green(msg) path = strings.TrimPrefix(path, "/")
if path == "" {
path = "(null)"
}
if err == nil {
OK("Route: %s - ok", path)
} else if errors.Is(err, config.ErrRouteMiss) {
WARN("Route: %s - %s", path, config.ErrRouteMiss.Error())
} else {
ERROR("Route: %s - bad", path)
}
} }
/* print message using the warning status color */ // System warning with prefix
func WARN(msg string) { func Warning(msg string, err error) error {
color.Magenta(msg) WARN("%s: %v", msg, err)
return err
} }
/* print message using the error status color */ // System error with prefix
func ERROR(msg string) { func Error(msg string, err error) error {
color.Red(msg) ERROR("%s: %v", msg, err)
return err
}
// Fatal system error
func Fatal(msg string, err error) error {
FATAL("%s: %v", msg, err)
return err
}
// Log on Verbose
func LogVerbose(msg string, args ...any) {
if *config.Verbose {
Log(msg, args...)
}
} }

View File

@@ -9,7 +9,7 @@ var gitCommit string = "devel"
const PROGRAM_NAME string = "fes" const PROGRAM_NAME string = "fes"
const PROGRAM_NAME_LONG string = "fes/fSD" const PROGRAM_NAME_LONG string = "fes/fSD"
const VERSION string = "beta" const VERSION string = "0.3.1"
func Version() { func Version() {
fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION) fmt.Printf("%s version %s\n", PROGRAM_NAME_LONG, VERSION)
@@ -20,3 +20,7 @@ func FullVersion() {
fmt.Printf("%s+%s\n", VERSION, gitCommit) fmt.Printf("%s+%s\n", VERSION, gitCommit)
os.Exit(0) os.Exit(0)
} }
func GetCommit() string {
return gitCommit
}