7 Commits

Author SHA1 Message Date
mae
b255f07b0f Merge remote-tracking branch 'origin/pkgserver' into pkgserver 2026-03-05 02:06:27 -06:00
mae
dec4cdd068 cmd/pkgserver: replace favicon 2026-03-05 02:06:07 -06:00
mae
73c620ecd5 cmd/pkgserver: pagination 2026-03-05 02:06:07 -06:00
mae
69467a1542 cmd/pkgserver: basic web ui 2026-03-05 02:06:07 -06:00
mae
1ae6a35bc8 cmd/pkgserver: replace favicon 2026-03-05 01:12:17 -06:00
mae
9ef5b52b85 cmd/pkgserver: pagination 2026-03-05 00:32:25 -06:00
mae
f93158cb3c cmd/pkgserver: basic web ui 2026-03-04 22:50:58 -06:00
15 changed files with 357 additions and 0 deletions

55
cmd/pkgserver/main.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"embed"
"fmt"
"net/http"
)
//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts"
//go:embed ui/*
var content embed.FS
func serveWebUI(w http.ResponseWriter, r *http.Request) {
fmt.Printf("serveWebUI: %s\n", r.URL.Path)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1")
w.Header().Set("X-Frame-Options", "DENY")
http.ServeFileFS(w, r, content, "ui/index.html")
}
func serveStaticContent(w http.ResponseWriter, r *http.Request) {
fmt.Printf("serveStaticContent: %s\n", r.URL.Path)
switch r.URL.Path {
case "/static/style.css":
darkTheme := r.CookiesNamed("dark_theme")
if len(darkTheme) > 0 && darkTheme[0].Value == "true" {
http.ServeFileFS(w, r, content, "ui/static/dark.css")
} else {
http.ServeFileFS(w, r, content, "ui/static/light.css")
}
break
case "/favicon.ico":
http.ServeFileFS(w, r, content, "ui/static/favicon.ico")
break
case "/static/index.js":
http.ServeFileFS(w, r, content, "ui/static/index.js")
break
default:
http.NotFound(w, r)
}
}
func serveAPI(w http.ResponseWriter, r *http.Request) {
}
func main() {
http.HandleFunc("GET /{$}", serveWebUI)
http.HandleFunc("GET /favicon.ico", serveStaticContent)
http.HandleFunc("GET /static/", serveStaticContent)
http.HandleFunc("GET /api/", serveAPI)
http.ListenAndServe(":8067", nil)
}

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="static/style.css">
<title>Hakurei PkgServer</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="static/index.js"></script>
</head>
<body>
<h1>Hakurei PkgServer</h1>
<table id="pkg-list">
<tr><th>Status</th><th>Name</th><th>Version</th></tr>
</table>
<p>Showing entries <span id="entry-counter"></span>.</p>
<span class="bottom-nav"><a href="javascript:prevPage()">&laquo; Previous</a> <span id="page-number">1</span> <a href="javascript:nextPage()">Next &raquo;</a></span>
<span><label for="count">Entries per page:</label><select name="count" id="count">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select></span>
</body>
<footer>&copy; <a href="https://hakurei.app/">Hakurei</a>. Licensed under the MIT license.</footer>
</html>

View File

View File

@@ -0,0 +1,6 @@
@use 'common';
html {
background-color: #2c2c2c;
color: ghostwhite; }
/*# sourceMappingURL=dark.css.map */

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAAA,aAAa;AAEb,IAAK;EACH,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,UAAU",
"sources": ["dark.scss"],
"names": [],
"file": "dark.css"
}

View File

@@ -0,0 +1,6 @@
@use 'common';
html {
background-color: #2c2c2c;
color: ghostwhite;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,67 @@
"use strict";
var PackageEntry = /** @class */ (function () {
function PackageEntry() {
}
return PackageEntry;
}());
var State = /** @class */ (function () {
function State() {
this.entriesPerPage = 10;
this.currentPage = 1;
this.entryIndex = 0;
this.loadedEntries = [];
}
State.prototype.getEntriesPerPage = function () {
return this.entriesPerPage;
};
State.prototype.setEntriesPerPage = function (entriesPerPage) {
this.entriesPerPage = entriesPerPage;
this.updateRange();
};
State.prototype.getCurrentPage = function () {
return this.currentPage;
};
State.prototype.setCurrentPage = function (page) {
this.currentPage = page;
document.getElementById("page-number").innerText = String(this.currentPage);
this.updateRange();
};
State.prototype.getEntryIndex = function () {
return this.entryIndex;
};
State.prototype.setEntryIndex = function (entryIndex) {
this.entryIndex = entryIndex;
this.updateRange();
};
State.prototype.getLoadedEntries = function () {
return this.loadedEntries;
};
State.prototype.getMaxPage = function () {
return this.loadedEntries.length / this.entriesPerPage;
};
State.prototype.updateRange = function () {
var max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length);
document.getElementById("entry-counter").innerText = "".concat(this.entryIndex, "-").concat(max, " of ").concat(this.loadedEntries.length);
};
return State;
}());
var STATE;
function prevPage() {
var current = STATE.getCurrentPage();
if (current > 1) {
STATE.setCurrentPage(STATE.getCurrentPage() - 1);
}
}
function nextPage() {
var current = STATE.getCurrentPage();
if (current < STATE.getMaxPage()) {
STATE.setCurrentPage(STATE.getCurrentPage() + 1);
}
}
document.addEventListener("DOMContentLoaded", function () {
STATE = new State();
STATE.updateRange();
document.getElementById("count").addEventListener("change", function (event) {
STATE.setEntriesPerPage(parseInt(event.target.value));
});
});

