commit d7c720a

Artur Manuel  ·  2026-04-20 02:33:25 +0000 UTC
parent d7c720a
initialise repository
14 files changed,  +784, -0
A go.mod
+3, -0
1@@ -0,0 +1,3 @@
2+[*.go]
3+indent_size = 8
4+indent_style = tab
+287, -0
  1@@ -0,0 +1,287 @@
  2+                      EUROPEAN UNION PUBLIC LICENCE v. 1.2
  3+                      EUPL © the European Union 2007, 2016
  4+
  5+This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
  6+below) which is provided under the terms of this Licence. Any use of the Work,
  7+other than as authorised under this Licence is prohibited (to the extent such
  8+use is covered by a right of the copyright holder of the Work).
  9+
 10+The Work is provided under the terms of this Licence when the Licensor (as
 11+defined below) has placed the following notice immediately following the
 12+copyright notice for the Work:
 13+
 14+        Licensed under the EUPL
 15+
 16+or has expressed by any other means his willingness to license under the EUPL.
 17+
 18+1. Definitions
 19+
 20+In this Licence, the following terms have the following meaning:
 21+
 22+- ‘The Licence’: this Licence.
 23+
 24+- ‘The Original Work’: the work or software distributed or communicated by the
 25+  Licensor under this Licence, available as Source Code and also as Executable
 26+  Code as the case may be.
 27+
 28+- ‘Derivative Works’: the works or software that could be created by the
 29+  Licensee, based upon the Original Work or modifications thereof. This Licence
 30+  does not define the extent of modification or dependence on the Original Work
 31+  required in order to classify a work as a Derivative Work; this extent is
 32+  determined by copyright law applicable in the country mentioned in Article 15.
 33+
 34+- ‘The Work’: the Original Work or its Derivative Works.
 35+
 36+- ‘The Source Code’: the human-readable form of the Work which is the most
 37+  convenient for people to study and modify.
 38+
 39+- ‘The Executable Code’: any code which has generally been compiled and which is
 40+  meant to be interpreted by a computer as a program.
 41+
 42+- ‘The Licensor’: the natural or legal person that distributes or communicates
 43+  the Work under the Licence.
 44+
 45+- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
 46+  Licence, or otherwise contributes to the creation of a Derivative Work.
 47+
 48+- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
 49+  the Work under the terms of the Licence.
 50+
 51+- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
 52+  renting, distributing, communicating, transmitting, or otherwise making
 53+  available, online or offline, copies of the Work or providing access to its
 54+  essential functionalities at the disposal of any other natural or legal
 55+  person.
 56+
 57+2. Scope of the rights granted by the Licence
 58+
 59+The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
 60+sublicensable licence to do the following, for the duration of copyright vested
 61+in the Original Work:
 62+
 63+- use the Work in any circumstance and for all usage,
 64+- reproduce the Work,
 65+- modify the Work, and make Derivative Works based upon the Work,
 66+- communicate to the public, including the right to make available or display
 67+  the Work or copies thereof to the public and perform publicly, as the case may
 68+  be, the Work,
 69+- distribute the Work or copies thereof,
 70+- lend and rent the Work or copies thereof,
 71+- sublicense rights in the Work or copies thereof.
 72+
 73+Those rights can be exercised on any media, supports and formats, whether now
 74+known or later invented, as far as the applicable law permits so.
 75+
 76+In the countries where moral rights apply, the Licensor waives his right to
 77+exercise his moral right to the extent allowed by law in order to make effective
 78+the licence of the economic rights here above listed.
 79+
 80+The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
 81+any patents held by the Licensor, to the extent necessary to make use of the
 82+rights granted on the Work under this Licence.
 83+
 84+3. Communication of the Source Code
 85+
 86+The Licensor may provide the Work either in its Source Code form, or as
 87+Executable Code. If the Work is provided as Executable Code, the Licensor
 88+provides in addition a machine-readable copy of the Source Code of the Work
 89+along with each copy of the Work that the Licensor distributes or indicates, in
 90+a notice following the copyright notice attached to the Work, a repository where
 91+the Source Code is easily and freely accessible for as long as the Licensor
 92+continues to distribute or communicate the Work.
 93+
 94+4. Limitations on copyright
 95+
 96+Nothing in this Licence is intended to deprive the Licensee of the benefits from
 97+any exception or limitation to the exclusive rights of the rights owners in the
 98+Work, of the exhaustion of those rights or of other applicable limitations
 99+thereto.
