2 Commits

Author SHA1 Message Date
2b2be7b4e7 I will save this but will probably change this 2025-12-29 17:11:28 -05:00
99c41a0d60 write stubs 2025-12-28 20:52:31 -05:00
51 changed files with 633 additions and 464 deletions

2
.gitignore vendored
View File

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

8
.luarc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"workspace.library": [
"./lsp"
],
"diagnostics.disable": [
"undefined-global"
]
}

View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) 2025-2026 fSD
Copyright (c) 2025 fSD
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
@@ -12,4 +12,4 @@ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
PERFORMANCE OF THIS SOFTWARE.

View File

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

View File

@@ -40,6 +40,6 @@ Run `fes doc` for the documentation website or goto [docs.vxserver.dev](https://
ISC License
Copyright (C) 2025-2026 fSD
Copyright (C) 2025 fSD
See `COPYING`

View File

@@ -1,22 +0,0 @@
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

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

View File

@@ -1,33 +0,0 @@
# 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

@@ -1,12 +0,0 @@
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

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,5 +1,4 @@
local std = require("lib.std")
local symbol = require("lib.symbol")
local M = {}
M.__index = M
@@ -319,30 +318,11 @@ em, i { font-style: italic; }
return setmetatable(self, M)
end
function M:g(str)
function M:custom(str)
table.insert(self.parts, str)
return self
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
if type(func) == "function" then
M[name] = function(self, ...)
@@ -360,11 +340,9 @@ function M:build()
header = header:gsub(
"{{FAVICON}}",
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 symbol.legal.copyright .. "The Copyright Holder")
local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder")
return header .. table.concat(self.parts, "\n") .. footer
end

View File

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

View File

@@ -1,75 +1,7 @@
local M = {}
local function get(s)
return "&" .. (s or "") .. ";"
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"),
}
M.copyright = "&#169;"
M.registered_trademark = "&#174;"
M.trademark = "&#8482;"
return M

View File