View File

@@ -0,0 +1,66 @@
"use strict"
class PackageEntry {
}
class State {
entriesPerPage: number = 10
currentPage: number = 1
entryIndex: number = 0
loadedEntries: PackageEntry[] = []
getEntriesPerPage(): number {
return this.entriesPerPage
}
setEntriesPerPage(entriesPerPage: number) {
this.entriesPerPage = entriesPerPage
this.updateRange()
}
getCurrentPage(): number {
return this.currentPage
}
setCurrentPage(page: number) {
this.currentPage = page
document.getElementById("page-number").innerText = String(this.currentPage)
this.updateRange()
}
getEntryIndex(): number {
return this.entryIndex
}
setEntryIndex(entryIndex: number) {
this.entryIndex = entryIndex
this.updateRange()
}
getLoadedEntries(): PackageEntry[] {
return this.loadedEntries
}
getMaxPage(): number {
return this.loadedEntries.length / this.entriesPerPage
}
updateRange() {
let max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length)
document.getElementById("entry-counter").innerText = `${this.entryIndex}-${max} of ${this.loadedEntries.length}`
}
}
let STATE: State
function prevPage() {
let current = STATE.getCurrentPage()
if (current > 1) {
STATE.setCurrentPage(STATE.getCurrentPage() - 1)
}
}
function nextPage() {
let current = STATE.getCurrentPage()
if (current < STATE.getMaxPage()) {
STATE.setCurrentPage(STATE.getCurrentPage() + 1)
}
}
document.addEventListener("DOMContentLoaded", () => {
STATE = new State()
STATE.updateRange()
document.getElementById("count").addEventListener("change", (event) => {
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
})
})

View File

@@ -0,0 +1,6 @@
@use 'common';
html {
background-color: #d3d3d3;
color: black; }
/*# sourceMappingURL=light.css.map */

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAAA,aAAa;AAEb,IAAK;EACH,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,KAAK",
"sources": ["light.scss"],
"names": [],
"file": "light.css"
}

View File

@@ -0,0 +1,6 @@
@use 'common';
html {
background-color: #d3d3d3;
color: black;
}

View File

69
internal/azalea/azalea.go Normal file
View File

@@ -0,0 +1,69 @@
//go:generate gocc -a azalea.bnf
package azalea
import (
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"hakurei.app/container/check"
)
type Parser struct {
Generator
}
func NewParser(gen Generator) *Parser {
return &Parser{
Generator: gen,
}
}
func (p Parser) Initialise() {
}
func (p Parser) Consume(ns string, file io.Reader) error {
return nil
}
// ConsumeDir walks a directory and consumes all Azalea source files within it and all its subdirectories, as long as they end with the .az extension.
func (p Parser) ConsumeDir(dir *check.Absolute) error {
ds := dir.String()
return filepath.WalkDir(ds, func(path string, d fs.DirEntry, err error) (e error) {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(d.Name(), ".az") {
return
}
rel, e := filepath.Rel(ds, path)
ns := strings.TrimSuffix(rel, ".az")
f, e := os.Open(path)
return p.Consume(ns, f)
})
}
// ConsumeAll consumes all provided readers as Azalea source code, each given the namespace `r%d` where `%d` is the index of the reader in the provided arguments.
func (p Parser) ConsumeAll(in ...io.Reader) error {
for i, r := range in {
err := p.Consume("r"+strconv.FormatInt(int64(i), 10), r)
if err != nil {
return err
}
}
return nil
}
// ConsumeStrings consumes all provided strings as Azalea source code, each given the namespace `s%d` where `%d` is the index of the string in the provided arugments.
func (p Parser) ConsumeStrings(in ...string) error {
for i, s := range in {
err := p.Consume("s"+strconv.FormatInt(int64(i), 10), strings.NewReader(s))
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,36 @@
package azalea
import (
"io"
)
type Generator interface {
Finalise() (error, io.Writer)
}
type JsonGenerator struct {
t any
}
func NewJsonGenerator[T any]() JsonGenerator {
t := new(T)
return JsonGenerator{
t,
}
}
func (j *JsonGenerator) Finalise() (error, io.Writer) {
}
type PkgIRGenerator struct {
}
func NewPkgIRGenerator() PkgIRGenerator {
return PkgIRGenerator{}
}
func (p *PkgIRGenerator) Finalise() (error, io.Writer) {
}