forked from tangled.org/core
Monorepo for Tangled

appview: pages/templates/repo: tabular commit log

anirudh.fi 9c1dd0c0 5d021119

verified
Changed files
+438 -517
appview
pages
templates
repo
state
+1
.gitignore
··· 6 6 appview/pages/static/* 7 7 result 8 8 !.gitkeep 9 + out/
+59 -150
appview/pages/templates/repo/log.html
··· 1 1 {{ define "title" }}commits &middot; {{ .RepoInfo.FullName }}{{ end }} 2 - 3 2 {{ define "repoContent" }} 4 - <section id="commit-message"> 5 - {{ $commit := index .Commits 0 }} 6 - {{ $messageParts := splitN $commit.Message "\n\n" 2 }} 7 - <div> 8 - <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white"> 9 - <p class="pb-5">{{ index $messageParts 0 }}</p> 10 - {{ if gt (len $messageParts) 1 }} 11 - <p class="mt-1 text-sm cursor-text pb-5"> 12 - {{ nl2br (unwrapText (index $messageParts 1)) }} 13 - </p> 14 - {{ end }} 15 - </a> 16 - </div> 17 - 18 - <div class="text-sm text-gray-500 dark:text-gray-400"> 19 - <span class="font-mono"> 20 - <a 21 - href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" 22 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 23 - >{{ slice $commit.Hash.String }}</a 24 - > 25 - </span> 26 - <span class="mx-2 before:content-['·'] before:select-none"></span> 27 - <span> 28 - {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 29 - {{ if $didOrHandle }} 30 - <a 31 - href="/{{ $didOrHandle }}" 32 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 33 - >{{ $didOrHandle }}</a 34 - > 35 - {{ else }} 36 - <a 37 - href="mailto:{{ $commit.Author.Email }}" 38 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 39 - >{{ $commit.Author.Name }}</a 40 - > 41 - {{ end }} 42 - </span> 43 - <div 44 - class="inline-block px-1 select-none after:content-['·']" 45 - ></div> 46 - <span>{{ timeFmt $commit.Author.When }}</span> 47 - </div> 48 - </section> 49 - {{ end }} 50 - 51 - {{ define "repoAfter" }} 52 - <main> 53 - <div id="commit-log" class="flex-1 relative"> 54 - <div class="absolute left-8 top-0 bottom-0 w-px bg-gray-300 dark:bg-gray-600"></div> 55 - {{ $end := length .Commits }} 56 - {{ $commits := subslice .Commits 1 $end }} 57 - {{ range $commits }} 58 - <div class="flex flex-row justify-between items-center"> 59 - <div 60 - class="relative w-full px-4 py-4 mt-4 rounded-sm bg-white dark:bg-gray-800" 61 - > 62 - <div id="commit-message"> 63 - {{ $messageParts := splitN .Message "\n\n" 2 }} 64 - <div class="text-base cursor-pointer"> 65 - <div> 66 - <div> 67 - <a 68 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 69 - class="inline no-underline hover:underline dark:text-white" 70 - >{{ index $messageParts 0 }}</a 71 - > 72 - {{ if gt (len $messageParts) 1 }} 73 - 74 - <button 75 - class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" 76 - hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')" 77 - > 78 - {{ i "ellipsis" "w-3 h-3" }} 79 - </button> 80 - {{ end }} 81 - </div> 82 - {{ if gt (len $messageParts) 1 }} 83 - <p 84 - class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 85 - > 86 - {{ nl2br (index $messageParts 1) }} 87 - </p> 88 - {{ end }} 89 - </div> 90 - </div> 3 + <section id="commit-table"> 4 + <table class="w-full border-collapse"> 5 + <thead class="bg-gray-100 dark:bg-gray-700"> 6 + <tr> 7 + <th class="px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Author</th> 8 + <th class="px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Commit</th> 9 + <th class="px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Message</th> 10 + <th class="px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Date</th> 11 + </tr> 12 + </thead> 13 + <tbody> 14 + {{ range $index, $commit := .Commits }} 15 + {{ $messageParts := splitN $commit.Message "\n\n" 2 }} 16 + <tr class="border-b border-gray-200 dark:border-gray-700"> 17 + <td class="px-4 py-3 align-top"> 18 + {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 19 + {{ if $didOrHandle }} 20 + <a href="/{{ $didOrHandle }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $didOrHandle }}</a> 21 + {{ else }} 22 + <a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a> 23 + {{ end }} 24 + </td> 25 + <td class="px-4 py-3 align-top font-mono flex items-end"> 26 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ slice $commit.Hash.String 0 8 }}</a> 27 + <div class="inline-flex"> 28 + <button class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" 29 + title="Copy SHA" 30 + onclick="navigator.clipboard.writeText('{{ $commit.Hash.String }}'); this.innerHTML=`{{ i "copy-check" "w-4 h-4" }}`; setTimeout(() => this.innerHTML=`{{ i "copy" "w-4 h-4" }}`, 1500)"> 31 + {{ i "copy" "w-4 h-4" }} 32 + </button> 33 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $commit.Hash.String }}" class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Browse repository at this commit"> 34 + {{ i "folder-code" "w-4 h-4" }} 35 + </a> 91 36 </div> 92 - 93 - <div class="text-sm text-gray-500 dark:text-gray-400 mt-3"> 94 - <span class="font-mono"> 95 - <a 96 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 97 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 98 - >{{ slice .Hash.String 0 8 }}</a 99 - > 100 - </span> 101 - <span 102 - class="mx-2 before:content-['·'] before:select-none" 103 - ></span> 104 - <span> 105 - {{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }} 106 - {{ if $didOrHandle }} 107 - <a 108 - href="/{{ $didOrHandle }}" 109 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 110 - >{{ $didOrHandle }}</a 111 - > 112 - {{ else }} 113 - <a 114 - href="mailto:{{ .Author.Email }}" 115 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 116 - >{{ .Author.Name }}</a 117 - > 37 + </td> 38 + <td class="px-4 py-3 align-top"> 39 + {{ if eq $index 0 }} 40 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline"> 41 + <p>{{ index $messageParts 0 }}</p> 42 + {{ if gt (len $messageParts) 1 }}<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (unwrapText (index $messageParts 1)) }}</p>{{ end }} 43 + </a> 44 + {{ else }} 45 + <div> 46 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a> 47 + {{ if gt (len $messageParts) 1 }} 48 + <button class="ml-2 py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" hx-on:click="this.nextElementSibling.classList.toggle('hidden')">{{ i "ellipsis" "w-3 h-3" }}</button> 49 + <p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p> 118 50 {{ end }} 119 - </span> 120 - <div 121 - class="inline-block px-1 select-none after:content-['·']" 122 - ></div> 123 - <span>{{ timeFmt .Author.When }}</span> 124 - </div> 125 - </div> 126 - </div> 127 - {{ end }} 128 - </div> 129 - 130 - {{ $commits_len := len .Commits }} 131 - <div class="flex justify-end mt-4 gap-2"> 132 - {{ if gt .Page 1 }} 133 - <a 134 - class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 135 - hx-boost="true" 136 - onclick="window.location.href = window.location.pathname + '?page={{ sub .Page 1 }}'" 137 - > 138 - {{ i "chevron-left" "w-4 h-4" }} 139 - previous 140 - </a> 141 - {{ else }} 142 - <div></div> 51 + </div> 52 + {{ end }} 53 + </td> 54 + <td class="px-4 py-3 align-top text-gray-500 dark:text-gray-400">{{ timeFmt $commit.Author.When }}</td> 55 + </tr> 143 56 {{ end }} 57 + </tbody> 58 + </table> 59 + </section> 144 60 145 - {{ if eq $commits_len 30 }} 146 - <a 147 - class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 148 - hx-boost="true" 149 - onclick="window.location.href = window.location.pathname + '?page={{ add .Page 1 }}'" 150 - > 151 - next 152 - {{ i "chevron-right" "w-4 h-4" }} 153 - </a> 154 - {{ end }} 155 - </div> 156 - </main> 61 + {{ $commits_len := len .Commits }} 62 + <div class="flex justify-end mt-4 gap-2"> 63 + {{ if gt .Page 1 }}<a class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" hx-boost="true" onclick="window.location.href = window.location.pathname + '?page={{ sub .Page 1 }}'">{{ i "chevron-left" "w-4 h-4" }} previous</a>{{ else }}<div></div>{{ end }} 64 + {{ if eq $commits_len 60 }}<a class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" hx-boost="true" onclick="window.location.href = window.location.pathname + '?page={{ add .Page 1 }}'">next {{ i "chevron-right" "w-4 h-4" }}</a>{{ end }} 65 + </div> 157 66 {{ end }}
+1 -1
appview/state/repo.go
··· 117 117 protocol = "https" 118 118 } 119 119 120 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/log/%s?page=%d&per_page=30", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, page)) 120 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/log/%s?page=%d&per_page=60", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, page)) 121 121 if err != nil { 122 122 log.Println("failed to reach knotserver", err) 123 123 return
+377 -366
flake.nix
··· 29 29 }; 30 30 }; 31 31 32 - outputs = { 33 - self, 34 - nixpkgs, 35 - indigo, 36 - htmx-src, 37 - lucide-src, 38 - gitignore, 39 - inter-fonts-src, 40 - ibm-plex-mono-src, 41 - }: let 42 - supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; 43 - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 44 - nixpkgsFor = forAllSystems (system: 45 - import nixpkgs { 46 - inherit system; 47 - overlays = [self.overlays.default]; 48 - }); 49 - inherit (gitignore.lib) gitignoreSource; 50 - in { 51 - overlays.default = final: prev: let 52 - goModHash = "sha256-EilWxfqrcKDaSR5zA3ZuDSCq7V+/IfWpKPu8HWhpndA="; 53 - buildCmdPackage = name: 54 - final.buildGoModule { 55 - pname = name; 56 - version = "0.1.0"; 57 - src = gitignoreSource ./.; 58 - subPackages = ["cmd/${name}"]; 59 - vendorHash = goModHash; 60 - CGO_ENABLED = 0; 61 - }; 62 - in { 63 - indigo-lexgen = final.buildGoModule { 64 - pname = "indigo-lexgen"; 65 - version = "0.1.0"; 66 - src = indigo; 67 - subPackages = ["cmd/lexgen"]; 68 - vendorHash = "sha256-pGc29fgJFq8LP7n/pY1cv6ExZl88PAeFqIbFEhB3xXs="; 69 - doCheck = false; 70 - }; 71 - 72 - appview = with final; 73 - final.pkgsStatic.buildGoModule { 74 - pname = "appview"; 75 - version = "0.1.0"; 76 - src = gitignoreSource ./.; 77 - postUnpack = '' 78 - pushd source 79 - mkdir -p appview/pages/static/{fonts,icons} 80 - cp -f ${htmx-src} appview/pages/static/htmx.min.js 81 - cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 82 - cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 83 - cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 84 - cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 85 - ${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o appview/pages/static/tw.css 86 - popd 87 - ''; 88 - doCheck = false; 89 - subPackages = ["cmd/appview"]; 90 - vendorHash = goModHash; 91 - CGO_ENABLED = 1; 92 - stdenv = pkgsStatic.stdenv; 93 - }; 94 - 95 - knotserver = with final; 96 - final.pkgsStatic.buildGoModule { 97 - pname = "knotserver"; 98 - version = "0.1.0"; 99 - src = gitignoreSource ./.; 100 - nativeBuildInputs = [final.makeWrapper]; 101 - subPackages = ["cmd/knotserver"]; 102 - vendorHash = goModHash; 103 - installPhase = '' 104 - runHook preInstall 105 - 106 - mkdir -p $out/bin 107 - cp $GOPATH/bin/knotserver $out/bin/knotserver 108 - 109 - wrapProgram $out/bin/knotserver \ 110 - --prefix PATH : ${pkgs.git}/bin 111 - 112 - runHook postInstall 113 - ''; 114 - CGO_ENABLED = 1; 115 - }; 116 - knotserver-unwrapped = final.pkgsStatic.buildGoModule { 117 - pname = "knotserver"; 118 - version = "0.1.0"; 119 - src = gitignoreSource ./.; 120 - subPackages = ["cmd/knotserver"]; 121 - vendorHash = goModHash; 122 - CGO_ENABLED = 1; 123 - }; 124 - repoguard = buildCmdPackage "repoguard"; 125 - keyfetch = buildCmdPackage "keyfetch"; 126 - }; 127 - packages = forAllSystems (system: { 128 - inherit 129 - (nixpkgsFor."${system}") 130 - indigo-lexgen 131 - appview 132 - knotserver 133 - knotserver-unwrapped 134 - repoguard 135 - keyfetch 136 - ; 137 - }); 138 - defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); 139 - formatter = forAllSystems (system: nixpkgsFor."${system}".alejandra); 140 - devShells = forAllSystems (system: let 141 - pkgs = nixpkgsFor.${system}; 142 - staticShell = pkgs.mkShell.override { 143 - stdenv = pkgs.pkgsStatic.stdenv; 144 - }; 145 - in { 146 - default = staticShell { 147 - nativeBuildInputs = [ 148 - pkgs.go 149 - pkgs.air 150 - pkgs.gopls 151 - pkgs.httpie 152 - pkgs.indigo-lexgen 153 - pkgs.litecli 154 - pkgs.websocat 155 - pkgs.tailwindcss 156 - pkgs.nixos-shell 157 - ]; 158 - shellHook = '' 159 - mkdir -p appview/pages/static/{fonts,icons} 160 - cp -f ${htmx-src} appview/pages/static/htmx.min.js 161 - cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 162 - cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 163 - cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 164 - cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 165 - ''; 166 - }; 167 - }); 168 - apps = forAllSystems (system: let 169 - pkgs = nixpkgsFor."${system}"; 170 - air-watcher = name: 171 - pkgs.writeShellScriptBin "run" 172 - '' 173 - ${pkgs.air}/bin/air -c /dev/null \ 174 - -build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ 175 - -build.bin "./out/${name}.out" \ 176 - -build.include_ext "go" 177 - ''; 178 - tailwind-watcher = 179 - pkgs.writeShellScriptBin "run" 180 - '' 181 - ${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css 182 - ''; 183 - in { 184 - watch-appview = { 185 - type = "app"; 186 - program = ''${air-watcher "appview"}/bin/run''; 187 - }; 188 - watch-knotserver = { 189 - type = "app"; 190 - program = ''${air-watcher "knotserver"}/bin/run''; 191 - }; 192 - watch-tailwind = { 193 - type = "app"; 194 - program = ''${tailwind-watcher}/bin/run''; 195 - }; 196 - }); 197 - 198 - nixosModules.appview = { 199 - config, 200 - pkgs, 201 - lib, 202 - ... 32 + outputs = 33 + { self 34 + , nixpkgs 35 + , indigo 36 + , htmx-src 37 + , lucide-src 38 + , gitignore 39 + , inter-fonts-src 40 + , ibm-plex-mono-src 41 + , 203 42 }: 204 - with lib; { 205 - options = { 206 - services.tangled-appview = { 207 - enable = mkOption { 208 - type = types.bool; 209 - default = false; 210 - description = "Enable tangled appview"; 43 + let 44 + supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; 45 + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 46 + nixpkgsFor = forAllSystems (system: 47 + import nixpkgs { 48 + inherit system; 49 + overlays = [ self.overlays.default ]; 50 + }); 51 + inherit (gitignore.lib) gitignoreSource; 52 + in 53 + { 54 + overlays.default = final: prev: 55 + let 56 + goModHash = "sha256-EilWxfqrcKDaSR5zA3ZuDSCq7V+/IfWpKPu8HWhpndA="; 57 + buildCmdPackage = name: 58 + final.buildGoModule { 59 + pname = name; 60 + version = "0.1.0"; 61 + src = gitignoreSource ./.; 62 + subPackages = [ "cmd/${name}" ]; 63 + vendorHash = goModHash; 64 + CGO_ENABLED = 0; 211 65 }; 212 - port = mkOption { 213 - type = types.int; 214 - default = 3000; 215 - description = "Port to run the appview on"; 216 - }; 217 - cookie_secret = mkOption { 218 - type = types.str; 219 - default = "00000000000000000000000000000000"; 220 - description = "Cookie secret"; 66 + in 67 + { 68 + indigo-lexgen = final.buildGoModule { 69 + pname = "indigo-lexgen"; 70 + version = "0.1.0"; 71 + src = indigo; 72 + subPackages = [ "cmd/lexgen" ]; 73 + vendorHash = "sha256-pGc29fgJFq8LP7n/pY1cv6ExZl88PAeFqIbFEhB3xXs="; 74 + doCheck = false; 75 + }; 76 + 77 + appview = with final; 78 + final.pkgsStatic.buildGoModule { 79 + pname = "appview"; 80 + version = "0.1.0"; 81 + src = gitignoreSource ./.; 82 + postUnpack = '' 83 + pushd source 84 + mkdir -p appview/pages/static/{fonts,icons} 85 + cp -f ${htmx-src} appview/pages/static/htmx.min.js 86 + cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 87 + cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 88 + cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 89 + cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 90 + ${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o appview/pages/static/tw.css 91 + popd 92 + ''; 93 + doCheck = false; 94 + subPackages = [ "cmd/appview" ]; 95 + vendorHash = goModHash; 96 + CGO_ENABLED = 1; 97 + stdenv = pkgsStatic.stdenv; 221 98 }; 222 - }; 223 - }; 224 99 225 - config = mkIf config.services.tangled-appview.enable { 226 - systemd.services.tangled-appview = { 227 - description = "tangled appview service"; 228 - wantedBy = ["multi-user.target"]; 100 + knotserver = with final; 101 + final.pkgsStatic.buildGoModule { 102 + pname = "knotserver"; 103 + version = "0.1.0"; 104 + src = gitignoreSource ./.; 105 + nativeBuildInputs = [ final.makeWrapper ]; 106 + subPackages = [ "cmd/knotserver" ]; 107 + vendorHash = goModHash; 108 + installPhase = '' 109 + runHook preInstall 229 110 230 - serviceConfig = { 231 - ListenStream = "0.0.0.0:${toString config.services.tangled-appview.port}"; 232 - ExecStart = "${self.packages.${pkgs.system}.appview}/bin/appview"; 233 - Restart = "always"; 234 - }; 111 + mkdir -p $out/bin 112 + cp $GOPATH/bin/knotserver $out/bin/knotserver 235 113 236 - environment = { 237 - TANGLED_DB_PATH = "appview.db"; 238 - TANGLED_COOKIE_SECRET = config.services.tangled-appview.cookie_secret; 114 + wrapProgram $out/bin/knotserver \ 115 + --prefix PATH : ${pkgs.git}/bin 116 + 117 + runHook postInstall 118 + ''; 119 + CGO_ENABLED = 1; 239 120 }; 121 + knotserver-unwrapped = final.pkgsStatic.buildGoModule { 122 + pname = "knotserver"; 123 + version = "0.1.0"; 124 + src = gitignoreSource ./.; 125 + subPackages = [ "cmd/knotserver" ]; 126 + vendorHash = goModHash; 127 + CGO_ENABLED = 1; 240 128 }; 129 + repoguard = buildCmdPackage "repoguard"; 130 + keyfetch = buildCmdPackage "keyfetch"; 241 131 }; 242 - }; 132 + packages = forAllSystems (system: { 133 + inherit 134 + (nixpkgsFor."${system}") 135 + indigo-lexgen 136 + appview 137 + knotserver 138 + knotserver-unwrapped 139 + repoguard 140 + keyfetch 141 + ; 142 + }); 143 + defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); 144 + formatter = forAllSystems (system: nixpkgsFor."${system}".alejandra); 145 + devShells = forAllSystems (system: 146 + let 147 + pkgs = nixpkgsFor.${system}; 148 + staticShell = pkgs.mkShell.override { 149 + stdenv = pkgs.pkgsStatic.stdenv; 150 + }; 151 + in 152 + { 153 + default = staticShell { 154 + nativeBuildInputs = [ 155 + pkgs.go 156 + pkgs.air 157 + pkgs.gopls 158 + pkgs.httpie 159 + pkgs.indigo-lexgen 160 + pkgs.litecli 161 + pkgs.websocat 162 + pkgs.tailwindcss 163 + pkgs.nixos-shell 164 + ]; 165 + shellHook = '' 166 + mkdir -p appview/pages/static/{fonts,icons} 167 + cp -f ${htmx-src} appview/pages/static/htmx.min.js 168 + cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 169 + cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 170 + cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 171 + cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 172 + ''; 173 + }; 174 + }); 175 + apps = forAllSystems (system: 176 + let 177 + pkgs = nixpkgsFor."${system}"; 178 + air-watcher = name: 179 + pkgs.writeShellScriptBin "run" 180 + '' 181 + TANGLED_DEV=true ${pkgs.air}/bin/air -c /dev/null \ 182 + -build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ 183 + -build.bin "./out/${name}.out" \ 184 + -build.include_ext "go" 185 + ''; 186 + tailwind-watcher = 187 + pkgs.writeShellScriptBin "run" 188 + '' 189 + ${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css 190 + ''; 191 + in 192 + { 193 + watch-appview = { 194 + type = "app"; 195 + program = ''${air-watcher "appview"}/bin/run''; 196 + }; 197 + watch-knotserver = { 198 + type = "app"; 199 + program = ''${air-watcher "knotserver"}/bin/run''; 200 + }; 201 + watch-tailwind = { 202 + type = "app"; 203 + program = ''${tailwind-watcher}/bin/run''; 204 + }; 205 + }); 243 206 244 - nixosModules.knotserver = { 245 - config, 246 - pkgs, 247 - lib, 248 - ... 249 - }: let 250 - cfg = config.services.tangled-knotserver; 251 - in 252 - with lib; { 253 - options = { 254 - services.tangled-knotserver = { 255 - enable = mkOption { 256 - type = types.bool; 257 - default = false; 258 - description = "Enable a tangled knotserver"; 207 + nixosModules.appview = 208 + { config 209 + , pkgs 210 + , lib 211 + , ... 212 + }: 213 + with lib; { 214 + options = { 215 + services.tangled-appview = { 216 + enable = mkOption { 217 + type = types.bool; 218 + default = false; 219 + description = "Enable tangled appview"; 220 + }; 221 + port = mkOption { 222 + type = types.int; 223 + default = 3000; 224 + description = "Port to run the appview on"; 225 + }; 226 + cookie_secret = mkOption { 227 + type = types.str; 228 + default = "00000000000000000000000000000000"; 229 + description = "Cookie secret"; 230 + }; 231 + }; 259 232 }; 260 233 261 - appviewEndpoint = mkOption { 262 - type = types.str; 263 - default = "https://tangled.sh"; 264 - description = "Appview endpoint"; 265 - }; 234 + config = mkIf config.services.tangled-appview.enable { 235 + systemd.services.tangled-appview = { 236 + description = "tangled appview service"; 237 + wantedBy = [ "multi-user.target" ]; 266 238 267 - gitUser = mkOption { 268 - type = types.str; 269 - default = "git"; 270 - description = "User that hosts git repos and performs git operations"; 271 - }; 239 + serviceConfig = { 240 + ListenStream = "0.0.0.0:${toString config.services.tangled-appview.port}"; 241 + ExecStart = "${self.packages.${pkgs.system}.appview}/bin/appview"; 242 + Restart = "always"; 243 + }; 272 244 273 - openFirewall = mkOption { 274 - type = types.bool; 275 - default = true; 276 - description = "Open port 22 in the firewall for ssh"; 277 - }; 278 - 279 - stateDir = mkOption { 280 - type = types.path; 281 - default = "/home/${cfg.gitUser}"; 282 - description = "Tangled knot data directory"; 283 - }; 284 - 285 - repo = { 286 - scanPath = mkOption { 287 - type = types.path; 288 - default = cfg.stateDir; 289 - description = "Path where repositories are scanned from"; 245 + environment = { 246 + TANGLED_DB_PATH = "appview.db"; 247 + TANGLED_COOKIE_SECRET = config.services.tangled-appview.cookie_secret; 248 + }; 290 249 }; 250 + }; 251 + }; 291 252 292 - mainBranch = mkOption { 293 - type = types.str; 294 - default = "main"; 295 - description = "Default branch name for repositories"; 253 + nixosModules.knotserver = 254 + { config 255 + , pkgs 256 + , lib 257 + , ... 258 + }: 259 + let 260 + cfg = config.services.tangled-knotserver; 261 + in 262 + with lib; { 263 + options = { 264 + services.tangled-knotserver = { 265 + enable = mkOption { 266 + type = types.bool; 267 + default = false; 268 + description = "Enable a tangled knotserver"; 296 269 }; 297 - }; 298 270 299 - server = { 300 - listenAddr = mkOption { 271 + appviewEndpoint = mkOption { 301 272 type = types.str; 302 - default = "0.0.0.0:5555"; 303 - description = "Address to listen on"; 273 + default = "https://tangled.sh"; 274 + description = "Appview endpoint"; 304 275 }; 305 276 306 - internalListenAddr = mkOption { 277 + gitUser = mkOption { 307 278 type = types.str; 308 - default = "127.0.0.1:5444"; 309 - description = "Internal address for inter-service communication"; 279 + default = "git"; 280 + description = "User that hosts git repos and performs git operations"; 310 281 }; 311 282 312 - secretFile = mkOption { 313 - type = lib.types.path; 314 - example = "KNOT_SERVER_SECRET=<hash>"; 315 - description = "File containing secret key provided by appview (required)"; 283 + openFirewall = mkOption { 284 + type = types.bool; 285 + default = true; 286 + description = "Open port 22 in the firewall for ssh"; 316 287 }; 317 288 318 - dbPath = mkOption { 289 + stateDir = mkOption { 319 290 type = types.path; 320 - default = "${cfg.stateDir}/knotserver.db"; 321 - description = "Path to the database file"; 291 + default = "/home/${cfg.gitUser}"; 292 + description = "Tangled knot data directory"; 322 293 }; 323 294 324 - hostname = mkOption { 325 - type = types.str; 326 - example = "knot.tangled.sh"; 327 - description = "Hostname for the server (required)"; 295 + repo = { 296 + scanPath = mkOption { 297 + type = types.path; 298 + default = cfg.stateDir; 299 + description = "Path where repositories are scanned from"; 300 + }; 301 + 302 + mainBranch = mkOption { 303 + type = types.str; 304 + default = "main"; 305 + description = "Default branch name for repositories"; 306 + }; 328 307 }; 329 308 330 - dev = mkOption { 331 - type = types.bool; 332 - default = false; 333 - description = "Enable development mode (disables signature verification)"; 309 + server = { 310 + listenAddr = mkOption { 311 + type = types.str; 312 + default = "0.0.0.0:5555"; 313 + description = "Address to listen on"; 314 + }; 315 + 316 + internalListenAddr = mkOption { 317 + type = types.str; 318 + default = "127.0.0.1:5444"; 319 + description = "Internal address for inter-service communication"; 320 + }; 321 + 322 + secretFile = mkOption { 323 + type = lib.types.path; 324 + example = "KNOT_SERVER_SECRET=<hash>"; 325 + description = "File containing secret key provided by appview (required)"; 326 + }; 327 + 328 + dbPath = mkOption { 329 + type = types.path; 330 + default = "${cfg.stateDir}/knotserver.db"; 331 + description = "Path to the database file"; 332 + }; 333 + 334 + hostname = mkOption { 335 + type = types.str; 336 + example = "knot.tangled.sh"; 337 + description = "Hostname for the server (required)"; 338 + }; 339 + 340 + dev = mkOption { 341 + type = types.bool; 342 + default = false; 343 + description = "Enable development mode (disables signature verification)"; 344 + }; 334 345 }; 335 346 }; 336 347 }; 337 - }; 338 348 339 - config = mkIf cfg.enable { 340 - environment.systemPackages = with pkgs; [git]; 349 + config = mkIf cfg.enable { 350 + environment.systemPackages = with pkgs; [ git ]; 341 351 342 - system.activationScripts.gitConfig = '' 343 - mkdir -p "${cfg.repo.scanPath}" 344 - chown -R ${cfg.gitUser}:${cfg.gitUser} \ 345 - "${cfg.repo.scanPath}" 352 + system.activationScripts.gitConfig = '' 353 + mkdir -p "${cfg.repo.scanPath}" 354 + chown -R ${cfg.gitUser}:${cfg.gitUser} \ 355 + "${cfg.repo.scanPath}" 346 356 347 - mkdir -p "${cfg.stateDir}/.config/git" 348 - cat > "${cfg.stateDir}/.config/git/config" << EOF 349 - [user] 350 - name = Git User 351 - email = git@example.com 352 - EOF 353 - chown -R ${cfg.gitUser}:${cfg.gitUser} \ 354 - "${cfg.stateDir}" 355 - ''; 357 + mkdir -p "${cfg.stateDir}/.config/git" 358 + cat > "${cfg.stateDir}/.config/git/config" << EOF 359 + [user] 360 + name = Git User 361 + email = git@example.com 362 + EOF 363 + chown -R ${cfg.gitUser}:${cfg.gitUser} \ 364 + "${cfg.stateDir}" 365 + ''; 356 366 357 - users.users.${cfg.gitUser} = { 358 - isSystemUser = true; 359 - useDefaultShell = true; 360 - home = cfg.stateDir; 361 - createHome = true; 362 - group = cfg.gitUser; 363 - }; 367 + users.users.${cfg.gitUser} = { 368 + isSystemUser = true; 369 + useDefaultShell = true; 370 + home = cfg.stateDir; 371 + createHome = true; 372 + group = cfg.gitUser; 373 + }; 364 374 365 - users.groups.${cfg.gitUser} = {}; 375 + users.groups.${cfg.gitUser} = { }; 366 376 367 - services.openssh = { 368 - enable = true; 369 - extraConfig = '' 370 - Match User ${cfg.gitUser} 371 - AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 372 - AuthorizedKeysCommandUser nobody 373 - ''; 374 - }; 377 + services.openssh = { 378 + enable = true; 379 + extraConfig = '' 380 + Match User ${cfg.gitUser} 381 + AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 382 + AuthorizedKeysCommandUser nobody 383 + ''; 384 + }; 375 385 376 - environment.etc."ssh/keyfetch_wrapper" = { 377 - mode = "0555"; 378 - text = '' 379 - #!${pkgs.stdenv.shell} 380 - ${self.packages.${pkgs.system}.keyfetch}/bin/keyfetch \ 381 - -repoguard-path ${self.packages.${pkgs.system}.repoguard}/bin/repoguard \ 382 - -internal-api "http://${cfg.server.internalListenAddr}" \ 383 - -git-dir "${cfg.repo.scanPath}" \ 384 - -log-path /tmp/repoguard.log 385 - ''; 386 - }; 386 + environment.etc."ssh/keyfetch_wrapper" = { 387 + mode = "0555"; 388 + text = '' 389 + #!${pkgs.stdenv.shell} 390 + ${self.packages.${pkgs.system}.keyfetch}/bin/keyfetch \ 391 + -repoguard-path ${self.packages.${pkgs.system}.repoguard}/bin/repoguard \ 392 + -internal-api "http://${cfg.server.internalListenAddr}" \ 393 + -git-dir "${cfg.repo.scanPath}" \ 394 + -log-path /tmp/repoguard.log 395 + ''; 396 + }; 387 397 388 - systemd.services.knotserver = { 389 - description = "knotserver service"; 390 - after = ["network.target" "sshd.service"]; 391 - wantedBy = ["multi-user.target"]; 392 - serviceConfig = { 393 - User = cfg.gitUser; 394 - WorkingDirectory = cfg.stateDir; 395 - Environment = [ 396 - "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 397 - "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 398 - "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 399 - "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 400 - "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 401 - "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 402 - "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 403 - ]; 404 - EnvironmentFile = cfg.server.secretFile; 405 - ExecStart = "${self.packages.${pkgs.system}.knotserver}/bin/knotserver"; 406 - Restart = "always"; 398 + systemd.services.knotserver = { 399 + description = "knotserver service"; 400 + after = [ "network.target" "sshd.service" ]; 401 + wantedBy = [ "multi-user.target" ]; 402 + serviceConfig = { 403 + User = cfg.gitUser; 404 + WorkingDirectory = cfg.stateDir; 405 + Environment = [ 406 + "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 407 + "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 408 + "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 409 + "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 410 + "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 411 + "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 412 + "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 413 + ]; 414 + EnvironmentFile = cfg.server.secretFile; 415 + ExecStart = "${self.packages.${pkgs.system}.knotserver}/bin/knotserver"; 416 + Restart = "always"; 417 + }; 407 418 }; 408 - }; 409 419 410 - networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22]; 420 + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ 22 ]; 421 + }; 411 422 }; 412 - }; 413 423 414 - nixosConfigurations.knotVM = nixpkgs.lib.nixosSystem { 415 - system = "x86_64-linux"; 416 - modules = [ 417 - self.nixosModules.knotserver 418 - ({ 419 - config, 420 - pkgs, 421 - ... 422 - }: { 423 - virtualisation.memorySize = 2048; 424 - virtualisation.cores = 2; 425 - services.getty.autologinUser = "root"; 426 - environment.systemPackages = with pkgs; [curl vim git]; 427 - systemd.tmpfiles.rules = let 428 - u = config.services.tangled-knotserver.gitUser; 429 - g = config.services.tangled-knotserver.gitUser; 430 - in [ 431 - "d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first 432 - "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=5b42390da4c6659f34c9a545adebd8af82c4a19960d735f651e3d582623ba9f2" 433 - ]; 434 - services.tangled-knotserver = { 435 - enable = true; 436 - server = { 437 - secretFile = "/var/lib/knotserver/secret"; 438 - hostname = "localhost:6000"; 439 - listenAddr = "0.0.0.0:6000"; 424 + nixosConfigurations.knotVM = nixpkgs.lib.nixosSystem { 425 + system = "x86_64-linux"; 426 + modules = [ 427 + self.nixosModules.knotserver 428 + ({ config 429 + , pkgs 430 + , ... 431 + }: { 432 + virtualisation.memorySize = 2048; 433 + virtualisation.cores = 2; 434 + services.getty.autologinUser = "root"; 435 + environment.systemPackages = with pkgs; [ curl vim git ]; 436 + systemd.tmpfiles.rules = 437 + let 438 + u = config.services.tangled-knotserver.gitUser; 439 + g = config.services.tangled-knotserver.gitUser; 440 + in 441 + [ 442 + "d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first 443 + "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=5b42390da4c6659f34c9a545adebd8af82c4a19960d735f651e3d582623ba9f2" 444 + ]; 445 + services.tangled-knotserver = { 446 + enable = true; 447 + server = { 448 + secretFile = "/var/lib/knotserver/secret"; 449 + hostname = "localhost:6000"; 450 + listenAddr = "0.0.0.0:6000"; 451 + }; 440 452 }; 441 - }; 442 - }) 443 - ]; 453 + }) 454 + ]; 455 + }; 444 456 }; 445 - }; 446 457 }