100+
101+5. Obligations of the Licensee
102+
103+The grant of the rights mentioned above is subject to some restrictions and
104+obligations imposed on the Licensee. Those obligations are the following:
105+
106+Attribution right: The Licensee shall keep intact all copyright, patent or
107+trademarks notices and all notices that refer to the Licence and to the
108+disclaimer of warranties. The Licensee must include a copy of such notices and a
109+copy of the Licence with every copy of the Work he/she distributes or
110+communicates. The Licensee must cause any Derivative Work to carry prominent
111+notices stating that the Work has been modified and the date of modification.
112+
113+Copyleft clause: If the Licensee distributes or communicates copies of the
114+Original Works or Derivative Works, this Distribution or Communication will be
115+done under the terms of this Licence or of a later version of this Licence
116+unless the Original Work is expressly distributed only under this version of the
117+Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
118+(becoming Licensor) cannot offer or impose any additional terms or conditions on
119+the Work or Derivative Work that alter or restrict the terms of the Licence.
120+
121+Compatibility clause: If the Licensee Distributes or Communicates Derivative
122+Works or copies thereof based upon both the Work and another work licensed under
123+a Compatible Licence, this Distribution or Communication can be done under the
124+terms of this Compatible Licence. For the sake of this clause, ‘Compatible
125+Licence’ refers to the licences listed in the appendix attached to this Licence.
126+Should the Licensee's obligations under the Compatible Licence conflict with
127+his/her obligations under this Licence, the obligations of the Compatible
128+Licence shall prevail.
129+
130+Provision of Source Code: When distributing or communicating copies of the Work,
131+the Licensee will provide a machine-readable copy of the Source Code or indicate
132+a repository where this Source will be easily and freely available for as long
133+as the Licensee continues to distribute or communicate the Work.
134+
135+Legal Protection: This Licence does not grant permission to use the trade names,
136+trademarks, service marks, or names of the Licensor, except as required for
137+reasonable and customary use in describing the origin of the Work and
138+reproducing the content of the copyright notice.
139+
140+6. Chain of Authorship
141+
142+The original Licensor warrants that the copyright in the Original Work granted
143+hereunder is owned by him/her or licensed to him/her and that he/she has the
144+power and authority to grant the Licence.
145+
146+Each Contributor warrants that the copyright in the modifications he/she brings
147+to the Work are owned by him/her or licensed to him/her and that he/she has the
148+power and authority to grant the Licence.
149+
150+Each time You accept the Licence, the original Licensor and subsequent
151+Contributors grant You a licence to their contributions to the Work, under the
152+terms of this Licence.
153+
154+7. Disclaimer of Warranty
155+
156+The Work is a work in progress, which is continuously improved by numerous
157+Contributors. It is not a finished work and may therefore contain defects or
158+‘bugs’ inherent to this type of development.
159+
160+For the above reason, the Work is provided under the Licence on an ‘as is’ basis
161+and without warranties of any kind concerning the Work, including without
162+limitation merchantability, fitness for a particular purpose, absence of defects
163+or errors, accuracy, non-infringement of intellectual property rights other than
164+copyright as stated in Article 6 of this Licence.
165+
166+This disclaimer of warranty is an essential part of the Licence and a condition
167+for the grant of any rights to the Work.
168+
169+8. Disclaimer of Liability
170+
171+Except in the cases of wilful misconduct or damages directly caused to natural
172+persons, the Licensor will in no event be liable for any direct or indirect,
173+material or moral, damages of any kind, arising out of the Licence or of the use
174+of the Work, including without limitation, damages for loss of goodwill, work
175+stoppage, computer failure or malfunction, loss of data or any commercial
176+damage, even if the Licensor has been advised of the possibility of such damage.
177+However, the Licensor will be liable under statutory product liability laws as
178+far such laws apply to the Work.
179+
180+9. Additional agreements
181+
182+While distributing the Work, You may choose to conclude an additional agreement,
183+defining obligations or services consistent with this Licence. However, if
184+accepting obligations, You may act only on your own behalf and on your sole
185+responsibility, not on behalf of the original Licensor or any other Contributor,
186+and only if You agree to indemnify, defend, and hold each Contributor harmless
187+for any liability incurred by, or claims asserted against such Contributor by
188+the fact You have accepted any warranty or additional liability.
189+
190+10. Acceptance of the Licence
191+
192+The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
193+placed under the bottom of a window displaying the text of this Licence or by
194+affirming consent in any other similar way, in accordance with the rules of
195+applicable law. Clicking on that icon indicates your clear and irrevocable
196+acceptance of this Licence and all of its terms and conditions.
197+
198+Similarly, you irrevocably accept this Licence and all of its terms and
199+conditions by exercising any rights granted to You by Article 2 of this Licence,
200+such as the use of the Work, the creation by You of a Derivative Work or the
201+Distribution or Communication by You of the Work or copies thereof.
202+
203+11. Information to the public
204+
205+In case of any Distribution or Communication of the Work by means of electronic
206+communication by You (for example, by offering to download the Work from a
207+remote location) the distribution channel or media (for example, a website) must
208+at least provide to the public the information requested by the applicable law
209+regarding the Licensor, the Licence and the way it may be accessible, concluded,
210+stored and reproduced by the Licensee.
211+
212+12. Termination of the Licence
213+
214+The Licence and the rights granted hereunder will terminate automatically upon
215+any breach by the Licensee of the terms of the Licence.
216+
217+Such a termination will not terminate the licences of any person who has
218+received the Work from the Licensee under the Licence, provided such persons
219+remain in full compliance with the Licence.
220+
221+13. Miscellaneous
222+
223+Without prejudice of Article 9 above, the Licence represents the complete
224+agreement between the Parties as to the Work.
225+
226+If any provision of the Licence is invalid or unenforceable under applicable
227+law, this will not affect the validity or enforceability of the Licence as a
228+whole. Such provision will be construed or reformed so as necessary to make it
229+valid and enforceable.
230+
231+The European Commission may publish other linguistic versions or new versions of
232+this Licence or updated versions of the Appendix, so far this is required and
233+reasonable, without reducing the scope of the rights granted by the Licence. New
234+versions of the Licence will be published with a unique version number.
235+
236+All linguistic versions of this Licence, approved by the European Commission,
237+have identical value. Parties can take advantage of the linguistic version of
238+their choice.
239+
240+14. Jurisdiction
241+
242+Without prejudice to specific agreement between parties,
243+
244+- any litigation resulting from the interpretation of this License, arising
245+  between the European Union institutions, bodies, offices or agencies, as a
246+  Licensor, and any Licensee, will be subject to the jurisdiction of the Court
247+  of Justice of the European Union, as laid down in article 272 of the Treaty on
248+  the Functioning of the European Union,
249+
250+- any litigation arising between other parties and resulting from the
251+  interpretation of this License, will be subject to the exclusive jurisdiction
252+  of the competent court where the Licensor resides or conducts its primary
253+  business.
254+
255+15. Applicable Law
256+
257+Without prejudice to specific agreement between parties,
258+
259+- this Licence shall be governed by the law of the European Union Member State
260+  where the Licensor has his seat, resides or has his registered office,
261+
262+- this licence shall be governed by Belgian law if the Licensor has no seat,
263+  residence or registered office inside a European Union Member State.
264+
265+Appendix
266+
267+‘Compatible Licences’ according to Article 5 EUPL are:
268+
269+- GNU General Public License (GPL) v. 2, v. 3
270+- GNU Affero General Public License (AGPL) v. 3
271+- Open Software License (OSL) v. 2.1, v. 3.0
272+- Eclipse Public License (EPL) v. 1.0
273+- CeCILL v. 2.0, v. 2.1
274+- Mozilla Public Licence (MPL) v. 2
275+- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
276+- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
277+  works other than software
278+- European Union Public Licence (EUPL) v. 1.1, v. 1.2
279+- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
280+  Reciprocity (LiLiQ-R+).
281+
282+The European Commission may update this Appendix to later versions of the above
283+licences without producing a new version of the EUPL, as long as they provide
284+the rights granted in Article 2 of this Licence and protect the covered Source
285+Code from exclusive appropriation.
286+
287+All other changes or additions to this Appendix require the production of a new
288+EUPL version.
+22, -0
 1@@ -0,0 +1,22 @@
 2+SHELL = /bin/sh
 3+GO ?= go
 4+PREFIX ?= /usr/local
 5+BINDIR = $(PREFIX)/bin
 6+
 7+.PHONY: all
 8+all:
 9+	$(GO) build ./cmd/webfetch
