diff options
| author | root <root@caddy-256phi.local> | 2026-03-22 22:36:36 +0100 |
|---|---|---|
| committer | root <root@caddy-256phi.local> | 2026-03-22 22:36:36 +0100 |
| commit | f6d1edb5e928b260afc8363b184123e017783285 (patch) | |
| tree | 51ad4e06b71e333797d052e8088e0327d4ca17ba | |
| parent | 5c6f7592a7a81c98a8179ebbfa9546405ac1b576 (diff) | |
Initial commit of the original website
| -rw-r--r-- | blog/Parsedown.php | 1995 | ||||
| -rw-r--r-- | blog/blog.php | 109 | ||||
| -rw-r--r-- | blog/posts/2026-03-05-how-i-use-nixos.md | 23 | ||||
| -rw-r--r-- | blog/posts/2026-03-05-why-i-use-nixos.md | 10 | ||||
| -rw-r--r-- | fonts/Hacked/1rl08.zip | bin | 0 -> 18408 bytes | |||
| -rw-r--r-- | fonts/Hacked/Hacked-KerX.ttf | bin | 0 -> 26664 bytes | |||
| -rw-r--r-- | fonts/Hacked/info.txt | 2 | ||||
| -rw-r--r-- | fonts/Hacked/misc/readme.txt | 6 | ||||
| -rw-r--r-- | guestbook/guestbook.json | 104 | ||||
| -rw-r--r-- | guestbook/guestbook.json.save | 68 | ||||
| -rw-r--r-- | guestbook/guestbook.php | 102 | ||||
| -rw-r--r-- | index.html | 28 | ||||
| -rw-r--r-- | main.js | 82 | ||||
| -rw-r--r-- | mountains.js | 40 | ||||
| -rw-r--r-- | pages/contact.html | 36 | ||||
| -rw-r--r-- | pages/music.html | 24 | ||||
| -rw-r--r-- | pages/services.html | 0 | ||||
| -rw-r--r-- | style.css | 176 | ||||
| -rw-r--r-- | sun.js | 29 |
19 files changed, 2834 insertions, 0 deletions
diff --git a/blog/Parsedown.php b/blog/Parsedown.php new file mode 100644 index 0000000..2f01d5c --- /dev/null +++ b/blog/Parsedown.php @@ -0,0 +1,1995 @@ +<?php + +# +# +# Parsedown +# http://parsedown.org +# +# (c) Emanuil Rusev +# http://erusev.com +# +# For the full license information, view the LICENSE file that was distributed +# with this source code. +# +# + +class Parsedown +{ + # ~ + + const version = '1.8.0'; + + # ~ + + function text($text) + { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) + { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') + { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) + { + $CurrentBlock = $Block; + } + else + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if ( ! isset($Component['element'])) + { + if (isset($Component['markup'])) + { + $Component['element'] = array('rawHtml' => $Component['markup']); + } + elseif (isset($Component['hidden'])) + { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (strpos($Line['text'], '<!--') === 0) + { + $Block = array( + 'element' => array( + 'rawHtml' => $Line['body'], + 'autobreak' => true, + ), + ); + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) + { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) + { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') + { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, ?array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) + { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) + { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } + elseif ($contentIndent === 0) + { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') + { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') + { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) + { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) + { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) + { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['elements'] as &$li) + { + if (end($li['handler']['argument']) !== '') + { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') + { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, ?array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, ?array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') + { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) + { + return; + } + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = (empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) + { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) + { + if ( ! isset($Element['autobreak'])) + { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) + { + $text = $matches[2]; + $text = preg_replace('/[ ]*+\n/', ' ', $text); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ){ + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['handler']['argument']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) + { + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) + { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } + else + { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') + { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) + { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) + { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) + { + $markup .= $this->elements($Element['elements']); + } + elseif (isset($Element['element'])) + { + $markup .= $this->element($Element['element']); + } + else + { + if (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + } + + $markup .= $hasName ? '</' . $Element['name'] . '>' : ''; + } + elseif ($hasName) + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) + { + if (empty($Element)) + { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if ( ! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) + { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) + { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + /** + * @deprecated use text() instead + */ + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if ( ! isset($Element['name'])) + { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/blog/blog.php b/blog/blog.php new file mode 100644 index 0000000..f944c2e --- /dev/null +++ b/blog/blog.php @@ -0,0 +1,109 @@ +<?php +require 'Parsedown.php'; +$Parsedown = new Parsedown(); + +$postsDir = __DIR__ . '/posts/'; + +// Get requested post from URL (pretty slug) +$postParam = $_GET['post'] ?? null; + +if ($postParam) { + // Search for matching file by slug (ignoring date prefix) + $files = glob($postsDir . '*.md'); + $found = null; + foreach ($files as $file) { + $filename = pathinfo($file, PATHINFO_FILENAME); // e.g., 2026-03-05-why-i-use-nixos + $slug = preg_replace('/^\d{4}-\d{2}-\d{2}-/', '', $filename); // remove date prefix + if ($slug === $postParam) { + $found = $file; + break; + } + } + + if ($found) { + $markdown = file_get_contents($found); + + // Extract first heading line (# Title) as title + if (preg_match('/^# (.+)/m', $markdown, $matches)) { + $title = trim($matches[1]); + // Remove the first heading from content + $markdown = preg_replace('/^# .*/', '', $markdown, 1); + } else { + $title = "Blog Post"; + } + + $content = $Parsedown->text($markdown); + } else { + // Fallback to list if slug not found + $postParam = null; + } +} + +// If no valid post requested → show list of all posts +if (!$postParam) { + $files = array_reverse(glob($postsDir . '*.md')); // newest first + $content = "<ul class='blog-list'>"; + foreach ($files as $file) { + $filename = pathinfo($file, PATHINFO_FILENAME); + $slug = preg_replace('/^\d{4}-\d{2}-\d{2}-/', '', $filename); + + // Extract first # heading as title + $mdContent = file_get_contents($file); + if (preg_match('/^# (.+)/m', $mdContent, $matches)) { + $name = htmlspecialchars(trim($matches[1])); + } else { + $name = htmlspecialchars($slug); + } + + $content .= "<li><a href='/blog/$slug'>$name</a></li>"; + } + $content .= "</ul>"; + $title = "Blog"; +} +?> + +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<title><?= htmlspecialchars($title) ?></title> +<link rel="stylesheet" href="../style.css"> +<style> +/* Blog-specific tweaks */ +.blog-content { + margin-top: 20px; + line-height: 1.6; +} +.blog-content h1, .blog-content h2, .blog-content h3 { + color: #ff00ff; + text-shadow: 0 0 5px #ff00ff, 0 0 15px rgba(255,0,255,0.6); +} +.blog-content p, .blog-content li { + color: #e0d6ff; +} +.blog-list li { + margin-bottom: 8px; +} +.blog-list a { + color: #00ffff; + text-decoration: none; +} +.blog-list a:hover { + text-shadow: 0 0 8px #00ffff; +} +</style> +</head> +<body> +<canvas id="bg"></canvas> +<div class="container"> + <h1><?= htmlspecialchars($title) ?></h1> + <div class="blog-content"> + <?= $content ?> + </div> + <footer> + <p>© 256phi | 2026 | <a href="/home">Home</a></p> + </footer> +</div> +<script type="module" src="../main.js"></script> +</body> +</html> diff --git a/blog/posts/2026-03-05-how-i-use-nixos.md b/blog/posts/2026-03-05-how-i-use-nixos.md new file mode 100644 index 0000000..38e057f --- /dev/null +++ b/blog/posts/2026-03-05-how-i-use-nixos.md @@ -0,0 +1,23 @@ +# How I Actually Use NixOS + +After a year of daily driving NixOS, I finally have a setup that just works. Fast, stable, reproducible, and exactly the way I want it. + +Everything is declarative. Every package, service, and dotfile is defined in code. Reinstalling my system, moving to a new machine, or testing something experimental is now painless. I don’t need scripts, I don’t forget tweaks, and I can roll back anything that goes wrong. + +I structure my system in layers: + +1. **System Configuration** – All the core system settings live in `configuration.nix`. Bootloader, networking, users, graphics, services, and packages. If I enable a new service, it’s declared here, and `nixos-rebuild switch` applies it safely. + +2. **Environment-Specific System** – I have separate configs for different environments, like i3wm or Hyprland. These import additional modules for X11 or Wayland, keyboard layouts, window managers, and display settings. Switching environments is just commenting/uncommenting a line in `configuration.nix`. + +3. **Home-Manager Configuration** – My personal settings, shell configs, editors, and fonts live in `mun.nix`. Everything I want in my environment is defined and version-controlled. Clone it to a new machine, run a rebuild, and I’m back to my workflow. + +4. **Environment-Specific Home Config** – For Hyprland or i3wm, I have orchestrators that import all related modules like dunst notifications, kitty terminal, polybar, neovim, picom, rofi, and custom scripts. Each visual rice, like my "chernobyl" setup, is fully modular. Switching or adding rices is trivial. + +The magic is in Nix’s atomic generations. Every change can be applied safely, and rolling back is a single command. No more hours spent fixing a broken system or chasing dependencies. + +Development environments are reproducible too. Each project has its own isolated shell defined in `shell.nix`. Jump between projects without conflicts. No pollution, no surprises. + +Even after a year, I’m still learning. I break things, experiment, and tweak my setup. But NixOS doesn’t punish me. Every change is auditable, reversible, and tracked in Git. The system finally feels *mine*—exactly how I imagine it: fast, clean, and under control. + +If you wish to take a loot at my dots yourself you can find them [here in this Gitlab repo](https://gitlab.com/natasha-linux/nixos/dots) diff --git a/blog/posts/2026-03-05-why-i-use-nixos.md b/blog/posts/2026-03-05-why-i-use-nixos.md new file mode 100644 index 0000000..3b70f79 --- /dev/null +++ b/blog/posts/2026-03-05-why-i-use-nixos.md @@ -0,0 +1,10 @@ +# Why do I use NixOS? +I've been daily driving Linux for aboutt 3 maybe 4 years. In that time I've done my fair share of distro hopping. Starting out with [Fedora](https://fedoraproject.org/), then switching to [Arch](archlinux.org) where I've stayed for a pretty long time. Most of my linux journey actually. + +It was lightweight, it was fast and it was exactly what I wanted because I made it. It even forced me to learn a lot about GNU/Linux and operating systems in general. I loved it. But I was prone to tinkering. Slowly, the system became more and more bloated with tiny little leftovers from my experiments which, as they piled up led to dependency hell and pretty bad instability. + +Another thing that I missed, was reproducibility. If I reinstall my OS, or switch computers anything. I'd have to spend hours reconfiguring everything. I tried writing ash scripts to help with this but that never worked out. + +But recently I've heard of NixOS. Described as the *secret final boss of Linux* with a learning curve steeper than Arch. But the things it promised. All of the benefits of the DIY of Arch without any of the downsides. Perfect reproducibility, no dependency hell (NixOS can have multiple versions of the same package at the same time.) stability rivaling that of Debian. Automatic generations and easy rollbacks when something does break. It was truly the promised land. + +I'll write about my configuration and how I actually use NixOS next time since its a giant thing that took me a year of dailying to figure out. diff --git a/fonts/Hacked/1rl08.zip b/fonts/Hacked/1rl08.zip Binary files differnew file mode 100644 index 0000000..9a65073 --- /dev/null +++ b/fonts/Hacked/1rl08.zip diff --git a/fonts/Hacked/Hacked-KerX.ttf b/fonts/Hacked/Hacked-KerX.ttf Binary files differnew file mode 100644 index 0000000..2917611 --- /dev/null +++ b/fonts/Hacked/Hacked-KerX.ttf diff --git a/fonts/Hacked/info.txt b/fonts/Hacked/info.txt new file mode 100644 index 0000000..2a18490 --- /dev/null +++ b/fonts/Hacked/info.txt @@ -0,0 +1,2 @@ +license: Creative Commons (by) Attribution
+link: https://www.fontspace.com/hacked-font-f28425
\ No newline at end of file diff --git a/fonts/Hacked/misc/readme.txt b/fonts/Hacked/misc/readme.txt new file mode 100644 index 0000000..10a8ef4 --- /dev/null +++ b/fonts/Hacked/misc/readme.txt @@ -0,0 +1,6 @@ +Thanks for downloading and don't forget to donate is you liked this font ! -> https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JF3NNU43JJ7E6
+
+By downloading and using the font called HACKED, you agreed this condition :
+Like the licence CC-BY (https://creativecommons.org/licenses/by/4.0/) saying, if you use this font into a published creation, YOU MUST indicate your source by credit the author "David Libeau" and the font's name "Hacked". You can also link the page http://bit.ly/WatchDogsFont.
+
+Discover my other works at http://DavidLibeau.fr
\ No newline at end of file diff --git a/guestbook/guestbook.json b/guestbook/guestbook.json new file mode 100644 index 0000000..317687f --- /dev/null +++ b/guestbook/guestbook.json @@ -0,0 +1,104 @@ +[ + { + "id": 1774121646, + "name": "vampqueen", + "message": "*bites*", + "timestamp": "2026-03-21T19:34:06+00:00" + }, + { + "id": 1773434445, + "name": "dari", + "message": "Meow", + "timestamp": "2026-03-13T20:40:45+00:00" + }, + { + "id": 1772820601, + "name": "bee movie", + "message": "According to all known laws of aviation, there is no way a bee should be able to fly.\r\nIts wings are too small to get its fat little body off the ground.\r\nThe bee, of course, flies anyway because bees don't care what humans think is impossible.\r\nYellow, black. Yellow, black. Yellow, black. Yellow, black.\r\nOoh, black and yellow!\r\nLet's shake it up a little.\r\nBarry! Breakfast is ready!\r\nComing!\r\nHang on a second.\r\nHello?\r\nBarry?\r\nAdam?\r\nCan you believe this is happening?\r\nI can't.\r\nI'll pick you u", + "timestamp": "2026-03-06T18:10:01+00:00" + }, + { + "id": 1772732369, + "name": "256phi", + "message": "Dear Alien CEO, we still got no fucking clue who you are and so do you regarding us. So do not try scaring us away. We will retaliate.", + "timestamp": "2026-03-05T17:39:29+00:00" + }, + { + "id": 1772732277, + "name": "alien ceo", + "message": "joined the conversation : hello 256phi and precious ikea alien named bleep the first we see that u have some problems with each other ( IF IM RIGHT THERES SOME ALIEN ULLYING GOING ON SO WE MUST FIND OUT CUZ PLUSHIE BULLYING IS VERY BAD YK ) \r\nSO WE ARE HERE TO STOP ANYTHING BAD THAT IS BETWEEN U TWO \r\nWITH RESPECT IN HUMANS AND A PRECIOUS IKEA ALIEN PLUSHIE \r\nTHE LEADER OF THE ALIEN CEO AND HIS WORKERS \r\nPS : WE ARE ARMED DONT EVEN THINK U ARE GOING TO DO SOMETHING", + "timestamp": "2026-03-05T17:37:57+00:00" + }, + { + "id": 1772731960, + "name": "bleepo", + "message": "well im ur cousins plushie \r\nthe one u like to throw away\r\nout the windowwwwwwwwwwwwwwwwwwwwwwww\r\nbleb\r\nwith love and respect \r\nbleepie", + "timestamp": "2026-03-05T17:32:40+00:00" + }, + { + "id": 1772731852, + "name": "bleepo", + "message": "okay no that was mean im sowwy", + "timestamp": "2026-03-05T17:30:52+00:00" + }, + { + "id": 1772731829, + "name": "bleepo", + "message": "ur\r\n mom", + "timestamp": "2026-03-05T17:30:29+00:00" + }, + { + "id": 1772731802, + "name": "256phi", + "message": "Dear bleepo, with due respect. Who tf are you?!", + "timestamp": "2026-03-05T17:30:02+00:00" + }, + { + "id": 1772731735, + "name": "BLEEPO", + "message": "please\r\nstop \r\nthrowing \r\nme \r\ninto \r\nthe \r\ntrash \r\nbin \r\ni hate it there \r\nthis is called plushie diskriminace \r\npwease\r\nstop\r\nor im gonna call my friend the freddy fazbear \r\nhar har har \r\nbe scared\r\nand dont even think ur gonna look into my sketchbook\r\nor\r\nu will get the ultimate punishment for me because i can draw anything i can \r\nso\r\nwe will see if ur gonna be good \r\nor ur going bald", + "timestamp": "2026-03-05T17:28:55+00:00" + }, + { + "id": 1772731474, + "name": "furrykovska_pastelka", + "message": "bleb gleep glorp glurp ful bop blep balb gurp garp HEH \r\nOWO FIRP GLIRP GIIP GUP LUB PLUB HUR \r\nUR DEAR IKEA ALIEN ADOPTED IN IKEA, GOT HOME AT 24.12 \r\nPS: STOP BULLYING ME OR I WILL RIP UR PC IN TWO PIECES OR MORE\r\nWITH LOVE \r\nBLEEPO", + "timestamp": "2026-03-05T17:24:34+00:00" + }, + { + "id": 1772722643, + "name": "syn", + "message": "arfarfarfarfarfarfarfarfarfarfarf", + "timestamp": "2026-03-05T14:57:23+00:00" + }, + { + "id": 1772314637, + "name": "magnetikk", + "message": "wow this website is so h", + "timestamp": "2026-02-28T21:37:17+00:00" + }, + { + "id": 1772055550, + "name": "jade", + "message": "banger site, i like, nice !", + "timestamp": "2026-02-25T21:39:10+00:00" + }, + { + "id": 1772054198, + "name": "BlueBlackCat", + "message": "meow :3\r\nAlso 265phi u are amazing", + "timestamp": "2026-02-25T21:16:38+00:00" + }, + { + "id": 1772052817, + "name": "Logan D.", + "message": "Hi, thanks for having me :3", + "timestamp": "2026-02-25T20:53:37+00:00" + }, + { + "id": 1772052569, + "name": "256phi", + "message": "Hello :3", + "timestamp": "2026-02-25T20:49:29+00:00" + } +]
\ No newline at end of file diff --git a/guestbook/guestbook.json.save b/guestbook/guestbook.json.save new file mode 100644 index 0000000..464fd25 --- /dev/null +++ b/guestbook/guestbook.json.save @@ -0,0 +1,68 @@ +[ + { + "id": 1772731852, + "name": "bleepo", + "message": "okay no that was mean im sowwy", + "timestamp": "2026-03-05T17:30:52+00:00" + }, + { + "id": 1772731829, + "name": "bleepo", + "message": "ur\r\n mom", + "timestamp": "2026-03-05T17:30:29+00:00" + }, + { + "id": 1772731802, + "name": "256phi", + "message": "Dear bleepo, with due respect. Who tf are you?!", + "timestamp": "2026-03-05T17:30:02+00:00" + }, + { + "id": 1772731735, + "name": "BLEEPO", + "message": "please\r\nstop \r\nthrowing \r\nme \r\ninto \r\nthe \r\ntrash \r\nbin \r\ni hate it there \r\nthis is called plushie diskriminace \r\npwease\r\nstop\r\nor im gonna call my friend the freddy fazbear \r\nhar har har \r\nbe scared\r\nand dont even think ur gonna look into my sketchbook\r\nor\r\nu will get the ultimate punishment for me because i can draw anything i can \r\nso\r\nwe will see if ur gonna be good \r\nor ur going bald", + "timestamp": "2026-03-05T17:28:55+00:00" + }, + { + "id": 1772731474, + "name": "furrykovska_pastelka", + "message": "bleb gleep glorp glurp ful bop blep balb gurp garp HEH \r\nOWO FIRP GLIRP GIIP GUP LUB PLUB HUR \r\nUR DEAR IKEA ALIEN ADOPTED IN IKEA, GOT HOME AT 24.12 \r\nPS: STOP BULLYING ME OR I WILL RIP UR PC IN TWO PIECES OR MORE\r\nWITH LOVE \r\nBLEEPO", + "timestamp": "2026-03-05T17:24:34+00:00" + }, + { + "id": 1772722643, + "name": "syn", + "message": "arfarfarfarfarfarfarfarfarfarfarf", + "timestamp": "2026-03-05T14:57:23+00:00" + }, + { + "id": 1772314637, + "name": "magnetikk", + "message": "wow this website is so h", + "timestamp": "2026-02-28T21:37:17+00:00" + }, + { + "id": 1772055550, + "name": "jade", + "message": "banger site, i like, nice !", + "timestamp": "2026-02-25T21:39:10+00:00" + }, + { + "id": 1772054198, + "name": "BlueBlackCat", + "message": "meow :3\r\nAlso 265phi u are amazing", + "timestamp": "2026-02-25T21:16:38+00:00" + }, + { + "id": 1772052817, + "name": "Logan D.", + "message": "Hi, thanks for having me :3", + "timestamp": "2026-02-25T20:53:37+00:00" + }, + { + "id": 1772052569, + "name": "256phi", + "message": "Hello :3", + "timestamp": "2026-02-25T20:49:29+00:00" + } +] diff --git a/guestbook/guestbook.php b/guestbook/guestbook.php new file mode 100644 index 0000000..3e70b83 --- /dev/null +++ b/guestbook/guestbook.php @@ -0,0 +1,102 @@ +<?php +// Enable error reporting while debugging +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); + +$dataFile = __DIR__ . '/guestbook.json'; + +// Make sure the JSON file exists +if (!file_exists($dataFile)) { + file_put_contents($dataFile, json_encode([])); +} + +// Read existing entries safely +$data = []; +$fileContents = file_get_contents($dataFile); +if ($fileContents !== false) { + $decoded = json_decode($fileContents, true); + if (is_array($decoded)) { + $data = $decoded; + } +} + +// Handle form submission +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + + // Honeypot spam field + if (!empty($_POST['website'])) { + header("Location: guestbook.php"); + exit; + } + + $name = trim($_POST['name']); + $message = trim($_POST['message']); + + if ($name && $message) { + $entry = [ + "id" => time(), + "name" => substr($name, 0, 50), + "message" => substr($message, 0, 500), + "timestamp" => date("c") + ]; + + // Add new entry at the beginning + array_unshift($data, $entry); + + // Save back to JSON file + if (file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT)) === false) { + echo "<p style='color:red'>Error: Could not save guestbook entry. Check file permissions.</p>"; + } + + // Redirect to avoid form resubmission + header("Location: guestbook"); + exit; + } +} + +// Entries to display +$entries = $data; +?> +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<title>Guestbook</title> +<link rel="stylesheet" href="../style.css"> +</head> +<body> + +<canvas id="bg"></canvas> +<div class="container"> +<h1>256phi Guestbook</h1> + +<form method="POST"> + <input type="text" name="name" placeholder="Your name" required> + <textarea name="message" placeholder="Leave a message..." required></textarea> + + <!-- Honeypot --> + <input type="text" name="website" style="display:none"> + + <button type="submit">Sign</button> +</form> + +<hr> + +<?php foreach ($entries as $entry): ?> + <div class="entry"> + <p><?= htmlspecialchars($entry['message']) ?></p> + <small> + — <?= htmlspecialchars($entry['name']) ?> + | <?= date("Y-m-d H:i", strtotime($entry['timestamp'])) ?> + </small> + </div> +<?php endforeach; ?> + +<footer> + <p>© 256phi | 2026 | <a href="/home">Home</a></p +</footer> +</div> +<script type="module" src="../main.js"></script> +</body> +</html> diff --git a/index.html b/index.html new file mode 100644 index 0000000..5fa05e1 --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>256phi</title> + <link rel="stylesheet" href="style.css"> + </head> + <body> + <!-- Background with OpenGL --> + <canvas id="bg"></canvas> + <div class="container"> + <h1>256 phi</h1> + <p class="subtitle">Welcome <b>user</b>. <br> + You've fallen through the cracks. It is safe here, you may rest as long as you need.</p> + <h2>What are we?</h2> + <p>256phi is a self-hosted infrastucture powering a little corner of the internet.</p> + <p> + Don't forget to sign the <a href="/guestbook"><i>guestbook</i> :3</a><br> + Also check out the <a href="/blog"><i>blog</i></a> — where I write about Linux, development, and science. + Or if you're looking for some music to listen to, check out what we <a href="/music">found</a>. + </p> + <footer> + <p>© 256phi | 2026 | <a href="/contact">Contact Us</a></p> + </footer> + </div> + <script type="module" src="main.js"></script> + </body> +</html> @@ -0,0 +1,82 @@ +import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; +import { createSun } from './sun.js'; +import { createMountains } from './mountains.js'; + +// Scene setup +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x090012); +scene.fog = new THREE.Fog(0x090012, 20, 120); + +const camera = new THREE.PerspectiveCamera( + 60, + window.innerWidth / window.innerHeight, + 0.1, + 500 +); +camera.position.set(0, 2, 15); +camera.rotation.x = -0.1; + +const renderer = new THREE.WebGLRenderer({ + canvas: document.getElementById('bg'), + antialias: true +}); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); + +// Grid +const size = 200; +const divisions = 200; +const grid = new THREE.GridHelper(size, divisions, 0xff00ff, 0xff00ff); +grid.material.transparent = true; +grid.material.opacity = 0.4; +scene.add(grid); + +// --- Occlusion floor (invisible) --- +const floorGeometry = new THREE.PlaneGeometry(size, size); +const floorMaterial = new THREE.MeshBasicMaterial({ + color: 0x090012, // matches background + opacity: 0.5, // invisible + transparent: true +}); +const floor = new THREE.Mesh(floorGeometry, floorMaterial); +floor.rotation.x = -Math.PI / 2; // horizontal +floor.position.y = 0; // same height as grid +scene.add(floor); + +// Sun +const { sun, glow } = createSun(); +scene.add(sun); +scene.add(glow); + +let scroll = 0; +let sunAngle = 0; + +// Mountains +const mountains = createMountains(); +scene.add(mountains); + +// Animation function +function animate() { + scroll += 0.05; + grid.position.z = scroll % 10; + + renderer.render(scene, camera); +} + +// Start loop with pause/resume on tab visibility +renderer.setAnimationLoop(animate); + +document.addEventListener('visibilitychange', () => { + if (document.hidden) { + renderer.setAnimationLoop(null); + } else { + renderer.setAnimationLoop(animate); + } +}); + +// Handle window resize +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); diff --git a/mountains.js b/mountains.js new file mode 100644 index 0000000..27cea87 --- /dev/null +++ b/mountains.js @@ -0,0 +1,40 @@ +import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; + +export function createMountains() { + const mountainCount = 100; // number of peaks + const mountainWidth = 2; // horizontal spacing + const mountainHeight = 5; // max height of peaks + const baseHeight = 0; // ground level + const mountainDepth = -50; // far behind grid + + // Create a 2D shape for the mountain silhouette + const shape = new THREE.Shape(); + shape.moveTo(-mountainCount * mountainWidth / 2, baseHeight); + + for (let i = -mountainCount / 2; i <= mountainCount / 2; i++) { + const x = i * mountainWidth; + const y = Math.random() * mountainHeight + 5; // peak height + shape.lineTo(x, y); + } + + // Close shape at the far right base + shape.lineTo(mountainCount * mountainWidth / 2, baseHeight); + shape.lineTo(-mountainCount * mountainWidth / 2, baseHeight); + + // Convert shape to geometry + const geometry = new THREE.ShapeGeometry(shape); + + // Rotate so it lies in XZ plane + // geometry.rotateX(-Math.PI / 2); + geometry.translate(0, 0, mountainDepth); + + const material = new THREE.MeshBasicMaterial({ + color: 0xff0077, + side: THREE.DoubleSide, + transparent: true, + opacity: 1 + }); + + const mesh = new THREE.Mesh(geometry, material); + return mesh; +} diff --git a/pages/contact.html b/pages/contact.html new file mode 100644 index 0000000..487bb52 --- /dev/null +++ b/pages/contact.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>Contact 256phi</title> + <link rel="stylesheet" href="../style.css"> + </head> + <body> + <canvas id="bg"></canvas> + <div class="container"> + <h1>Contact Us</h1> + <p class="subtitle">Reach out if you need assistance or just want to say hello.</p> + + <hr> + + <h2>Email</h2> + <p><a href="mailto:contact@256phi.eu">contact@256phi.eu</a></p> + + <!-- <h2>Matrix / XMPP</h2> --> + <!-- <p>matrix: <b>@256phi:256phi</b><br> --> + + <h2>Socials</h2> + <ul> + <li><a href="https://gitlab.com/TashaTheInnkeeper" target="_blank">GitLab</a></li> + <li><a href="https://reddit.com/u/TashaTheInnkeeper" target="_blank">Reddit</a></li> + </ul> + + <hr> + + <footer> + <p>© 256phi | 2026 | <a href="/home">Home</a></p> + </footer> + </div> + <script type="module" src="../main.js"></script> + </body> +</html> diff --git a/pages/music.html b/pages/music.html new file mode 100644 index 0000000..52b2583 --- /dev/null +++ b/pages/music.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>256phi</title> + <link rel="stylesheet" href="../style.css"> + </head> + <body> + <!-- Background with OpenGL --> + <canvas id="bg"></canvas> + <div class="container"> + <h1>Music we found on the web and like</h1> + + <ul> + <li><a href="https://www.youtube.com/watch?v=WIKLHxdhhHo", target="_blank"> RUSSIAN ROULETTE // Staircatte (2mb_ki's_200BPM_Hardcore_Bootleg)</a> + <li><a href="https://www.youtube.com/watch?v=jb-S1gmQgcY", target="_blank">crazy error | looking out for you - joy again</a> + </ul> +<footer> + <p>© 256phi | 2026 | <a href="/contact">Contact Us</a></p> + </footer> + </div> + <script type="module" src="../main.js"></script> + </body> +</html> diff --git a/pages/services.html b/pages/services.html new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pages/services.html diff --git a/style.css b/style.css new file mode 100644 index 0000000..61e39e0 --- /dev/null +++ b/style.css @@ -0,0 +1,176 @@ +/* Synthwave Terminal */ + +/* Define the font */ +@font-face { + font-family: 'Hacked'; + src: url('./fonts/Hacked/Hacked-KerX.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} +/* Make canvas sit behind everything */ +canvas { + position: fixed; + top: 0; + left: 0; + z-index: -1; +} +html, body { + height: 100%; + margin: 0; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +html::-webkit-scrollbar, +body::-webkit-scrollbar { + display: none; +} +html { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +body { + background: radial-gradient(ellipse at bottom, #1a0f2e 0%, #090012 60%, #050008 100%); + color: #e0d6ff; + font-family: "Courier New", monospace; + margin: 0; + padding: 0; + +} + +/* Floating terminal panel */ +.container { + /* visibility: hidden; */ + position: relative; + top: 35%; + z-index: 1; + max-width: 800px; + margin: 80px auto; + padding: 30px; + background: rgba(10, 0, 25, 0.6); + border: 1px solid rgba(255, 0, 255, 0.4); + box-shadow: + 0 0 15px rgba(255, 0, 255, 0.2), + 0 0 40px rgba(0, 255, 255, 0.1); + backdrop-filter: blur(6px); +} + +/* Title glow */ +h1 { + font-size: 42px; + text-align: center; + font-family: 'Hacked'; + color: #ff00ff; + text-shadow: + 0 0 5px #ff00ff, + 0 0 15px rgba(255, 0, 255, 0.6); +} + +/* Subtitle */ +.subtitle { + text-align: center; + font-size: 18px; + margin-bottom: 40px; + color: #c9b6ff; +} + +/* Section headers */ +h2 { + font-size: 22px; + margin-top: 30px; + color: #00ffff; + text-shadow: 0 0 8px rgba(0, 255, 255, 0.6); +} + +/* Terminal-style list */ +ul { + list-style: none; + padding-left: 20px; +} + +li::before { + content: "> "; + color: #ff00ff; +} + +/* Links */ +a { + color: #00ffff; + text-decoration: none; +} + +a:hover { + text-shadow: 0 0 8px #00ffff; +} + +/* Exclude h1 links */ +h1 a { + color: inherit; /* take h1 color */ + text-decoration: none; /* remove underline if any */ + text-shadow: none; /* remove link glow */ +} + +/* Terminal-style form elements */ +input[type="text"], +textarea, +button { + font-family: 'Hacked', monospace; + font-size: 16px; + background: rgba(10, 0, 25, 0.6); + color: #e0d6ff; + border: 1px solid #ff00ff; + border-radius: 4px; + padding: 10px; + outline: none; + box-shadow: + 0 0 5px #ff00ff, + 0 0 15px rgba(255, 0, 255, 0.2); + backdrop-filter: blur(4px); + transition: all 0.2s ease-in-out; + width: 100%; + margin-bottom: 15px; +} + +input[type="text"] { + font-family: "Courier New", monospace; +} + +/* Textarea bigger height */ +textarea { + min-height: 100px; + resize: vertical; +} + +/* Hover/focus effects */ +input[type="text"]:focus, +textarea:focus { + border-color: #00ffff; + box-shadow: + 0 0 5px #00ffff, + 0 0 15px rgba(0, 255, 255, 0.2); +} + +/* Button */ +button { + cursor: pointer; + background: #ff00ff; + color: #000; + font-weight: bold; + border: none; + box-shadow: + 0 0 10px #ff00ff, + 0 0 20px rgba(255, 0, 255, 0.2); +} + +button:hover { + background: #ff66ff; + box-shadow: + 0 0 15px #ff66ff, + 0 0 30px rgba(255, 102, 255, 0.3); +} + +/* Footer */ +footer { + text-align: center; + margin-top: 60px; + font-size: 14px; + color: #888; +} @@ -0,0 +1,29 @@ +// sun.js +import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; + +export function createSun() { + const sunRadius = 15; + const sunSegments = 64; + + // Main sun mesh + const sunGeometry = new THREE.CircleGeometry(sunRadius, sunSegments); + const sunMaterial = new THREE.MeshBasicMaterial({ + color: 0xf49922, // neon pink + transparent: false, + opacity: 1 + }); + const sun = new THREE.Mesh(sunGeometry, sunMaterial); + sun.position.set(0, 5, -51); + + // Optional glow halo + const glowGeometry = new THREE.CircleGeometry(sunRadius * 1.4, sunSegments); + const glowMaterial = new THREE.MeshBasicMaterial({ + color: 0xf49922, + transparent: true, + opacity: 0.1 + }); + const glow = new THREE.Mesh(glowGeometry, glowMaterial); + glow.position.copy(sun.position); + + return { sun, glow }; +} |