@@ -3,33 +3,12 @@ local symbol = require("lib.symbol")
local M = {}
function M.cc(tbl, sep)
return table.concat(tbl, sep or "")
end
function M.year(y)
return y or os.date("%Y")
function M.cc(tbl)
return table.concat(tbl)
end
function M.copyright(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
return symbol.copyright .. " " .. std.external(link, holder)
end
return M

31
lsp/fes.lua Normal file
View File

@@ -0,0 +1,31 @@
---@meta
---@class Fes
---@field version string|nil
---@field title string|nil
---@field copyright string|nil
---@field favicon string|nil
---@field header string
---@field footer string
---@field parts string[]
---@field [string] fun(self: Fes, ...: any): Fes
local Fes = {}
Fes.__index = Fes
---@param header? string
---@param footer? string
---@return Fes
function Fes.fes(header, footer) end
---@param html string
---@return Fes
function Fes:custom(html) end
---@return string
function Fes:build() end
---@return string
function Fes:__tostring() end
return Fes

155
lsp/std.lua Normal file
View File

@@ -0,0 +1,155 @@
---@meta
---@class FesStd
local M = {}
---@return string
function M.fes_version() end
---@return string
function M.site_version() end
---@param link? string
---@param str? string
---@return string
function M.a(link, str) end
---@param link? string
---@param str? string
---@return string
function M.ha(link, str) end
---@param link string
---@param str string
---@return string
function M.external(link, str) end
---@param str string
---@return string
function M.note(str) end
---@param str string
---@return string
function M.muted(str) end
---@param str string
---@return string
function M.callout(str) end
---@param str? string
---@return string
function M.h1(str) end
---@param str? string
---@return string
function M.h2(str) end
---@param str? string
---@return string
function M.h3(str) end
---@param str? string
---@return string
function M.h4(str) end
---@param str? string
---@return string
function M.h5(str) end
---@param str? string
---@return string
function M.h6(str) end
---@param str? string
---@return string
function M.p(str) end
---@param str? string
---@return string
function M.pre(str) end
---@param str? string
---@return string
function M.code(str) end
---@param items? any[]
---@return string
function M.ul(items) end
---@param items? any[]
---@return string
function M.ol(items) end
---@param items? any[]
---@return string
function M.tl(items) end
---@param str? string
---@return string
function M.blockquote(str) end
---@return string
function M.hr() end
---@param src? string
---@param alt? string
---@return string
function M.img(src, alt) end
---@param str? string
---@return string
function M.strong(str) end
---@param str? string
---@return string
function M.em(str) end
---@return string
function M.br() end
---@param content? string
---@param class? string
---@return string
function M.div(content, class) end
---@param content? string
---@param class? string
---@return string
function M.span(content, class) end
---@param str any
---@return string
function M.escape(str) end
---@return string
function M.site_name() end
---@return string
function M.site_title() end
---@return any[]
function M.site_authors() end
---@param str? string
---@return string
function M.highlight(str) end
---@param str? string
---@return string
function M.banner(str) end
---@param str? string
---@return string
function M.center(str) end
---@param link? string
---@param str? string
---@return string
function M.nav(link, str) end
---@param right? string
---@param left? string
---@return string
function M.rl(right, left) end
return M

20
lsp/symbol.lua Normal file
View File

@@ -0,0 +1,20 @@
---@meta
---@class FesSymbol
local M = {}
--@type string
--@readonly
M.copyright = "&#169;"
--@type string
--@readonly
M.registered_trademark = "&#174;"
--@type string
--@readonly
M.trademark = "&#8482;"
return M

15
lsp/util.lua Normal file
View File

@@ -0,0 +1,15 @@
---@meta
---@class CopyrightUtil
local M = {}
---@param tbl string[]
---@return string
function M.cc(tbl) end
---@param link string
---@param holder string
---@return string
function M.copyright(link, holder) end
return M

66
main.go
View File

@@ -6,21 +6,23 @@ import (
"flag"
"fmt"
"os"
"runtime"
"github.com/fatih/color"
"fes/modules/config"
"fes/modules/doc"
"fes/modules/lsp"
"fes/modules/new"
"fes/modules/server"
"fes/modules/ui"
"fes/modules/version"
)
//go:embed lib/*
var lib embed.FS
//go:embed lsp/*
var lspStubs embed.FS
//go:embed index.html
var documentation string
@@ -29,20 +31,20 @@ func init() {
config.Color = flag.Bool("no-color", false, "Disable color output")
config.Static = flag.Bool("static", false, "Render and save all pages")
config.Docker = flag.Bool("docker", false, "Create a docker project")
config.Lib = lib
config.LspStubs = lspStubs
config.Doc = documentation
config.Verbose = flag.Bool("verbose", false, "Enable verbose logging")
}
func main() {
var m runtime.MemStats
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <command> <project_dir>\n", os.Args[0])
fmt.Fprintln(flag.CommandLine.Output(), "Commands:")
fmt.Fprintln(flag.CommandLine.Output(), " new <project_dir> Create a new project")
fmt.Fprintln(flag.CommandLine.Output(), " doc Open documentation")
fmt.Fprintln(flag.CommandLine.Output(), " run <project_dir> Start the server")
fmt.Fprintln(flag.CommandLine.Output(), " lsp <sub_command> Work with Lsp")
fmt.Fprintln(flag.CommandLine.Output(), "Options:")
flag.PrintDefaults()
fmt.Fprintln(flag.CommandLine.Output(), "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.")
@@ -72,41 +74,39 @@ func main() {
}
cmd := args[0]
var dir string
if cmd == "new" || cmd == "run" {
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Error: %s requires <project_dir>\n", cmd)
flag.Usage()
os.Exit(1)
}
dir = args[1]
}
var arg string
switch cmd {
case "new":
if err := new.Project(dir); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
case "doc":
if cmd == "doc" {
if err := doc.Open(); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
case "run":
if *config.Port == 3000 {
ui.WARNING("Using default port, this may lead to conflicts with other services")
os.Exit(0)
}
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Error: not enough arguments")
flag.Usage()
os.Exit(1)
}
arg = args[1]
switch cmd {
case "new":
if err := new.Project(arg); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
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 {
case "lsp":
if err := lsp.Do(arg); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
case "run":
if err := server.Start(arg); err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "%s does not exist\n", dir)
fmt.Fprintf(os.Stderr, "Try: fes new %s\n", dir)
fmt.Fprintf(os.Stderr, "%s does not exist\n", arg)
fmt.Fprintf(os.Stderr, "Try: fes new %s\n", arg)
os.Exit(1)
} else {
fmt.Fprintln(os.Stderr, "Error:", err)

View File

@@ -6,12 +6,13 @@ import (
)
var Lib embed.FS
var LspStubs embed.FS
var Doc string
var Port *int
var Color *bool
var Static *bool
var Docker *bool
var Verbose *bool
type AppConfig struct {
App struct {

173
modules/lsp/lsp.go Normal file
View File

@@ -0,0 +1,173 @@
package lsp
import (
"embed"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"fes/modules/config"
)
var destRoot string = filepath.Join(func () string {
res, err := os.UserHomeDir()
if err != nil { panic(err) }
return res
}(), ".local", "LuaAddons", "fes")
func usage(status int) {
fmt.Fprintln(flag.CommandLine.Output(), "Lsp Usage:")
fmt.Fprintln(flag.CommandLine.Output(), " help Print this help and exit")
fmt.Fprintln(flag.CommandLine.Output(), " install Install the Lsp")
fmt.Fprintln(flag.CommandLine.Output(), " uninstall Uninstall the Lsp")
fmt.Fprintln(flag.CommandLine.Output(), " doctor Verify Lsp installation")
os.Exit(status)
}
func writeFile(fs embed.FS, src, dst string) error {
in, err := fs.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
func install() error {
if err := os.RemoveAll(destRoot); err != nil {
return err
}
var found bool
err := fsWalkDir(config.LspStubs, "lsp", func(path string) error {
if path == "lsp" {
return nil
}
rel := strings.TrimPrefix(path, "lsp/")
dst := filepath.Join("library", destRoot, rel)
info, err := fsStat(config.LspStubs, path)
if err != nil {
return err
}
if info.IsDir() {
return os.MkdirAll(dst, 0755)
}
found = true
return writeFile(config.LspStubs, path, dst)
})
if err != nil {
return err
}
if !found {
return errors.New("no lsp stubs found in embedded fs")
}
err = os.WriteFile(filepath.Join("dstRoot", "config.json"), []byte(""), 0755)
fmt.Println("Lua LSP installed to:", destRoot)
return nil
}
func uninstall() error {
if _, err := os.Stat(destRoot); err != nil {
if os.IsNotExist(err) {
fmt.Println("Lua LSP not installed")
return nil
}
return err
}
if err := os.RemoveAll(destRoot); err != nil {
return err
}
fmt.Println("Lua LSP uninstalled from:", destRoot)
return nil
}
func doctor() error {
info, err := os.Stat(destRoot)
if err != nil {
if os.IsNotExist(err) {
return errors.New("lsp not installed")
}
return err
}
if !info.IsDir() {
return errors.New("lsp install path is not a directory")
}
var luaFiles int
err = filepath.Walk(destRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".lua") {
luaFiles++
}
return nil
})
if err != nil {
return err
}
if luaFiles == 0 {
return errors.New("no lua stubs found in lsp directory")
}
fmt.Println("LSP stubs: OK")
fmt.Println("Location:", destRoot)
fmt.Println()
fmt.Println("LuaLS configuration required:")
fmt.Println()
fmt.Println(` "Lua.workspace.library": {`)
fmt.Printf(` "%s": true`, destRoot)
fmt.Println()
fmt.Println(` }`)
fmt.Println()
fmt.Println("Restart your editor after installing or updating the LSP stubs")
return nil
}
func Do(arg string) error {
switch arg {
case "help":
usage(0)
case "install":
return install()
case "uninstall":
return uninstall()
case "doctor":
return doctor()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", arg)
usage(1)
}
return nil
}

36
modules/lsp/util.go Normal file
View File

@@ -0,0 +1,36 @@
package lsp
import (
"embed"
"os"
"path/filepath"
)
func fsWalkDir(fs embed.FS, root string, fn func(string) error) error {
entries, err := fs.ReadDir(root)
if err != nil {
return err
}
for _, e := range entries {
p := filepath.Join(root, e.Name())
if err := fn(p); err != nil {
return err
}
if e.IsDir() {
if err := fsWalkDir(fs, p, fn); err != nil {
return err
}
}
}
return nil
}
func fsStat(fs embed.FS, path string) (os.FileInfo, error) {
f, err := fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return f.Stat()
}

View File

@@ -2,7 +2,6 @@ package new
import (
"fes/modules/config"
"fes/modules/ui"
"fmt"
"os"
"os/exec"
@@ -115,16 +114,5 @@ All commands are run from the root of the project, from a terminal:
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).`, "$$", "`"), dir, dir)
ui.Hint("you can run this with `fes run %s`", dir)
fmt.Println("Created new Fes project at", func () string {
if cwd, err := os.Getwd(); err != nil {
return dir
} else {
return cwd
}
}())
return nil
}

View File

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

View File

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

View File

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