10+
11+.PHONY: install
12+install: all
13+	install -Dm755 ./webfetch $(BINDIR)/webfetch
14+
15+clean:
16+	rm ./webfetch
17+
18+.PHONY: help
19+help:
20+	@echo "Usage:"
21+	@echo "all:     make webfetch"
22+	@echo "install: install webfetch"
23+
+28, -0
 1@@ -0,0 +1,28 @@
 2+# webfetch
 3+
 4+Styling CLI programs is a hassle. Let's use our browser!
 5+
 6+## Why did you make this?
 7+
 8+In a message that wasn't directed to me, izder456 asked someone if they
 9+saw their new project. It was exfetch running via the web. This
10+intrigued me enough to try it myself given that templates where not in
11+use.
12+
13+> Does crystal have a templating thing like Go or other languages
14+ 
15+> either way now i have the idea, thanks :)
16+
17+Now we are here, a Go program that creates an HTML page with your
18+systems fetch information.
19+
20+## How do I build and install this?
21+
22+`make install` should do the trick, but make sure to specify the PREFIX
23+lest you are fine with installing it to /usr/local.
24+
25+## Where can I find this project?
26+
27+This projects official repository lies on [Codeberg][repo].
28+
29+[repo]: https://codeberg.org/amadaluzia/webfetch "amadaluzia/webfetch on codeberg.org"
+27, -0
 1@@ -0,0 +1,27 @@
 2+package main
 3+
 4+import (
 5+	"html/template"
 6+	"log"
 7+	"os"
 8+
 9+	"codeberg.org/amadaluzia/webfetch/internal"
10+)
11+
12+func main() {
13+	var sys internal.SystemInfo
14+	if err := sys.Load(); err != nil {
15+		log.Fatalln(err) 
16+	}
17+	output, err := os.OpenFile("output.html", os.O_WRONLY|os.O_CREATE, 0644)
18+	if err != nil {
19+		log.Fatalln(err) 
20+	}
21+	t, err := template.ParseFiles("webfetch.html")
22+	if err != nil {
23+		log.Fatalln(err) 
24+	}
25+	if err := t.Execute(output, sys); err != nil {
26+		log.Fatalln(err) 
27+	}
28+}
A go.mod
+3, -0
1@@ -0,0 +1,3 @@
2+module codeberg.org/amadaluzia/webfetch
3+
4+go 1.26.1
+33, -0
 1@@ -0,0 +1,33 @@
 2+package internal
 3+
 4+import (
 5+	"bufio"
 6+	"bytes"
 7+	"fmt"
 8+	"strings"
 9+)
