diff --git a/src/consts.ts b/src/consts.ts index 0049e2f..c746a7c 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -29,5 +29,5 @@ export const SOCIALS = [ // hide: repo-navne der skjules helt. pinned: vises først (i den rækkefølge). export const PROJECTS = { hide: [] as string[], - pinned: [] as string[], + pinned: ['ubl-tools', 'gtk-ahfail', 'pi3-smart-workspace'] as string[], }; diff --git a/src/content.config.ts b/src/content.config.ts index 9e0b254..87ae8d9 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -14,4 +14,24 @@ const blog = defineCollection({ }), }); -export const collections = { blog }; +// Projektsider (/projekter/): én markdown-fil pr. værktøj jeg udgiver. +// Frontmatter bærer de faste links, så siden og søgeindekset kan vise dem ens. +const projekter = defineCollection({ + loader: glob({ pattern: '**/*.md', base: './src/content/projekter' }), + schema: () => + z.object({ + title: z.string(), + description: z.string(), + repo: z.string(), // repo-navn på Gitea under SITE.giteaUser + sprog: z.string(), // fx Rust, Python + licens: z.string(), + platforme: z.array(z.string()).default([]), + aur: z.array(z.string()).default([]), // AUR-pakkenavne + tags: z.array(z.string()).default([]), + date: z.coerce.date(), // første udgivelse + updated: z.coerce.date().optional(), + draft: z.boolean().default(false), + }), +}); + +export const collections = { blog, projekter }; diff --git a/src/content/projekter/ahfail.md b/src/content/projekter/ahfail.md new file mode 100644 index 0000000..fb6a28c --- /dev/null +++ b/src/content/projekter/ahfail.md @@ -0,0 +1,55 @@ +--- +title: "ahfail" +description: "En skærmlås til Linux og macOS. Skriver du forkert kodeord, dukker Dennis Nedry op og siger \"ah ah ah, you didn't say the magic word\"." +repo: gtk-ahfail +sprog: Rust +licens: AGPL-3.0-only +platforme: [Linux (Wayland), Linux (X11), macOS] +aur: [ahfail, ahfail-bin] +tags: [linux, wayland, x11, macos, pam, rust] +date: 2026-09-30 +updated: 2026-09-30 +--- + +Skriver du forkert kodeord, kører en animation i loop af mit eget ansigt photoshoppet ind på Dennis Nedry fra Jurassic Park, mens klippet med "ah ah ah, you didn't say the magic word" spiller. Lyden skrues helt op, så den der prøvede at snige sig ind på din maskine også får det hele med (`--no-max-volume` hvis du hellere vil være diskret). Under joken er det en rigtig skærmlås. + +```bash +ahfail # lås skærmen +ahfail test # se effekten uden at låse +ahfail doctor # hvad mangler maskinen? +``` + +Den er ét program på Linux, og den finder selv ud af om du kører Wayland eller X11. På Wayland låser den gennem `ext-session-lock-v1`, så det virker i sway, Hyprland, niri, river, labwc og KDE Plasma 6. GNOME lader ikke andre programmer låse skærmen, så der er den ude. På X11 gør den som i3lock: fuldskærmsvinduer på alle skærme og et greb om tastatur og mus, og kan den ikke få grebet, nægter den at låse i stedet for at vise en lås dine tastetryk går lige igennem. + +Kodeordet tjekkes gennem dens egen PAM-tjeneste, `/etc/pam.d/ahfail`, som pakken installerer selv. Den rører ikke andre pakkers PAM-opsætning, på samme måde som swaylock og gtklock gør det. Den inkluderer `login`, så systemets `pam_faillock` gælder også her, og på Arch betyder det at tre forkerte kodeord på et kvarter låser kontoen i ti minutter. Det er værd at vide inden man viser den frem til fredagsbaren. + +En skærmlås sidder direkte i login-vejen, så en fejl i den kan i værste fald svække låsen. Der er ingen garantier, og du installerer den på eget ansvar. Det står også i pakken. + +## Installation + +På Arch Linux, som kildepakke eller færdigbygget: + +```bash +yay -S ahfail +yay -S ahfail-bin +``` + +Kør `ahfail doctor` bagefter. Den fortæller om sessionstypen er understøttet, om PAM-filen ligger rigtigt, om GStreamer kan afspille klippet og om lydstyrken kan styres, med den præcise rettelse hvis noget mangler. + +Vil du have den til at låse af sig selv, så peg din idle-dæmon på den: + +```bash +swayidle -w timeout 300 'ahfail' before-sleep 'ahfail' # Wayland +xss-lock -- ahfail # X11 +``` + +## macOS + +På Mac er det systemets egen låseskærm der låser, og effekten kommer fra en PAM-hook der installeres via Homebrew: + +```bash +brew tap asger-weirsoee/tap +brew install ahfail +``` + +Homebrew skriver bagefter den ene linje ud som skal tilføjes i `/etc/pam.d/screensaverui`. Det er det eneste manuelle trin. diff --git a/src/content/projekter/pi3-smart-workspace.md b/src/content/projekter/pi3-smart-workspace.md new file mode 100644 index 0000000..1bc10dc --- /dev/null +++ b/src/content/projekter/pi3-smart-workspace.md @@ -0,0 +1,47 @@ +--- +title: "pi3-smart-workspace" +description: "Et lille værktøj til i3 der gør $mod+1 til den første workspace på den skærm musen står på, i stedet for workspace nummer 1 et helt andet sted." +repo: pi3-smart-workspace +sprog: Python +licens: Apache-2.0 +platforme: [Linux (i3)] +aur: [pi3-smart-workspace] +tags: [i3, linux, python] +date: 2020-09-04 +updated: 2026-09-30 +--- + +Med flere skærme i i3 er workspaces nummereret globalt. Trykker du `$mod+1`, hopper du til workspace 1, også selvom den ligger på skærmen til venstre og du sidder og arbejder på den til højre. `pi3-smart-workspace` vender det om: tasten vælger den N'te workspace på den skærm musen står på. `$mod+1` er altid den første workspace der hvor musen er, og et setup med flere skærme føles meget mere sammenhængende. + +Den læser din kørende i3-konfiguration og bruger tre ting fra den: navne på skærmene (`set $ `), navne på workspaces og linjerne der binder en workspace til en skærm. Rækkefølgen af de linjer bestemmer hvilken workspace der er nummer 1, 2 og 3 på hver skærm. Den kan også flytte det aktive vindue derhen, og hvis du vil, følge med. + +Den første udgave er fra 2020. + +## Installation + +Fra AUR eller PyPI: + +```bash +yay -S pi3-smart-workspace +pip install pi3-smart-workspace +``` + +Den kræver Python 3.14 og en kørende i3-session. + +## Opsætning + +Den nemmeste vej er at lade den skrive konfigurationen selv: + +```bash +pi3-smart-workspace init +``` + +Den finder alle aktive skærme gennem i3 og skriver en mappe med en `workspaces.conf` pr. skærm og en `bindings.conf` med `$mod+N`, `$mod+Shift+N` og `$mod+Ctrl+N`. Hver skærm får sit eget sammenhængende interval af globale numre, og indekset efter kolon (fx `9:1`) betyder "første workspace på den her skærm". Tilføj de include-linjer `init` skriver ud til din i3-config og genindlæs med `$mod+Shift+r`. + +Vil du selv binde tasterne, ser det sådan ud: + +``` +bindsym $mod+1 exec --no-startup-id pi3-smart-workspace -i 1 +bindsym $mod+Shift+1 exec --no-startup-id pi3-smart-workspace -i 1 -s +bindsym $mod+Ctrl+1 exec --no-startup-id pi3-smart-workspace -i 1 -s -k +``` diff --git a/src/content/projekter/ubl-tools.md b/src/content/projekter/ubl-tools.md new file mode 100644 index 0000000..09b224d --- /dev/null +++ b/src/content/projekter/ubl-tools.md @@ -0,0 +1,59 @@ +--- +title: "ubl-tools" +description: "Læs e-fakturaer i UBL (Peppol BIS 3.0 og OIOUBL) i terminalen eller som en side du kan printe, og træk den vedhæftede PDF ud. Alt foregår på din egen maskine." +repo: ubl-tools +sprog: Rust +licens: MIT OR Apache-2.0 +platforme: [Linux, macOS, Windows] +aur: [ubl-tools, ubl-tools-bin] +tags: [peppol, oioubl, nemhandel, e-faktura, rust] +date: 2026-09-30 +updated: 2026-10-01 +--- + +En e-faktura er en XML-fil. Vil man læse en, skal man enten scrolle gennem et par hundrede linjer `cbc:` og `cac:`, eller uploade den til en online viewer. Viewerne virker fint, men de vil have hele fakturaen, og den indeholder navne, CVR-numre, bankoplysninger og hvad kunden har købt. Det har jeg det lidt stramt med at sende til en fremmed server bare for at kunne læse den. + +Så jeg skrev `ubl`. Den læser filen lokalt og viser den som en almindelig faktura: sælger og køber med deres identifikatorer navngivet ordentligt (CVR, GLN, SE-nr. og Peppol-endpoints i stedet for skemakoder som `0184`), linjerne med antal, enhed, pris og moms, momsopgørelsen pr. kategori og totalerne. Betalingsoplysningerne kommer med, også danske FI-kort i den velkendte `+71<…+…<`-notation. Er fakturaen pakket ind i en Peppol-kuvert (SBDH), pakker den den selv ud. + +Den forstår Peppol BIS Billing 3.0, OIOUBL 2.1 og andre EN 16931-fakturaer og kreditnotaer. OIOUBL har en lille finte, hvor `TaxExclusiveAmount` faktisk er momsbeløbet og ikke beløbet før moms.. det har den lært at tage højde for, så tallene passer. + +## Installation + +På Arch Linux ligger den på AUR, både som kildepakke og som færdigbygget binær: + +```bash +yay -S ubl-tools # bygges fra kildekoden +yay -S ubl-tools-bin # færdigbygget binær +``` + +På macOS via min Homebrew-tap: + +```bash +brew tap asger-weirsoee/tap +brew install ubl-tools +``` + +På Windows 10/11 med Scoop: + +```powershell +scoop install https://asger.weirsoe.dk/tarballz/ubl-tools.json +``` + +Du kan også hente zip-filen fra [tarballz](/tarballz/) og pakke den ud. Binæren er ikke kodesigneret, så Windows spørger måske første gang den køres. + +## Brug + +```bash +ubl view faktura.xml # i terminalen +ubl view faktura.xml --open # som side i browseren +ubl view faktura.xml --lang da # danske labels +ubl extract faktura.xml anden.xml # gem PDF'en ved hver fil +ubl extract *.xml -o ./bilag # eller alle i én mappe +ubl open faktura.xml # gem bilag og åbn i browser +``` + +`ubl extract` overskriver aldrig noget. Ligger der allerede en identisk fil, genbruges den, og er den anderledes, får den nye et andet navn. På Windows kan du trække en eller flere XML-filer direkte over på `ubl.exe` i Stifinder, så gemmes PDF'erne og fakturaerne åbnes, og vinduet bliver stående til du har læst resultatet. + +HTML-siden er én selvstændig fil uden eksterne forespørgsler, sat op til A4, så den printer pænt. + +Skal du bare have afkodet en CustomizationID eller se hvilken profil en faktura følger, kan [UBL-tjekket](/ubl) her på siden gøre det direkte i browseren. diff --git a/src/pages/projekter.astro b/src/pages/projekter.astro deleted file mode 100644 index cfb36cc..0000000 --- a/src/pages/projekter.astro +++ /dev/null @@ -1,49 +0,0 @@ ---- -import Base from '../layouts/Base.astro'; -import { SITE, PROJECTS } from '../consts'; - -type Repo = { name: string; description: string | null; html_url: string; language: string | null; stars_count: number; updated_at: string; fork: boolean; archived: boolean; private: boolean; }; -let repos: Repo[] = []; -let error = false; -try { - const res = await fetch(`${SITE.giteaBase}/api/v1/users/${SITE.giteaUser}/repos?limit=50&sort=updated`, { - headers: { 'Accept': 'application/json' }, - }); - if (res.ok) { - const hide = new Set(PROJECTS.hide.map((s) => s.toLowerCase())); - const data = (await res.json()) as Repo[]; - repos = data - .filter((r) => !r.fork && !r.archived && !r.private && !hide.has(r.name.toLowerCase())); - // pinned først (i opsat rækkefølge), resten efter senest opdateret - const order = (n: string) => { - const i = PROJECTS.pinned.findIndex((p) => p.toLowerCase() === n.toLowerCase()); - return i === -1 ? Infinity : i; - }; - repos.sort((a, b) => { - const oa = order(a.name), ob = order(b.name); - if (oa !== ob) return oa - ob; - return +new Date(b.updated_at) - +new Date(a.updated_at); - }); - } else { error = true; } -} catch { error = true; } ---- - -

Projekter

-

Hentet live fra min Gitea.

- - {error &&

Kunne ikke hente repositories lige nu.

} - - - diff --git a/src/pages/projekter/[slug].astro b/src/pages/projekter/[slug].astro new file mode 100644 index 0000000..1c64ae7 --- /dev/null +++ b/src/pages/projekter/[slug].astro @@ -0,0 +1,59 @@ +--- +import { getCollection, render } from 'astro:content'; +import Base from '../../layouts/Base.astro'; +import { SITE } from '../../consts'; +import { softwareSourceCode, breadcrumb } from '../../seo'; + +export async function getStaticPaths() { + const sider = await getCollection('projekter', ({ data }) => !data.draft); + return sider.map((side) => ({ params: { slug: side.id }, props: { side } })); +} +const { side } = Astro.props; +const { Content } = await render(side); +const d = side.data; + +const url = new URL(`/projekter/${side.id}`, SITE.url).href; +const repo = `${SITE.giteaBase}/${SITE.giteaUser}/${d.repo}`; +const aur = d.aur.map((n) => ({ navn: n, url: `https://aur.archlinux.org/packages/${n}` })); +const published = d.date.toISOString(); +const modified = (d.updated ?? d.date).toISOString(); +const ld = softwareSourceCode({ + url, name: d.title, description: d.description, codeRepository: repo, + language: d.sprog, license: d.licens, platforms: d.platforme, + sameAs: aur.map((a) => a.url), dateCreated: published, dateModified: modified, keywords: d.tags, +}); +const crumbs = breadcrumb([ + { name: 'Forside', url: `${SITE.url}/` }, + { name: 'Projekter', url: `${SITE.url}/projekter` }, + { name: d.title, url }, +]); +--- + + +
+
+

{d.title}

+

{d.description}

+
+
Sprog
{d.sprog}
+
Licens
{d.licens}
+ {d.platforme.length > 0 &&
Kører på
{d.platforme.join(', ')}
} +
+
+ Kildekode på Gitea + {aur.map((a) => ( + AUR: {a.navn} + ))} + {d.aur.some((n) => n.endsWith('-bin')) && ( + Færdigbyggede pakker + )} +
+
+
+
+ diff --git a/src/pages/projekter/index.astro b/src/pages/projekter/index.astro new file mode 100644 index 0000000..809d50d --- /dev/null +++ b/src/pages/projekter/index.astro @@ -0,0 +1,68 @@ +--- +import { getCollection } from 'astro:content'; +import Base from '../../layouts/Base.astro'; +import { SITE, PROJECTS } from '../../consts'; + +// Repos med en projektside her på sitet linker dertil; resten går direkte til Gitea. +const sider = new Map( + (await getCollection('projekter', ({ data }) => !data.draft)).map((p) => [p.data.repo.toLowerCase(), p]), +); + +type Repo = { name: string; description: string | null; html_url: string; language: string | null; stars_count: number; updated_at: string; fork: boolean; archived: boolean; private: boolean; }; +let repos: Repo[] = []; +let error = false; +try { + const res = await fetch(`${SITE.giteaBase}/api/v1/users/${SITE.giteaUser}/repos?limit=50&sort=updated`, { + headers: { 'Accept': 'application/json' }, + }); + if (res.ok) { + const hide = new Set(PROJECTS.hide.map((s) => s.toLowerCase())); + const data = (await res.json()) as Repo[]; + repos = data + .filter((r) => !r.fork && !r.archived && !r.private && !hide.has(r.name.toLowerCase())); + // pinned først (i opsat rækkefølge), resten efter senest opdateret + const order = (n: string) => { + const i = PROJECTS.pinned.findIndex((p) => p.toLowerCase() === n.toLowerCase()); + return i === -1 ? Infinity : i; + }; + repos.sort((a, b) => { + const oa = order(a.name), ob = order(b.name); + if (oa !== ob) return oa - ob; + return +new Date(b.updated_at) - +new Date(a.updated_at); + }); + } else { error = true; } +} catch { error = true; } +--- + +

Projekter

+

Hentet live fra min Gitea.

+ + {error &&

Kunne ikke hente repositories lige nu.

} + + + diff --git a/src/pages/search-index.json.js b/src/pages/search-index.json.js index 92f335d..67a1a8a 100644 --- a/src/pages/search-index.json.js +++ b/src/pages/search-index.json.js @@ -3,10 +3,16 @@ import { getCollection } from 'astro:content'; export async function GET() { const posts = await getCollection('blog', ({ data }) => !data.draft); + const projekter = await getCollection('projekter', ({ data }) => !data.draft); const items = [ { type: 'page', title: 'Forside', url: '/' }, { type: 'page', title: 'CV', url: '/cv' }, { type: 'page', title: 'Projekter', url: '/projekter' }, + ...projekter.map((p) => ({ + type: 'page', title: `Projekt: ${p.data.title}`, url: `/projekter/${p.id}`, + extra: [p.data.sprog, ...p.data.aur, ...p.data.tags].join(' '), + })), + { type: 'page', title: 'Færdigbyggede pakker (tarballz)', url: '/tarballz/' }, { type: 'page', title: 'Blog', url: '/blog' }, { type: 'page', title: 'Tags', url: '/tags' }, { type: 'page', title: 'NemHandel-opslag', url: '/nemhandel' }, diff --git a/src/pages/ubl.astro b/src/pages/ubl.astro index fbf20b4..e2c34dc 100644 --- a/src/pages/ubl.astro +++ b/src/pages/ubl.astro @@ -22,6 +22,11 @@ const idx = profiles.map((p) => ({ EN 16931-status og kobler til profil-opslaget. Alt sker lokalt i din browser; intet sendes nogen steder.

+

+ Vil du læse hele fakturaen eller have den vedhæftede PDF ud, så har jeg skrevet + ubl-tools til terminalen, + også på Windows og macOS. +

Træk en .xml-fil hertil, eller diff --git a/src/seo.ts b/src/seo.ts index e4f76e0..20bb89c 100644 --- a/src/seo.ts +++ b/src/seo.ts @@ -159,3 +159,31 @@ export function collectionPage(opts: { url: string; name: string; description: s about: { '@id': personId }, }; } + +// SoftwareSourceCode — a project page for a tool the Person wrote and maintains. +// codeRepository points at Gitea; sameAs lists the package pages (AUR) for the same tool. +export function softwareSourceCode(opts: { + url: string; name: string; description: string; codeRepository: string; + language: string; license: string; platforms: string[]; sameAs: string[]; + dateCreated: string; dateModified: string; keywords: string[]; +}) { + return { + '@context': 'https://schema.org', + '@type': 'SoftwareSourceCode', + '@id': `${opts.url}#software`, + url: opts.url, + name: opts.name, + description: opts.description, + codeRepository: opts.codeRepository, + programmingLanguage: opts.language, + license: opts.license, + runtimePlatform: opts.platforms, + sameAs: opts.sameAs, + dateCreated: opts.dateCreated, + dateModified: opts.dateModified, + keywords: opts.keywords, + inLanguage: 'da-DK', + author: { '@id': personId }, + maintainer: { '@id': personId }, + }; +}