10+
11+type OsRelease map[string]string
12+
13+func (os OsRelease) UnmarshalText(text []byte) error {
14+	var b bytes.Buffer
15+	if _, err := b.Write(text); err != nil {
16+		return fmt.Errorf("failed to write bytes to buffer: %v", err)
17+	}
18+	scanner := bufio.NewScanner(&b)
19+	line := 1
20+	for scanner.Scan() {
21+		pair := strings.SplitN(scanner.Text(), "=", 2)
22+		if len(pair) < 2 {
23+			return fmt.Errorf("failed to parse /etc/os-release: expected = at line %d", line)
24+		}
25+		key, val := pair[0], pair[1]
26+		if val[0] != '"' {
27+			os[key] = val
28+		} else {
29+			os[key] = val[1 : len(val)-1]
30+		}
31+		line++
32+	}
33+	return nil
34+}
+72, -0
 1@@ -0,0 +1,72 @@
 2+package internal
 3+
 4+import (
 5+	"bufio"
 6+	"bytes"
 7+	"fmt"
 8+	"slices"
 9+	"strconv"
10+)
11+
12+type PasswdEntry struct {
13+	Username string
14+	Password string
15+	UID      int
16+	GID      int
17+	GECOS    string
18+	Home     string
19+	Shell    string
20+}
21+
22+type Passwd []PasswdEntry
23+
24+func (passwd *PasswdEntry) UnmarshalText(text []byte) error {
25+	line := bytes.Split(text, []byte{':'})
26+	passwd.Username = string(line[0])
27+	passwd.Password = string(line[1])
28+	uid, err := strconv.Atoi(string(line[2]))
29+	if err != nil {
30+		return fmt.Errorf("failed to convert UID to int: %v", err)
31+	}
32+	gid, err := strconv.Atoi(string(line[3]))
33+	if err != nil {
34+		return fmt.Errorf("failed to convert GID to int: %v", err)
35+	}
36+	passwd.UID = uid
37+	passwd.GID = gid
38+	passwd.GECOS = string(line[4])
39+	passwd.Home = string(line[5])
40+	passwd.Shell = string(line[6])
41+	return nil
42+}
43+
44+func (passwd *Passwd) UnmarshalText(text []byte) error {
45+	var b bytes.Buffer
46+	if _, err := b.Write(text); err != nil {
47+		return fmt.Errorf("failed to write passwd file to buffer: %v", err)
48+	}
49+	scanner := bufio.NewScanner(&b)
50+	line := 1
51+	for scanner.Scan() {
52+		var entry PasswdEntry
53+		if err := entry.UnmarshalText(scanner.Bytes()); err != nil {
54+			return fmt.Errorf("failed to parse passwd file, line %d: %v", line, err)
55+		}
56+		*passwd = append(*passwd, entry)
57+		line++
58+	}
59+	return nil
60+}
61+
62+func (passwd Passwd) Entry(user string) (PasswdEntry, error) {
63+	if len(passwd) == 0 {
64+		return PasswdEntry{}, fmt.Errorf("passwd has no entries")
65+	}
66+	i := slices.IndexFunc(passwd, func(pwent PasswdEntry) bool {
67+		return pwent.Username == user
68+	})
69+	if i == -1 {
70+		return PasswdEntry{}, fmt.Errorf("failed to find user %s", user)
71+	}
72+	return passwd[i], nil
73+}
+74, -0
 1@@ -0,0 +1,74 @@
 2+package internal
 3+
 4+import (
 5+	"syscall"
 6+)
 7+
 8+type SystemInfo struct {
 9+	OS            string
10+	Hostname      string
11+	Username      string
12+	KernelRelease string
13+	KernelName    string
14+	MachineArch   string
15+	Shell         string
16+	Uptime        string
17+	Terminal      string
18+}
19+
20+func (sysinfo *SystemInfo) Load() error {
21+	hostname, err := GetHostname()
22+	if err != nil {
23+		return err 
24+	}
25+	osRelease, err := GetOsRelease()
26+	if err != nil {
27+		return err 
28+	}
29+	username, err := GetUsername()
30+	if err != nil {
31+		return err 
32+	}
33+	var uname syscall.Utsname
34+	if err := syscall.Uname(&uname); err != nil {
35+		return err 
36+	}
37+	kversion, err := GetKernelRelease(uname)
38+	if err != nil {
39+		return err 
40+	}
41+	kname, err := GetKernelName(uname)
42+	if err != nil {
43+		return err 
44+	}
45+	march, err := GetMachineArch(uname)
46+	if err != nil {
47+		return err 
48+	}
49+	passwd, err := GetPasswd()
50+	if err != nil {
51+		return err 
52+	}
53+	shell, err := GetShell(username, passwd)
54+	if err != nil {
55+		return err 
56+	}
57+	up, err := GetUptime()
58+	if err != nil {
59+		return err 
60+	}
61+	terminal, err := GetTerminal()
62+	if err != nil {
63+		return err 
64+	}
65+	sysinfo.OS = osRelease["PRETTY_NAME"] 
66+	sysinfo.Hostname = hostname
67+	sysinfo.Username = username
68+	sysinfo.KernelRelease = kversion
69+	sysinfo.KernelName = kname
70+	sysinfo.MachineArch = march
71+	sysinfo.Shell = shell
72+	sysinfo.Uptime = up.String()
73+	sysinfo.Terminal = terminal
74+	return nil
75+}
+44, -0
 1@@ -0,0 +1,44 @@
 2+package internal
 3+
 4+import (
 5+	"bytes"
 6+	"fmt"
 7+	"strings"
 8+	"syscall"
 9+)
10+
11+func cleanNulls(s string) string {
12+	return strings.TrimFunc(s, func(r rune) bool {
13+		return r == 0
14+	})
15+}
16+
17+func GetKernelName(uname syscall.Utsname) (string, error) {
18+	var b bytes.Buffer
19+	for _, v := range uname.Sysname {
20+		if err := b.WriteByte(byte(v)); err != nil {
21+			return "", fmt.Errorf("failed to write kernel name to buffer: %v", err)
22+		}
23+	}
24+	return cleanNulls(b.String()), nil
25+}
26+
27+func GetKernelRelease(uname syscall.Utsname) (string, error) {
28+	var b bytes.Buffer
29+	for _, v := range uname.Release {
30+		if err := b.WriteByte(byte(v)); err != nil {
31+			return "", fmt.Errorf("failed to write kernel release to buffer: %v", err)
32+		}
33+	}
34+	return cleanNulls(b.String()) , nil
35+}
36+
37+func GetMachineArch(uname syscall.Utsname) (string, error) {
38+	var b bytes.Buffer
39+	for _, v := range uname.Machine {
40+		if err := b.WriteByte(byte(v)); err != nil {
41+			return "", fmt.Errorf("failed to write machine to buffer: %v", err)
42+		}
43+	}
44+	return cleanNulls(b.String()) , nil
45+}
+35, -0
 1@@ -0,0 +1,35 @@
 2+package internal
 3+
 4+import (
 5+	"bytes"
 6+	"fmt"
 7+	"math"
 8+	"strconv"
 9+)
10+
11+type Uptime struct {
12+	Days    int
13+	Hours   int
14+	Minutes int
15+	Seconds int
16+}
17+
18+func (up *Uptime) UnmarshalText(text []byte) error {
19+	times := bytes.Split(text, []byte{' '})
20+	if len(times) < 2 {
21+		return fmt.Errorf("there must be more than one time")
22+	}
23+	upSeconds, err := strconv.ParseFloat(string(times[0]), 32)
24+	if err != nil {
25+		return fmt.Errorf("failed to parse seconds to float: %v", err)
26+	}
27+	up.Days = int(math.Floor(upSeconds)) / 86400
28+	up.Hours = int(math.Floor(upSeconds)) / 3600 % 24
29+	up.Minutes = int(math.Floor(upSeconds)) / 60 % 60
30+	up.Seconds = int(math.Floor(upSeconds)) % 60
31+	return nil
32+}
33+
34+func (up *Uptime) String() string {
35+	return fmt.Sprintf("%d days, %d hours, %d minutes and %d seconds", up.Days, up.Hours, up.Minutes, up.Seconds)
36+}
+92, -0
 1@@ -0,0 +1,92 @@
 2+package internal
 3+
 4+import (
 5+	"fmt"
 6+	"os"
 7+	"strings"
 8+)
 9+
10+func GetPasswd() (Passwd, error) {
11+	var passwd Passwd
12+	text, err := os.ReadFile("/etc/passwd")
13+	if err != nil {
14+		return nil, fmt.Errorf("failed to read /etc/passwd: %v", err)
15+	}
16+	if err := passwd.UnmarshalText(text); err != nil {
17+		return nil, err
18+	}
19+	return passwd, nil
20+}
21+
22+func GetOsRelease() (OsRelease, error) {
23+	var osRelease OsRelease = map[string]string{}
24+	text, err := os.ReadFile("/etc/os-release")
25+	if err != nil {
26+		return nil, fmt.Errorf("failed to read /etc/os-release: %v", err)
27+	}
28+	if err := osRelease.UnmarshalText(text); err != nil {
29+		return nil, err
30+	}
31+	return osRelease, nil
32+}
33+
34+func GetHostname() (string, error) {
35+	var sb strings.Builder
36+	hostname, err := os.ReadFile("/etc/hostname")
37+	if err != nil {
38+		return "", fmt.Errorf("failed to read /etc/hostname: %v", err)
39+	}
40+	_, err = sb.Write(hostname)
41+	if err != nil {
42+		return "", fmt.Errorf("failed to use hostname: %v", err)
43+	}
44+	return strings.TrimSpace(sb.String()), nil
45+}
46+
47+func GetUsername() (string, error) {
48+	userEnvVars := []string{"USER", "username"}
49+	var ok bool
50+	var val string
51+	for _, envvar := range userEnvVars {
52+		val, ok = os.LookupEnv(envvar)
53+		if ok {
54+			break
55+		}
56+	}
57+	if !ok {
58+		return "", fmt.Errorf("failed to get username")
59+	}
60+	return val, nil
61+}
62+
63+func GetShell(user string, passwd Passwd) (string, error) {
64+	pwEnt, err := passwd.Entry(user)
65+	if err != nil {
66+		return "", err
67+	}
68+	return pwEnt.Shell, nil
69+}
70+
71+func GetUptime() (Uptime, error) {
72+	var up Uptime
73+	text, err := os.ReadFile("/proc/uptime")
74+	if err != nil {
75+		return up, fmt.Errorf("failed to read /proc/uptime: %v", err)
76+	}
77+	if err := up.UnmarshalText(text); err != nil {
78+		return up, err
79+	}
80+	return up, nil
81+}
82+
83+func GetTerminal() (string, error) {
84+	val, ok := os.LookupEnv("TERMINAL")
85+	if ok {
86+		return val, nil
87+	}
88+	val, ok = os.LookupEnv("TERM")
89+	if ok {
90+		return val, nil
91+	}
92+	return "", fmt.Errorf("failed to find terminal")
93+}
+32, -0
 1@@ -0,0 +1,32 @@
 2+<!DOCTYPE html>
 3+<html>
 4+    <head>
 5+    </head>
 6+    <body>
 7+        <main>
 8+            <p>artur@napoleon</p>
 9+            <table>
10+                <tr>
11+                    <td>OS</td>
12+                    <td>NixOS 26.05 (Yarara)</td>
13+                </tr>
14+                <tr>
15+                    <td>Kernel</td>
16+                    <td>Linux x86_64 6.18.22</td>
17+                </tr>
18+                <tr>
19+                    <td>Uptime</td>
20+                    <td>0 days, 13 hours, 50 minutes and 6 seconds</td>
21+                </tr>
22+                <tr>
23+                    <td>Shell</td>
24+                    <td>/run/current-system/sw/bin/zsh</td>
25+                </tr>
26+                <tr>
27+                    <td>Terminal</td>
28+                    <td>alacritty</td>
29+                </tr>
30+            </table>
31+        </main>
32+    </body>
33+</html>
+32, -0
 1@@ -0,0 +1,32 @@
 2+<!DOCTYPE html>
 3+<html>
 4+    <head>
 5+    </head>
 6+    <body>
 7+        <main>
 8+            <p>{{ .Username}}@{{ .Hostname }}</p>
 9+            <table>
10+                <tr>
11+                    <td>OS</td>
12+                    <td>{{ .OS }}</td>
13+                </tr>
14+                <tr>
15+                    <td>Kernel</td>
16+                    <td>{{ .KernelName }} {{ .MachineArch }} {{ .KernelRelease }}</td>
17+                </tr>
18+                <tr>
19+                    <td>Uptime</td>
20+                    <td>{{ .Uptime }}</td>
21+                </tr>
22+                <tr>
23+                    <td>Shell</td>
24+                    <td>{{ .Shell }}</td>
25+                </tr>
26+                <tr>
27+                    <td>Terminal</td>
28+                    <td>{{ .Terminal }}</td>
29+                </tr>
30+            </table>
31+        </main>
32+    </body>
33+</html>