diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 1ed1181..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# Use the [Choice] comment to indicate option arguments that should appear in VS Code UX -# -# [Choice] Haskell version: 9, 8 -ARG VARIANT=9 -FROM haskell:${VARIANT} - -# Use the [Option] comment to specify true/false arguments that should appear in VS Code UX -# -# [Option] Install zsh -ARG INSTALL_ZSH="true" -# [Option] Upgrade OS packages to their latest versions -ARG UPGRADE_PACKAGES="false" - -# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID -COPY library-scripts/*.sh /tmp/library-scripts/ -RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true"\ - && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts - -# Setup ghcup, rbenv, and ruby dependencies. -USER vscode -RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 BOOTSTRAP_HASKELL_MINIMAL=1 sh \ - && echo '[ -f "/home/vscode/.ghcup/env" ] && source "/home/vscode/.ghcup/env" # ghcup-env' >> /home/vscode/.zshrc \ - && git clone https://github.com/rbenv/rbenv.git /home/vscode/.rbenv \ - && echo '[ -f "/home/vscode/.rbenv/bin/rbenv" ] && eval "$(rbenv init - bash)" # rbenv' >> /home/vscode/.zshrc \ - && echo 'export PATH="/home/vscode/.rbenv/bin:$PATH"' >> /home/vscode/.zshrc \ - && echo 'export PATH="/home/vscode/.rbenv/bin:$PATH"' >> /home/vscode/.bashrc \ - && mkdir -p /home/vscode/.rbenv/versions \ - && mkdir -p /home/vscode/.rbenv/plugins \ - && git clone https://github.com/rbenv/ruby-build.git /home/vscode/.rbenv/plugins/ruby-build diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 042f7a5..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,31 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.208.0/containers/haskell -{ - "name": "Haskell (Community)", - - // Update the 'dockerfile' property if you aren't using the standard 'Dockerfile' filename. - "build": { - "dockerfile": "Dockerfile", - "args": { - // Update 'VARIANT' to pick a Haskell version: 9, 8 - "VARIANT": "9" - } - }, - - // Set *default* container specific settings.json values on container create. - "settings": {}, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": ["haskell.haskell"], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "uname -a", - // Run this command everytime the container is being prebuilt. - "updateContentCommand": ".devcontainer/update.sh", - - // Comment out connect as root instead. To add a non-root user, see: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" -} diff --git a/.devcontainer/library-scripts/common-debian.sh b/.devcontainer/library-scripts/common-debian.sh deleted file mode 100644 index f453a6b..0000000 --- a/.devcontainer/library-scripts/common-debian.sh +++ /dev/null @@ -1,454 +0,0 @@ -#!/usr/bin/env bash -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- -# -# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md -# Maintainer: The VS Code and Codespaces Teams -# -# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] - -set -e - -INSTALL_ZSH=${1:-"true"} -USERNAME=${2:-"automatic"} -USER_UID=${3:-"automatic"} -USER_GID=${4:-"automatic"} -UPGRADE_PACKAGES=${5:-"true"} -INSTALL_OH_MYS=${6:-"true"} -ADD_NON_FREE_PACKAGES=${7:-"false"} -SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" -MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" - -if [ "$(id -u)" -ne 0 ]; then - echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' - exit 1 -fi - -# Ensure that login shells get the correct path if the user updated the PATH using ENV. -rm -f /etc/profile.d/00-restore-env.sh -echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh -chmod +x /etc/profile.d/00-restore-env.sh - -# If in automatic mode, determine if a user already exists, if not use vscode -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in ${POSSIBLE_USERS[@]}; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=vscode - fi -elif [ "${USERNAME}" = "none" ]; then - USERNAME=root - USER_UID=0 - USER_GID=0 -fi - -# Load markers to see which steps have already run -if [ -f "${MARKER_FILE}" ]; then - echo "Marker file found:" - cat "${MARKER_FILE}" - source "${MARKER_FILE}" -fi - -# Ensure apt is in non-interactive to avoid prompts -export DEBIAN_FRONTEND=noninteractive - -# Function to call apt-get if needed -apt_get_update_if_needed() -{ - if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then - echo "Running apt-get update..." - apt-get update - else - echo "Skipping apt-get update." - fi -} - -# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies -if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then - - package_list="apt-utils \ - openssh-client \ - gnupg2 \ - dirmngr \ - iproute2 \ - procps \ - lsof \ - htop \ - net-tools \ - psmisc \ - curl \ - wget \ - rsync \ - ca-certificates \ - unzip \ - zip \ - nano \ - vim-tiny \ - less \ - jq \ - lsb-release \ - apt-transport-https \ - dialog \ - libc6 \ - libgcc1 \ - libkrb5-3 \ - libgssapi-krb5-2 \ - libicu[0-9][0-9] \ - liblttng-ust0 \ - libstdc++6 \ - zlib1g \ - locales \ - sudo \ - ncdu \ - man-db \ - strace \ - manpages \ - manpages-dev \ - init-system-helpers" - - # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian - if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then - # Bring in variables from /etc/os-release like VERSION_CODENAME - . /etc/os-release - sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list - sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list - sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list - sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list - sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list - sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list - sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list - sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list - # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html - sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list - sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list - echo "Running apt-get update..." - apt-get update - package_list="${package_list} manpages-posix manpages-posix-dev" - else - apt_get_update_if_needed - fi - - # Install libssl1.1 if available - if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then - package_list="${package_list} libssl1.1" - fi - - # Install appropriate version of libssl1.0.x if available - libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') - if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then - if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then - # Debian 9 - package_list="${package_list} libssl1.0.2" - elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then - # Ubuntu 18.04, 16.04, earlier - package_list="${package_list} libssl1.0.0" - fi - fi - - echo "Packages to verify are installed: ${package_list}" - apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) - - # Install git if not already installed (may be more recent than distro version) - if ! type git > /dev/null 2>&1; then - apt-get -y install --no-install-recommends git - fi - - PACKAGES_ALREADY_INSTALLED="true" -fi - -# Get to latest versions of all packages -if [ "${UPGRADE_PACKAGES}" = "true" ]; then - apt_get_update_if_needed - apt-get -y upgrade --no-install-recommends - apt-get autoremove -y -fi - -# Ensure at least the en_US.UTF-8 UTF-8 locale is available. -# Common need for both applications and things like the agnoster ZSH theme. -if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then - echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen - locale-gen - LOCALE_ALREADY_SET="true" -fi - -# Create or update a non-root user to match UID/GID. -group_name="${USERNAME}" -if id -u ${USERNAME} > /dev/null 2>&1; then - # User exists, update if needed - if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then - group_name="$(id -gn $USERNAME)" - groupmod --gid $USER_GID ${group_name} - usermod --gid $USER_GID $USERNAME - fi - if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then - usermod --uid $USER_UID $USERNAME - fi -else - # Create user - if [ "${USER_GID}" = "automatic" ]; then - groupadd $USERNAME - else - groupadd --gid $USER_GID $USERNAME - fi - if [ "${USER_UID}" = "automatic" ]; then - useradd -s /bin/bash --gid $USERNAME -m $USERNAME - else - useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME - fi -fi - -# Add add sudo support for non-root user -if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then - echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME - chmod 0440 /etc/sudoers.d/$USERNAME - EXISTING_NON_ROOT_USER="${USERNAME}" -fi - -# ** Shell customization section ** -if [ "${USERNAME}" = "root" ]; then - user_rc_path="/root" -else - user_rc_path="/home/${USERNAME}" -fi - -# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty -if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then - cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" -fi - -# Restore user .profile defaults from skeleton file if it doesn't exist or is empty -if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then - cp /etc/skel/.profile "${user_rc_path}/.profile" -fi - -# .bashrc/.zshrc snippet -rc_snippet="$(cat << 'EOF' - -if [ -z "${USER}" ]; then export USER=$(whoami); fi -if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi - -# Display optional first run image specific notice if configured and terminal is interactive -if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then - if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then - cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" - elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then - cat "/workspaces/.codespaces/shared/first-run-notice.txt" - fi - mkdir -p "$HOME/.config/vscode-dev-containers" - # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it - ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) -fi - -# Set the default git editor if not already set -if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then - if [ "${TERM_PROGRAM}" = "vscode" ]; then - if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then - export GIT_EDITOR="code-insiders --wait" - else - export GIT_EDITOR="code --wait" - fi - fi -fi - -EOF -)" - -# code shim, it fallbacks to code-insiders if code is not available -cat << 'EOF' > /usr/local/bin/code -#!/bin/sh - -get_in_path_except_current() { - which -a "$1" | grep -A1 "$0" | grep -v "$0" -} - -code="$(get_in_path_except_current code)" - -if [ -n "$code" ]; then - exec "$code" "$@" -elif [ "$(command -v code-insiders)" ]; then - exec code-insiders "$@" -else - echo "code or code-insiders is not installed" >&2 - exit 127 -fi -EOF -chmod +x /usr/local/bin/code - -# systemctl shim - tells people to use 'service' if systemd is not running -cat << 'EOF' > /usr/local/bin/systemctl -#!/bin/sh -set -e -if [ -d "/run/systemd/system" ]; then - exec /bin/systemctl/systemctl "$@" -else - echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services intead. e.g.: \n\nservice --status-all' -fi -EOF -chmod +x /usr/local/bin/systemctl - -# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme -codespaces_bash="$(cat \ -<<'EOF' - -# Codespaces bash prompt theme -__bash_prompt() { - local userpart='`export XIT=$? \ - && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ - && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' - local gitbranch='`\ - if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ - export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ - if [ "${BRANCH}" != "" ]; then \ - echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ - && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ - echo -n " \[\033[1;33m\]✗"; \ - fi \ - && echo -n "\[\033[0;36m\]) "; \ - fi; \ - fi`' - local lightblue='\[\033[1;34m\]' - local removecolor='\[\033[0m\]' - PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " - unset -f __bash_prompt -} -__bash_prompt - -EOF -)" - -codespaces_zsh="$(cat \ -<<'EOF' -# Codespaces zsh prompt theme -__zsh_prompt() { - local prompt_username - if [ ! -z "${GITHUB_USER}" ]; then - prompt_username="@${GITHUB_USER}" - else - prompt_username="%n" - fi - PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow - PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd - PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status - PROMPT+='%{$fg[white]%}$ %{$reset_color%}' - unset -f __zsh_prompt -} -ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" -ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " -ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" -ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" -__zsh_prompt - -EOF -)" - -# Add RC snippet and custom bash prompt -if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then - echo "${rc_snippet}" >> /etc/bash.bashrc - echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" - echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" - if [ "${USERNAME}" != "root" ]; then - echo "${codespaces_bash}" >> "/root/.bashrc" - echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" - fi - chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" - RC_SNIPPET_ALREADY_ADDED="true" -fi - -# Optionally install and configure zsh and Oh My Zsh! -if [ "${INSTALL_ZSH}" = "true" ]; then - if ! type zsh > /dev/null 2>&1; then - apt_get_update_if_needed - apt-get install -y zsh - fi - if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then - echo "${rc_snippet}" >> /etc/zsh/zshrc - ZSH_ALREADY_INSTALLED="true" - fi - - # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. - # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. - oh_my_install_dir="${user_rc_path}/.oh-my-zsh" - if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then - template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" - user_rc_file="${user_rc_path}/.zshrc" - umask g-w,o-w - mkdir -p ${oh_my_install_dir} - git clone --depth=1 \ - -c core.eol=lf \ - -c core.autocrlf=false \ - -c fsck.zeroPaddedFilemode=ignore \ - -c fetch.fsck.zeroPaddedFilemode=ignore \ - -c receive.fsck.zeroPaddedFilemode=ignore \ - "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 - echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} - sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} - - mkdir -p ${oh_my_install_dir}/custom/themes - echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" - # Shrink git while still enabling updates - cd "${oh_my_install_dir}" - git repack -a -d -f --depth=1 --window=1 - # Copy to non-root user if one is specified - if [ "${USERNAME}" != "root" ]; then - cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root - chown -R ${USERNAME}:${group_name} "${user_rc_path}" - fi - fi -fi - -# Persist image metadata info, script if meta.env found in same directory -meta_info_script="$(cat << 'EOF' -#!/bin/sh -. /usr/local/etc/vscode-dev-containers/meta.env - -# Minimal output -if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then - echo "${VERSION}" - exit 0 -elif [ "$1" = "release" ]; then - echo "${GIT_REPOSITORY_RELEASE}" - exit 0 -elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then - echo "${CONTENTS_URL}" - exit 0 -fi - -#Full output -echo -echo "Development container image information" -echo -if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi -if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi -if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi -if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi -if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi -if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi -if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi -echo -EOF -)" -if [ -f "${SCRIPT_DIR}/meta.env" ]; then - mkdir -p /usr/local/etc/vscode-dev-containers/ - cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env - echo "${meta_info_script}" > /usr/local/bin/devcontainer-info - chmod +x /usr/local/bin/devcontainer-info -fi - -# Write marker file -mkdir -p "$(dirname "${MARKER_FILE}")" -echo -e "\ - PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ - LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ - EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ - RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ - ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" - -echo "Done!" diff --git a/.devcontainer/update.sh b/.devcontainer/update.sh deleted file mode 100755 index 5a3eb28..0000000 --- a/.devcontainer/update.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -whoami -pwd - -rbenv install 2.6.5 -rbenv global 2.6.5 - -gem install compass - -ghcup install ghc 9.2.5 -ghcup set ghc 9.2.5 - -stack upgrade -stack setup diff --git a/.ghci b/.ghci deleted file mode 100644 index edfda70..0000000 --- a/.ghci +++ /dev/null @@ -1,5 +0,0 @@ -:set -XOverloadedStrings -:set -fwarn-unused-binds -fwarn-unused-imports -:set -isrc -:load Main -import Hakyll diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index 432eff5..0000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "CD: Deploy site" - -# Check out https://help.github.com/en/articles/workflow-syntax-for-github-actions for documentation on Actions. -on: - workflow_dispatch: - release: - types: [released] - -concurrency: production - -defaults: - run: - shell: bash # Set the default shell to bash. - -jobs: - cd: - runs-on: ubuntu-latest - environment: - name: production - url: https://codetalk.io - steps: - - uses: actions/checkout@v3 - - - name: Download latest release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release download --pattern "_site.tar.gz" --dir ./ - tar -zxvf _site.tar.gz - - - name: Upload site to S3 - env: - AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - run: | - aws s3 sync _site s3://codetalk.io --delete - - - name: Purge CloudFront cache - env: - CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }} - CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }} - run: | - curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/purge_cache" \ - -H "Authorization: Bearer ${CLOUDFLARE_TOKEN}" \ - -H "Content-Type: application/json" \ - --data '{"purge_everything":true}' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index c731cd2..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,74 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "master" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] - schedule: - - cron: '44 12 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/cron-cache-deps.yml b/.github/workflows/cron-cache-deps.yml deleted file mode 100644 index 661b096..0000000 --- a/.github/workflows/cron-cache-deps.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: "CRON: Cache Dependencies" - -# Check out https://help.github.com/en/articles/workflow-syntax-for-github-actions for documentation on Actions. -on: - schedule: - # Run every Monday at 01:00 in the night. - - cron: "0 1 * * 1" - -defaults: - run: - shell: bash # Set the default shell to bash. - -jobs: - ci: - runs-on: ubuntu-20.04 - environment: - name: ci - env: - GHC_VERSION: "9.2.5" - CABAL_VERSION: "3.6.2.0" - STACK_VERSION: "2.9.3" - steps: - - uses: actions/checkout@v1 - - # Setup tooling - - uses: haskell/actions/setup@v1 - name: Setup Haskell Stack - with: - ghc-version: ${{env.GHC_VERSION}} - cabal-version: ${{env.CABAL_VERSION}} - stack-version: ${{env.STACK_VERSION}} - enable-stack: true - - # Enable cache for cabal and stack - - uses: actions/cache@v2 - name: Cache ~/.cabal/packages, ~/.cabal/store and dist-newstyle - with: - path: | - ~/.cabal/packages - ~/.cabal/store - dist-newstyle - key: ${{ runner.os }}-${{env.GHC_VERSION}}-cabal-store-${{ hashFiles('*.cabal') }} - - uses: actions/cache@v1 - name: Cache ~/.stack - with: - path: ~/.stack - key: ${{ runner.os }}-${{env.GHC_VERSION}}-stack-${{ hashFiles('stack.yaml.lock') }} - - - name: Install dependencies - run: | - stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks --only-dependencies diff --git a/.gitignore b/.gitignore index 1feb004..8e074fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +_dist dist cabal-dev *.o diff --git a/.htmlhintrc b/.htmlhintrc deleted file mode 100644 index f4621ba..0000000 --- a/.htmlhintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "doctype-first": false -} diff --git a/.pages.yml b/.pages.yml index 7b0fff1..cb4dba5 100644 --- a/.pages.yml +++ b/.pages.yml @@ -1,11 +1,11 @@ media: - input: resources/images - output: /resources/images + input: static/content-images + output: /static/content-images content: - name: posts label: Posts - path: posts + path: content/posts type: collection filename: '{year}-{month}-{day}-{primary}.md' view: @@ -42,7 +42,7 @@ content: required: true - name: drafts label: Drafts - path: drafts + path: content/drafts type: collection filename: '{year}-{month}-{day}-{primary}.md' view: @@ -76,4 +76,4 @@ content: type: rich-text options: format: markdown - required: true \ No newline at end of file + required: true diff --git a/.sass-lint.yml b/.sass-lint.yml deleted file mode 100644 index 47ebbac..0000000 --- a/.sass-lint.yml +++ /dev/null @@ -1,95 +0,0 @@ -options: - formatter: stylish -files: - include: '**/*.s+(a|c)ss' -rules: - # Extends - extends-before-mixins: 1 - extends-before-declarations: 1 - placeholder-in-extend: 1 - - # Mixins - mixins-before-declarations: 1 - - # Line Spacing - one-declaration-per-line: 1 - empty-line-between-blocks: 1 - single-line-per-selector: 1 - - # Disallows - no-attribute-selectors: 0 - no-color-hex: 0 - no-color-keywords: 1 - no-color-literals: 1 - no-combinators: 0 - no-css-comments: 1 - no-debug: 1 - no-disallowed-properties: 0 - no-duplicate-properties: 1 - no-empty-rulesets: 1 - no-extends: 0 - no-ids: 0 - no-important: 1 - no-invalid-hex: 1 - no-mergeable-selectors: 1 - no-misspelled-properties: 1 - no-qualifying-elements: 0 - no-trailing-whitespace: 1 - no-trailing-zero: 1 - no-transition-all: 1 - no-universal-selectors: 0 - no-url-domains: 1 - no-url-protocols: 1 - no-vendor-prefixes: 0 - no-warn: 1 - property-units: 0 - - # Nesting - declarations-before-nesting: 1 - force-attribute-nesting: 1 - force-element-nesting: 1 - force-pseudo-nesting: 1 - - # Name Formats - class-name-format: 0 - function-name-format: 1 - id-name-format: 0 - mixin-name-format: 1 - placeholder-name-format: 1 - variable-name-format: 1 - - # Style Guide - attribute-quotes: 1 - bem-depth: 0 - border-zero: 1 - brace-style: 1 - clean-import-paths: 1 - empty-args: 1 - hex-length: 1 - hex-notation: 1 - indentation: 1 - leading-zero: 0 - max-line-length: 0 - max-file-line-count: 0 - nesting-depth: 1 - property-sort-order: 0 - pseudo-element: 1 - quotes: 1 - shorthand-values: 1 - url-quotes: 1 - variable-for-property: 1 - zero-unit: 1 - - # Inner Spacing - space-after-comma: 1 - space-before-colon: 1 - space-after-colon: 1 - space-before-brace: 1 - space-before-bang: 1 - space-after-bang: 1 - space-between-parens: 1 - space-around-operator: 1 - - # Final Items - trailing-semicolon: 1 - final-newline: 1 diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml deleted file mode 100644 index fad1657..0000000 --- a/.stylish-haskell.yaml +++ /dev/null @@ -1,25 +0,0 @@ -steps: - - imports: - align: none - list_align: with_module_name - pad_module_names: false - long_list_align: new_line_multiline - empty_list_align: inherit - list_padding: 7 # length "import " - separate_lists: false - space_surround: false - - language_pragmas: - style: vertical - align: false - remove_redundant: true - - simple_align: - cases: false - top_level_patterns: false - records: false - - trailing_whitespace: {} - -# You need to put any language extensions that's enabled for the entire project here. -language_extensions: [] - -# This is up to personal preference, but 80 is the right answer. -columns: 80 diff --git a/LICENSE b/LICENSE index 7b95fc1..520ada1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright Christian Kjær Laustsen (c) 2016 +Copyright Christian Kjær Laustsen (c) 2024 All rights reserved. diff --git a/README.md b/README.md index dfad159..41cdecf 100644 --- a/README.md +++ b/README.md @@ -1,61 +1 @@ -# [codetalk.io](https://codetalk.io) site [![CI: Build](https://github.com/codetalkio/codetalk.io/actions/workflows/ci.yml/badge.svg)](https://github.com/codetalkio/codetalk.io/actions/workflows/ci.yml) [![CD: Deploy](https://github.com/codetalkio/codetalk.io/actions/workflows/cd.yml/badge.svg)](https://github.com/codetalkio/codetalk.io/actions/workflows/cd.yml) [![Codespaces: Prebuild](https://github.com/codetalkio/codetalk.io/actions/workflows/codespaces/create_codespaces_prebuilds/badge.svg)](https://github.com/codetalkio/codetalk.io/actions/workflows/codespaces/create_codespaces_prebuilds) - -## Dependencies - -You'll need sass/compass to compile the (S)CSS files, else `hakyll` will complain in the build phase. - -```bash -$ npm i -g sass -``` - -To avoid needing to rebuild the entire Haskell application, you can use the ready-made binary (Linux only), - -```bash -$ gh release download --pattern "hakyll-site" --dir ./dist -$ chmod +x ./dist/hakyll-site -$ ./dist/hakyll-site watch -``` - -## Quick walkthrough of Hakyll - -If you change something in `site.hs`, then it needs to be recompiled, using GHC. Everything else, should just need a rebuild via Hakyll. - -```bash -$ stack build -``` - -After this, rebuilding the site is as simple as, - -```bash -$ stack exec -- hakyll-site rebuild -``` - -or, alternatively use `watch` to launch a preview server while developing, - -```bash -$ stack exec -- hakyll-site watch -$ sass resources/scss/app.scss:_site/app.css --style compressed --watch -``` - -## Image Optimization - -```bash -$ brew install imagemagick -``` - -Either put the images in `resources/images/unoptimized`, or copy them with the following script, - -```bash -$ cd resources/images -$ mkdir -p unoptimized -$ for f in *.png; do cp "$f" "unoptimized/${f%.png}.thumbnail.png"; done -``` - -Now that the images you want to generate optimized versions for are in `resources/images/unoptimized`, run the optimization script inside this folder, - -```bash -$ cd unoptimized -$ mogrify -resize 50% *.thumbnail.png -``` - -Finally, copy them out to images again. +# codethoughts diff --git a/Setup.hs b/Setup.hs deleted file mode 100644 index ebdc00e..0000000 --- a/Setup.hs +++ /dev/null @@ -1,3 +0,0 @@ -import Distribution.Simple - -main = defaultMain diff --git a/codetalk-io.cabal b/codetalk-io.cabal deleted file mode 100644 index aa8a8c4..0000000 --- a/codetalk-io.cabal +++ /dev/null @@ -1,34 +0,0 @@ -name: codetalk-io -version: 1.3.0 -synopsis: The codetalk.io blog, generated using Hakyll -description: Please see README.md -homepage: https://github.com/codetalkio/codetalk.io#readme -license: BSD3 -license-file: LICENSE -author: Christian Kjær Laustsen -maintainer: ckl@codetalk.io -copyright: codetalk -category: Web -build-type: Simple -cabal-version: >=1.10 - -executable hakyll-site - main-is: Main.hs - hs-source-dirs: src - default-language: Haskell2010 - ghc-options: -Wall -threaded - other-modules: Site.Compiler - , Site.Context - , Site.Feed - build-depends: base - , hakyll - , pandoc - , text - , bytestring - , aeson - , hxt - , HandsomeSoup - -source-repository head - type: git - location: https://github.com/codetalkio/codetalk.io diff --git a/codetalk.code-workspace b/codetalk.code-workspace index a3d32be..5c5d501 100644 --- a/codetalk.code-workspace +++ b/codetalk.code-workspace @@ -1,24 +1,12 @@ { "folders": [ - { - "name": "drafts", - "path": "drafts" - }, { "name": "posts", - "path": "posts" - }, - { - "name": "importable", - "path": "importable" - }, - { - "name": "images", - "path": "resources/images" + "path": "content/posts" }, { - "name": "thumbnails", - "path": "resources/images/unoptimized" + "name": "drafts", + "path": "content/drafts" }, { "name": "project", @@ -28,30 +16,36 @@ "settings": { "editor.formatOnSave": true, "[yaml]": { - "editor.formatOnSave": false + "editor.formatOnSave": true }, "[markdown]": { + "editor.formatOnSave": true + }, + "[html]": { "editor.formatOnSave": false }, - "workspaceKeybindings.codetalk.optimize-images.enabled": true + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.ruby-lsp": true, + "_dist": true, + "legacy": true + }, + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/*.code-search": true, + "**/.ruby-lsp": true, + "static/styles/output.css": true, + "static/js/**": true + } }, "tasks": { "version": "2.0.0", - "tasks": [ - { - "label": "Optimize Images", - "type": "shell", - // First install brew install rename. Then drop a file in resources/images/unoptimized and run this task. - // - Copy all PNG/JPG files to ./thumbnails - // - Move all PNG/JPG files to the images folder at ../ - // - Rename all PNG/JPG files in ./thumbnails to have a .thumbnail.png/.thumbnail.jpg extension - // - Resize all PNG/JPG files in ./thumbnails to 70% of their original size - // - Move all PNG/JPG files in ./thumbnails to the images folder at ../../ - "command": "(cp *.png ./thumbnails || true) && (cp *.jpg ./thumbnails || true) && (mv *.png ../ || true) && (mv *.jpg ../ || true) && cd ./thumbnails && ls && (rename 's/.png/.thumbnail.png/' *.png || true) && (rename 's/.jpg/.thumbnail.jpg/' *.jpg || true) && (mogrify -resize 70% *.thumbnail.png || true) && (mogrify -resize 70% *.thumbnail.jpg || true) && (mv *.png ../../ || true) && (mv *.jpg ../../ || true)", - "options": { - "cwd": "${workspaceFolder}/../resources/images/unoptimized" - } - } - ] + "tasks": [] } } diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..13028e8 --- /dev/null +++ b/config.toml @@ -0,0 +1,79 @@ +# The URL the site will be built for +base_url = "/" + +# Whether to automatically compile all Sass files in the sass directory +compile_sass = false # We use TailwindCSS instead. + +# When set to "true", the generated HTML files are minified. +minify_html = true + +# For overriding the default output directory `public`, set it to another value (e.g.: "docs") +output_dir = "_dist" + +# Whether to build a search index to be used later on by a JavaScript library +build_search_index = true + +# When set to "true", a feed is automatically generated. +generate_feed = true + +# The filename to use for the feed. Used as the template filename, too. +# Defaults to "atom.xml", which has a built-in template that renders an Atom 1.0 feed. +# There is also a built-in template "rss.xml" that renders an RSS 2.0 feed. +feed_filename = "atom.xml" + +# The default author for pages +author = "Christian Kjær" + +# The taxonomies to be rendered for the site and their configuration of the default languages +# Example: +# taxonomies = [ +# {name = "tags", feed = true}, # each tag will have its own feed +# {name = "tags"}, # you can have taxonomies with the same name in multiple languages +# {name = "categories", paginate_by = 5}, # 5 items per page for a term +# {name = "authors"}, # Basic definition: no feed or pagination +# ] +# +taxonomies = [ + { name = "tags", feed = true }, # each tag will have its own feed +] + +[markdown] +# Whether to do syntax highlighting +# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola +highlight_code = true + +# Use CSS for styling instead of inline styles. +highlight_theme = "css" + +# Set the themes for both dark and light mode. +highlight_themes_css = [ + { theme = "OneHalfDark", filename = "syntax-theme-dark.css" }, # Or visual-studio-dark + { theme = "OneHalfLight", filename = "syntax-theme-light.css" }, +] + +# Whether external links are to be opened in a new tab +# If this is true, a `rel="noopener"` will always automatically be added for security reasons +external_links_target_blank = true +# Whether to set rel="nofollow" for all external links +external_links_no_follow = true +# Whether to set rel="noreferrer" for all external links +external_links_no_referrer = true + +# Whether to set decoding="async" and loading="lazy" for all images +# When turned on, the alt text must be plain text. +# For example, `![xx](...)` is ok but `![*x*x](...)` isn’t ok +lazy_async_image = true + +# Whether footnotes are rendered in the GitHub-style (at the bottom, with back references) or plain (in the place, where they are defined) +bottom_footnotes = true + +# Various slugification strategies, see below for details +# Defaults to everything being a slug +[slugify] +# Whether to remove date prefixes for page path slugs. +# For example, content/posts/2016-10-08_a-post-with-dates.md => posts/a-post-with-dates +# When true, content/posts/2016-10-08_a-post-with-dates.md => posts/2016-10-08-a-post-with-dates +paths_keep_dates = true + +[extra] +# Put all your custom variables here diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..73e90f1 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,4 @@ ++++ +sort_by = "date" +paginate_by = 7 ++++ diff --git a/drafts/2022-01-01-api-migration-and-modernization.md b/content/drafts/2022-01-01-api-migration-and-modernization.md similarity index 100% rename from drafts/2022-01-01-api-migration-and-modernization.md rename to content/drafts/2022-01-01-api-migration-and-modernization.md diff --git a/drafts/2022-01-01-your-aws-bill-tells-a-story.md b/content/drafts/2022-01-01-your-aws-bill-tells-a-story.md similarity index 100% rename from drafts/2022-01-01-your-aws-bill-tells-a-story.md rename to content/drafts/2022-01-01-your-aws-bill-tells-a-story.md diff --git a/drafts/2023-01-01-the-stack.md b/content/drafts/2023-01-01-the-stack.md similarity index 100% rename from drafts/2023-01-01-the-stack.md rename to content/drafts/2023-01-01-the-stack.md diff --git a/drafts/2023-10-17-the-stack-part-4.md b/content/drafts/2023-10-17-the-stack-part-4.md similarity index 95% rename from drafts/2023-10-17-the-stack-part-4.md rename to content/drafts/2023-10-17-the-stack-part-4.md index a965d91..75a7dd3 100644 --- a/drafts/2023-10-17-the-stack-part-4.md +++ b/content/drafts/2023-10-17-the-stack-part-4.md @@ -15,21 +15,9 @@ At the end of this post we will have: There is quite a lot to cover. My recommendation is to clone down the Part 4 branch in the [GitHub repository](https://github.com/codetalkio/the-stack/tree/part-4-api) and use this post as an explanation of what is set up. -
- -- [Prelude: Federated GraphQL](#prelude-federated-graphql) -- [Our GraphQL Schema](#our-graphql-schema) -- [Subgraph: Products, Users, and Reviews](#subgraph-products-users-and-reviews) - - [Local Development](#local-development) - - [Rust GraphQL Server](#rust-graphql-server) - - [Performance \& Pricing](#performance-pricing) -- [Apollo Router](#apollo-router) - - [Performance \& Pricing: App Runner v.s. Lambda](#performance-pricing-app-runner-vs-lambda) -- [Automating Deployments via CDK](#automating-deployments-via-cdk) - - [Building artifacts in CI](#building-artifacts-in-ci) - - [Deploying to App Runner](#deploying-to-app-runner) - - [Deploying to AWS Lambda](#deploying-to-aws-lambda) -- [Next Steps](#next-steps) + + +{{ toc() }} ## Prelude: Federated GraphQL diff --git a/content/drafts/_index.md b/content/drafts/_index.md new file mode 100644 index 0000000..35b3e6c --- /dev/null +++ b/content/drafts/_index.md @@ -0,0 +1,8 @@ ++++ +title = "List of draft posts" +sort_by = "date" +template = "posts.html" +page_template = "drafts-post-page.html" +in_search_index = false +insert_anchor_links = "heading" ++++ diff --git a/content/drafts/component-overview/example.ts b/content/drafts/component-overview/example.ts new file mode 100644 index 0000000..7ee4459 --- /dev/null +++ b/content/drafts/component-overview/example.ts @@ -0,0 +1,13 @@ +// @ts-check + +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "export", + trailingSlash: true, + experimental: { + // Statically type links to prevent typos and other errors when using next/link, improving type safety when navigating between pages. + typedRoutes: true, + }, +}; + +module.exports = nextConfig; diff --git a/content/drafts/component-overview/index.md b/content/drafts/component-overview/index.md new file mode 100644 index 0000000..a865909 --- /dev/null +++ b/content/drafts/component-overview/index.md @@ -0,0 +1,238 @@ ++++ +title = "Component overview" +date = 2023-11-29 ++++ + +Generated Table of Contents by inserting a `toc()` shortcode wrapped in `{{-Let's say I have a list of numbers and want to add `2` to each of these -numbers. You can write `fmap (+2) [1..10]` (using `Functor`). -But what if I had two lists of numbers and wanted to add each number from the -first list to each number from the second list? With `Applicative` you can do -just that - it's like `fmap` only it can take multiple arguments. You can write -`(+) <$> [1,5,10] <*> [11..13]`. -+> Let's say I have a list of numbers and want to add `2` to each of these +> numbers. You can write `fmap (+2) [1..10]` (using `Functor`). +> +> But what if I had two lists of numbers and wanted to add each number from the +> first list to each number from the second list? With `Applicative` you can do +> just that - it's like `fmap` only it can take multiple arguments. You can write +> `(+) <$> [1,5,10] <*> [11..13]`.[^1] -__From here, one can motivate `Monad` by asking:__ -
-What if I wanted to abort midway through - say, if I encountered a problem? You can then write: -+**From here, one can motivate `Monad` by asking:** +> What if I wanted to abort midway through - say, if I encountered a problem? You can then write:[^1] -```haskell + +```haskell,linenos add xs ys = do x <- xs if x < 0 then [] @@ -178,8 +179,8 @@ because on each step the bind function `>>=` (or `=<<` for the other direction) To understand this better, let's take a look at the `Maybe` instance, -
- * Statically compiled Haskell executable - * Shipped in Electron app bundle - * main.js in Electron spawns the subprocess - * Executable creates a localhost HTTP server - * JS talks to Haskell over AJAX -++++ +title =" Using Electron with Haskell" +date = 2016-05-11 +[taxonomies] +tags = ["haskell", "electron"] +[extra] +versions = ["Electron 1.0.1", "Stackage LTS 5.15", "servant 0.4.4.7"] ++++ + +_If you want to grab the whole code from this post, it can be found at [codetalkio/Haskell-Electron-app](https://github.com/codetalkio/Haskell-Electron-app)._ + +Not much literature exist on using `Electron` as a GUI tool for Haskell development, so I thought I'd explore the space a little. Being initially a bit clueless on how `Electron` would launch the Haskell web server, I was watching [the Electron meetup talk by Mike Craig from Wagon HG](https://youtu.be/mUAu7lcgYWE?t=6m54s) (they use `Electron` with Haskell) and noticed they actually mention it on the slides: + + + +> * Statically compiled Haskell executable +> * Shipped in Electron app bundle +> * main.js in Electron spawns the subprocess +> * Executable creates a localhost HTTP server +> * JS talks to Haskell over AJAX Importantly the bit _main.js in Electron spawns the subprocess_ is the part that was somehow missing in my mental model of how this would be structured (my JavaScript experience mainly lies in webdev and not really with Node.js and server-side/desktop JS). @@ -38,8 +35,12 @@ Haskell-Electron-app/ ...servant files ``` +Let's dive into it: + +{{ toc() }} + ## Setting up Electron -`Electron` has a nice [quick start guide](http://electron.atom.io/docs/latest/tutorial/quick-start/){target="_blank" rel="noopener noreferrer"}, which helps you get going fairly, well, quick. For our purposes, the following will set up the initial app we will use throughout. +`Electron` has a nice [quick start guide](http://electron.atom.io/docs/latest/tutorial/quick-start/), which helps you get going fairly, well, quick. For our purposes, the following will set up the initial app we will use throughout. ```bash $ cd Haskell-Electron-app @@ -52,7 +53,7 @@ And that's it really. You've now got a basic `Electron` app running locally. The ## Setting up the Haskell webserver -We'll be using [servant](http://haskell-servant.readthedocs.io/en/stable/){target="_blank" rel="noopener noreferrer"} for a minimal application, but you could really use anything that will run a web server (such as [Yesod](http://www.yesodweb.com){target="_blank" rel="noopener noreferrer"}, [WAI](https://www.stackage.org/package/wai){target="_blank" rel="noopener noreferrer"}, [Snap](http://snapframework.com){target="_blank" rel="noopener noreferrer"}, [Happstack](http://www.happstack.com){target="_blank" rel="noopener noreferrer"} etc, you get the idea :). +We'll be using [servant](http://haskell-servant.readthedocs.io/en/stable/) for a minimal application, but you could really use anything that will run a web server (such as [Yesod](http://www.yesodweb.com), [WAI](https://www.stackage.org/package/wai), [Snap](http://snapframework.com), [Happstack](http://www.happstack.com) etc, you get the idea :). Assuming that `stack` is installed, you can set up a new `servant` project with @@ -63,9 +64,9 @@ $ cd backend $ stack build ``` -which will download the `servant` project template for you (from the [stack templates repo](https://github.com/commercialhaskell/stack-templates){target="_blank" rel="noopener noreferrer"}) and build it. +which will download the `servant` project template for you (from the [stack templates repo](https://github.com/commercialhaskell/stack-templates)) and build it. -To test that it works run `stack exec backend-exe` which will start the executable that `stack build` produced. You now have a web server running at `127.0.0.1:8080` - try and navigate to [127.0.0.1:8080/users](http://127.0.0.1:8080/users){target="_blank" rel="noopener noreferrer"} and check it out! :) +To test that it works run `stack exec backend-exe` which will start the executable that `stack build` produced. You now have a web server running at `127.0.0.1:8080` - try and navigate to [127.0.0.1:8080/users](http://127.0.0.1:8080/users) and check it out! :) For the lack of a better named I have called the application _backend_, but it could really be anything you fancy. @@ -77,7 +78,7 @@ Since the `servant` template project has given us the endpoint `127.0.0.1:8080/u By default the chromium developer tools are enabled in `Electron`. I suggest you keep them enabled while debugging, since that makes it a lot easier to see if anything went wrong. If you want to disable it, you just need to comment/remove a line in `Haskell-Electron-app/haskell-app/main.js`: -```javascript +```typescript ,linenos ... function createWindow () { // Create the browser window, @@ -91,11 +92,11 @@ function createWindow () { ... ``` -Short interlude: we'll be a bit lazy and download [jQuery 2.2.3 minified](https://code.jquery.com/jquery-2.2.3.min.js){target="_blank" rel="noopener noreferrer"}. Put that into `Haskell-Electron-app/haskell-app/resources/jQuery-2.2.3.min.js` so we can include it later on and get the nice AJAX functionality it provides. +Short interlude: we'll be a bit lazy and download [jQuery 2.2.3 minified](https://code.jquery.com/jquery-2.2.3.min.js). Put that into `Haskell-Electron-app/haskell-app/resources/jQuery-2.2.3.min.js` so we can include it later on and get the nice AJAX functionality it provides. Back to work in `Haskell-Electron-app/haskell-app`, lets change the `index.html` page and prepare it for our list of users. -```html +```html ,linenos @@ -126,7 +127,7 @@ Back to work in `Haskell-Electron-app/haskell-app`, lets change the `index.html` And finally we'll implement the logic in `renderer.js`, -```javascript +```typescript ,linenos // Backend and endpoint details. const host = 'http://127.0.0.1:8080' const endpoint = '/users' @@ -172,7 +173,7 @@ fetchUserList(waitTimeBetweenAttempt, maxNoOfAttempts) We simply request the `JSON` data at `http://127.0.0.1:8080/users`, with `$.getJSON(...)`, and display it if we received the data. If the request failed, we keep retrying until we either get a response or reach the maximum number of attempts (here set to 50 via `maxNoOfAttempts`). -The real purpose behind the retry will become apparent later on, when we might need to wait for the server to become available. Normally you will use a status endpoint that you are 100% sure is correct and not failing to check for the availability (inspired by the answer [Mike from Wagon HQ gave here](https://www.reddit.com/r/haskell/comments/4ipah2/resources_for_electron_haskell/d30mupm){target="_blank" rel="noopener noreferrer"}). +The real purpose behind the retry will become apparent later on, when we might need to wait for the server to become available. Normally you will use a status endpoint that you are 100% sure is correct and not failing to check for the availability (inspired by the answer [Mike from Wagon HQ gave here](https://www.reddit.com/r/haskell/comments/4ipah2/resources_for_electron_haskell/d30mupm)). ## Launching the Haskell web server from Electron @@ -180,7 +181,7 @@ Now to the interesting part, let's try to launch the Haskell web server from ins First though, let us set the `haskell-app/resources` folder as the target for our binary, in the `stack.yaml` file, with the `local-bin-path` configuration value. -```yaml +```yaml ,linenos resolver: lts-5.15 local-bin-path: ../haskell-app/resources ... @@ -199,7 +200,7 @@ After that it is surprisingly easy to launch the executable from within `Electro Since there are bits and pieces that are added I'll include the whole `Haskell-Electron-app/haskell-app/main.js` file, with most of the comments removed. -```javascript +```typescript ,linenos const electron = require('electron') // Used to spawn processes. const child_process = require('child_process') @@ -246,13 +247,13 @@ app.on('activate', function () { Let's briefly go through what is happening: -* We are using the [child_process.spawn](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options){target="_blank" rel="noopener noreferrer"} command to launch our backend web server -* We imported the `child_process` module with `const child_process = require('child_process')` -* Defined a variable `let backendServer` that'll let us keep the backend server from being garbage collected -* Added a function `createBackendServer` that runs `child_process.spawn('./resources/backend-exe')` to spawn the process -* Added the `createBackendServer` function to the `ready` hook with `app.on('ready', createBackendServer)` -* Close the `backendServer` when the event `will-quit` occurs +- We are using the [child_process.spawn](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) command to launch our backend web server +- We imported the `child_process` module with `const child_process = require('child_process')` +- Defined a variable `let backendServer` that'll let us keep the backend server from being garbage collected +- Added a function `createBackendServer` that runs `child_process.spawn('./resources/backend-exe')` to spawn the process +- Added the `createBackendServer` function to the `ready` hook with `app.on('ready', createBackendServer)` +- Close the `backendServer` when the event `will-quit` occurs __And voila!__ We now have Electron spawning a process that runs a Haskell web server! :) -Next step would be to package the app up for distribution to see if that affects anything, but I'll save that for another time (and `Electron` already has [a page on distribution here](http://electron.atom.io/docs/v0.37.8/tutorial/application-distribution/){target="_blank" rel="noopener noreferrer"}). +Next step would be to package the app up for distribution to see if that affects anything, but I'll save that for another time (and `Electron` already has [a page on distribution here](http://electron.atom.io/docs/v0.37.8/tutorial/application-distribution/)). diff --git a/posts/2018-02-07-Mobile-Haskell.md b/content/posts/2018-02-07-mobile-haskell/index.md similarity index 64% rename from posts/2018-02-07-Mobile-Haskell.md rename to content/posts/2018-02-07-mobile-haskell/index.md index 30e6383..ac5ebf9 100644 --- a/posts/2018-02-07-Mobile-Haskell.md +++ b/content/posts/2018-02-07-mobile-haskell/index.md @@ -1,34 +1,36 @@ ---- -title: Mobile Haskell (iOS) -tags: haskell, mobile, iOS -date: 2018-02-07 -versions: -- 'Xcode Version 9.2 (9C40b)' -- 'Cabal HEAD (commit [94a7374](https://github.com/haskell/cabal/commit/94a7374d7b1a9b55454209e92d5057ba81be7d6a){target="_blank" rel="noopener noreferrer"})' -- 'Stack Version 1.6.3' -- 'LLVM Version 5.0.1' ---- ++++ +title = "Mobile Haskell (iOS)" +date = 2018-02-07 +[taxonomies] +tags = ["haskell", "mobile", "iOS"] +[extra] +versions = ["Xcode Version 9.2 (9C40b)", "Cabal HEAD (commit [94a7374](https://github.com/haskell/cabal/commit/94a7374d7b1a9b55454209e92d5057ba81be7d6a){target=\"_blank\" rel=\"noopener noreferrer\"})", "Stack Version 1.6.3", "LLVM Version 5.0.1"] ++++ A lot of progress has been going on to make Haskell work on mobile natively, instead of e.g. generating JavaScript via GHCJS and using that. Unfortunately, not much documentation exists yet on how to build a project using these tools all together. This post will be an attempt to piece together the tools and various attempts into a coherent step-by-step guide. We will start by setting up the tools needed, and then build an iOS app that runs in both the simulator and on the device itself (i.e. a x86 build and an arm build). - + For the impatient and brave, simply, -- clone down the [MobileHaskellFun](https://github.com/Tehnix/MobileHaskellFun){target="_blank" rel="noopener noreferrer"} repository, +- clone down the [MobileHaskellFun](https://github.com/Tehnix/MobileHaskellFun) repository, - run `./setup-tools.sh` to set up the tools, - `cd` into `Offie/hs-src/` - build the package index `./call x86_64-apple-ios-cabal new-update --allow-newer`, - run `./call make iOS` to compile the program for iOS, - and finally launch Xcode and start the simulator. +Otherwise, let's go through the steps in detail: + +{{ toc() }} + ## Setting up the Tools A bunch of tools are needed, so we will set these up first. You might have some of these, but I will go through them anyways, for good measure. The steps will assume that we are on macOS for some parts, but it should not be part to adapt these to your system (all steps using `brew`). -If you don't have [stack](https://docs.haskellstack.org/en/stable/README/){target="_blank" rel="noopener noreferrer"} installed already, set it up with, +If you don't have [stack](https://docs.haskellstack.org/en/stable/README/) installed already, set it up with, ```bash $ curl -sSL https://get.haskellstack.org/ | sh @@ -40,7 +42,7 @@ We will collect all our tools and GHC versions in a folder in `$HOME`—for conv $ mkdir -p ~/.mobile-haskell ``` -Next step is cloning down [cabal](github.com/haskell/cabal){target="_blank" rel="noopener noreferrer"} and building cabal-install. This is necessary until `new-update` lands. +Next step is cloning down [cabal](github.com/haskell/cabal) and building cabal-install. This is necessary until `new-update` lands. ```bash $ cd ~/.mobile-haskell @@ -60,7 +62,7 @@ $ brew install llvm This should set up LLVM in `/usr/local/opt/llvm@5/bin` (or just `/usr/local/opt/llvm/bin`), remember this path for later. -We'll now set up the tools from [http://hackage.mobilehaskell.org](http://hackage.mobilehaskell.org){target="_blank" rel="noopener noreferrer"}, namely the toolchain-wrapper and the different GHC versions we will use. +We'll now set up the tools from [http://hackage.mobilehaskell.org](http://hackage.mobilehaskell.org), namely the toolchain-wrapper and the different GHC versions we will use. Let's start off with getting our GHCs, by downloading `ghc-8.4.0.20180109-x86_64-apple-ios.tar.xz` and `ghc-8.4.0.20180109-aarch64-apple-ios.tar.xz`, for the simulator and device respectively. You can download the by cliking their links on the website, or curl them down with (the links are probably outdated soon, so replace the links with the ones on the site), @@ -120,7 +122,7 @@ $ make && make install We should now have our libffi files for the two targets living in `~/.mobile-haskell/lffi/aarch64-apple-ios` and `~/.mobile-haskell/lffi/x86_64-apple-ios` respectively. --> -And that's it! We have now set up all the tools we need for later. If you want all the steps as a single script, check out the [setup script in the MobileHaskellFun repo](https://github.com/Tehnix/MobileHaskellFun/blob/master/setup-tools.sh){target="_blank" rel="noopener noreferrer"}. +And that's it! We have now set up all the tools we need for later. If you want all the steps as a single script, check out the [setup script in the MobileHaskellFun repo](https://github.com/Tehnix/MobileHaskellFun/blob/master/setup-tools.sh). ## Setting up the Xcode Project @@ -128,25 +130,20 @@ Setting up Xcode is a bit of a visual process, so I'll augment these steps with First, let's set up our Xcode project, by creating a new project. - +{{ image(path="mobile-haskell-1. Create Project.png", caption="1. Create Project", width=600) }} Choose `Single View Application`, - +{{ image(path="mobile-haskell-1.1. Create Project - Single View Application.png", caption="1.1. Create Project - Single View Application", width=600) }} And set the name and location of your project, - -Now, let's add a folder to keep our Haskell code in and call it `hs-src`, by right-clicking our project and adding a `New Group`, +{{ images(paths=["mobile-haskell-1.2. Create Project - Name.png", "mobile-haskell-1.3. Create Project - Set Location.png"], captions=["1.2. Create Project - Name", "1.3. Create Project - Set Location"], widths=[400, 400]) }} - +Now, let's add a folder to keep our Haskell code in and call it `hs-src`, by right-clicking our project and adding a `New Group`, +{{ image(path="mobile-haskell-2. Add Source Folder for Haskell Code.png", caption="2. Add Source Folder for Haskell Code", width=300) }} ## Interlude: Set up the Haskell Code @@ -162,7 +159,7 @@ $ touch MobileFun.cabal cabal.project Makefile call LICENSE src/Lib.hs We use the features of `cabal.project` to set our package repository to use the hackage.mobilehaskell.org overlay. -```haskell +```haskell ,linenos packages: . repository hackage.mobilehaskell @@ -178,7 +175,7 @@ repository hackage.mobilehaskell Just a simple cabal package setup. -```haskell +```haskell ,linenos name: MobileFun version: 0.1.0.0 license: BSD3 @@ -202,7 +199,7 @@ library The Makefile simplifies a lot of the compilation process and passes the flags we need to use. -```makefile +```makefile ,linenos LIB=MobileFun ARCHIVE=libHS${LIB} @@ -250,7 +247,7 @@ clean: Our Haskell code for now, is simply some C FFI that sets up a small toy function. -```haskell +```haskell ,linenos module Lib where import Foreign.C (CString, newCString) @@ -270,7 +267,7 @@ hello = "Hello from Haskell" We use the `call` script to set up the various path variables that point to our tools, so we don't need these polluting our global command space. If you've followed the setup so far, the paths should match out-of-the-box. -```bash +```bash ,linenos #!/usr/bin/env bash # Path to LLVM (this is the default when installing via `brew`) export PATH=/usr/local/opt/llvm@5/bin:$PATH @@ -325,23 +322,15 @@ We should now have our library file at `hs-src/binaries/iOS/libHSMobileFun.a`. I Now we need to tie together the Haskell code with Xcode. Drag-and-drop the newly created files into the `hs-src` group in Xcode (if it hasn't found it by itself). - +And set the name and location of your project, + +{{ image(path="mobile-haskell-3. Drag the files to Xcode.png", caption="3. Drag the files to Xcode", width=600) }} Since we are using Swift, we need a bridging header to bring our C prototypes into Swift. We'll do this by adding an Objective-C file to the project, `tmp.m`, which will make Xcode ask if we want to create a bridging header, `Offie-Bridging-Header.h`, for which we will answer yes. - - - +{{ image(path="mobile-haskell-4. Create Objective-C File.png", caption="4. Create Objective-C File", width=300) }} - +{{ images(paths=["mobile-haskell-4.1. Create Objective-C File - Choose Filetype.png", "mobile-haskell-4.2. Create Objective-C File - Set Name.png"], captions=["4.1. Create Objective-C File - Choose Filetype", "4.2. Create Objective-C File - Set Name"], widths=[400, 400]) }} @@ -349,7 +338,7 @@ Since we are using Swift, we need a bridging header to bring our C prototypes in In our bridging file, `Offie-Bridging-Header.h`, we add our prototypes that we need to glue in the Haskell code, -```c +```c ,linenos extern void hs_init(int * argc, char ** argv[]); extern char * hello(); ``` @@ -358,7 +347,7 @@ extern char * hello(); Now let's go into `AppDelegate.swift` and call `hs_init` to initialize the Haskell code, -```objective-c +```objective-c ,linenos import UIKit @UIApplicationMain @@ -388,15 +377,15 @@ Next, we will set up a label in a view controller. You can either set this up in First go into the `Main.storyboard` and create a label element somewhere on the screen. - +{{ image(path="mobile-haskell-7. Add Label.png", caption="7. Add Label", width=600) }} Then enable the `Assistant Editor` in the top right cornor, and ctrl-click on the label, dragging it over to the `ViewController.swift` and name `helloWorldLabel`. - +{{ image(path="mobile-haskell-7.1. Add Label - Connect IBOutlet.png", caption="7.1. Add Label - Connect IBOutlet", width=600) }} We can now set the text of the label by calling our Haskell function with `cString: hello()`, making our `ViewController.swift` look like, -```objective-c +```objective-c ,linenos import UIKit class ViewController: UIViewController { @@ -420,46 +409,43 @@ The final step we need to do, is linking in our library that we built earlier, ` We do this by going into `Build Phases`, which is exposed under the Xcode project settings, and click the `+` to add a new library, - +{{ image(path="mobile-haskell-5. Build Phases.png", caption="5. Build Phases", width=650) }} Choose `Add Other...` to locate the library, - +{{ image(path="mobile-haskell-5.1. Build Phases - Add New.png", caption="5.1. Build Phases - Add New", width=650) }} and finally locate the library file in `hs-src/binaries/iOS/libHSMobileFun.a`, - +{{ image(path="mobile-haskell-5.2. Build Phases - Locate the Library.png", caption="5.2. Build Phases - Locate the Library", width=650) }} We also need to set the build to not generate bytecode, because we are using the external GHC library. This is done under `Build Settings`, locating `Enable Bitcode` (e.g. via the search) and setting it to `No`. - +{{ image(path="mobile-haskell-6. Build Settings.png", caption="6. Build Settings", width=650) }} ## Run the Code! Final step, let's run our code in the simulator - +{{ image(path="mobile-haskell-9. Run Simulator.png", caption="9. Run Simulator", width=300) }} Congratulations! You're now calling Haskell code from Swift and running it in an iOS simulator. -
"Hello, world!"
} +} +``` + +We can now run our App using Trunk: + +```bash +$ trunk serve --open +``` + +Voila, we've got a little Hello World Leptos app! + +#### Setting up Tailwind CSS + +Let's configure Tailwind CSS for our Leptos App. First, we need to tell Tailwind where to look for files that might contain our CSS classes. Create a `ui-internal/tailwind.config.ts` file with the following: + + +```typescript ,linenos +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: { + files: ["*.html", "./src/**/*.rs"], + }, + theme: { + extend: {}, + }, + plugins: [], +}; +``` + +We also need to tell `trunk` to build Tailwind CSS as part of its build process. We can do this by creating a `ui-internal/Trunk.toml` file: + + +```toml ,linenos +[[hooks]] +stage = "pre_build" +command = "sh" +command_arguments = [ + "-c", + "bunx tailwindcss --input resources/input.css --output public/output.css", +] +``` + +This let's `trunk` know that before it builds our WASM App, it should run the `bunx tailwindcss ...` command, which will generate our Tailwind CSS file, which it puts into `public/output.css`. + +Now, you might have noticed we also have an input file. Let's get that set up, along with a `resources/` folder: + +```bash +$ mkdir ui-internal/resources +``` + +We'll then create our base Tailwind CSS file at `ui-internal/resources/input.css`, mimicing our Next.js setup: + + +```css ,linenos +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +``` + +Final step, we need to pull in our Tailwind CSS file in our `index.html`. Update the contents to the following: + + +```html ,linenos + + + + + + + +(path: P, view: F) -> impl IntoView
+where
+ P: std::fmt::Display,
+ F: Fn() -> IV + 'static,
+ IV: IntoView,
+{
+ view! {
+
-
- Dioxus (WASM)
- Yew (WASM)
- …etc | - Capacitor (WASM in WebView)
- Tauri (Beta, WASM in WebView) |
-| TypeScript | - Node.js
- Bun | - Native Support (React, Vue, Svelte, Solid, etc) | - Capacitor (JS in WebView) |
+| Language / Target | Backend Options | Frontend Options | Mobile Options |
+|-------------|------------------|--------------------------|-------------------------------|
+| Rust | - Native Support | - Leptos (WASM)
- Dioxus (WASM)
- Yew (WASM)
- …etc | - Capacitor (WASM in WebView)
- Tauri (Beta, WASM in WebView) |
+| TypeScript | - Node.js
- Bun | - Native Support (React, Vue, Svelte, Solid, etc) | - Capacitor (JS in WebView) |
+| Dart/Flutter| - Native Support | - Native for Dart
- Flutter (HTML or Canvas) | - Flutter |
For Rust there are some interesting projects brewing such as [Robius](https://robius.rs) which is quite ambitious and hopefully gains traction.
+The choice is not yet super clear here, and all could work so far. One concern I have with Dart/Flutter specifically is the lack of maturity in its Backend ecosystem currently. It you’ll quickly run into missing or unmaintained libraries.
+
**Choice**: Rust & WASM
-**Alternative**: TypeScript & Node.js/Bun
+**Alternative**: TypeScript + Node.js/Bun or Dart/Flutter
## **All Targets**
@@ -77,9 +85,7 @@ If you want to develop an iOS Widget ([those little things](https://support.appl
Here’s an example of a Widget for a Todo App, providing interactivity from the Home Screen:
-
+{{ image(path="mobile-a-different-way-widget-screenshot.jpg", caption="Screenshot of Capacitor an iOS Widget", width=450) }}
This seemingly innocuous example is also where most of our options get limited and we’ll have to get a bit creative with how we solve it.
@@ -87,14 +93,15 @@ So how does support look like for our Mobile options? (we’ll simplify for a mo
| Mobile Options / Target Support | iOS App | macOS App | Widget Extension | watchOS | watchOS Complication |
| --- | --- | --- | --- | --- | --- |
-| Tauri | ✅ (sorta)* | ✅ (same App as the iOS App)*** | ❌ | ❌ | ❌ |
-| Capacitor | ✅ (sorta)** | ✅ (same App as the iOS App)*** | ✅⚠️ (via Native code) | ✅⚠️ (via Native code) | ✅⚠️ (via Native code) |
+| Tauri | ✅ (sorta)[^1] | ✅ (same App as the iOS App)[^2] | ❌ | ❌ | ❌ |
+| Capacitor | ✅ (sorta)[^3] | ✅ (same App as the iOS App)[^2] | ✅⚠️ (via Native code) | ✅⚠️ (via Native code) | ✅⚠️ (via Native code) |
+| Flutter | ✅ | ✅ | ✅⚠️ (via Native code) | ✅⚠️ (via Native code) | ✅⚠️ (via Native code) |
-*: Platform support overview here [https://v2.tauri.app/plugin/](https://v2.tauri.app/plugin/)
+[^1]: Platform support overview here [https://v2.tauri.app/plugin/](https://v2.tauri.app/plugin/)
-**: Platform support overview here [https://capacitorjs.com/docs/apis](https://capacitorjs.com/docs/apis)
+[^2]: This is the “Designed for iPad” type of App which is essentially an iOS App that runs completely native on ARM-based Macbooks. Alternatively you can also deploy it as an Electron App using [https://github.com/capacitor-community/electron](https://github.com/capacitor-community/electron) for other targets.
-***: This is the “Designed for iPad” type of App which is essentially an iOS App that runs completely native on ARM-based Macbooks. Alternatively you can also deploy it as an Electron App using [https://github.com/capacitor-community/electron](https://github.com/capacitor-community/electron) for other targets.
+[^3]: Platform support overview here [https://capacitorjs.com/docs/apis](https://capacitorjs.com/docs/apis)
Before continuing, let’s talk about that ✅⚠️ score though—what does that mean exactly?
@@ -104,23 +111,19 @@ But will our chosen framework support that, or will we get completely blocked ev
For Tauri, the answer is: We are blocked ([#9766](https://github.com/tauri-apps/tauri/issues/9766) is still open in the Tauri repo), and cannot proceed.
-Luckily, for Capacitor, that’s not entirely the case. We can open XCode and add our own Targets that will build alongside our App, for anything that we want, such as Widgets and our watchOS App.
+Luckily, for both Capacitor and Dart/Flutter, that’s not entirely the case. We can open XCode and add our own Targets that will build alongside our App, for anything that we want, such as Widgets and our watchOS App.
That looks like this in XCode:
-
-
-The limitation here is that these additional targets only support native code (i.e. Swift), which neatly brings us onto our next section.
+{{ image(path="mobile-a-different-way-xcode-adding-targets.png", caption="Adding additional Targets in Xcode", width=650) }}
-Now, getting back to the topic of the main app, how do we actually communicate with and/or use the native capabilities that Capacitor supports?
+The limitation here is that these additional targets only support native code (i.e. Swift or Kotlin/Java).
-I’ve written up a guide on how exactly to do that in detail here [Using Capacitor Plugins from Rust/WASM](/posts/2024-06-24-using-capacitor-plugins-from-rust-wasm.html).
+**Before jumping into the next section where we’ll look at code sharing**, how do we actually communicate with and/or use the native capabilities that Capacitor supports? I’ve written up a guide on how exactly to do that in detail here: [Using Capacitor Plugins from Rust/WASM](/posts/2024-06-24-using-capacitor-plugins-from-rust-wasm.html).
**Choice**: Capacitor
-**Alternative**: None
+**Alternative**: Dart/Flutter
## **Code Sharing**
@@ -132,12 +135,15 @@ This is where we get into the differences in our language choice. Our options ar
- Calling Rust using [UniFFI](https://mozilla.github.io/uniffi-rs/latest/) and generated Swift bindings
- Calling JavaScript by evaluating it using [JavaScriptCore](https://developer.apple.com/documentation/javascriptcore)
+- Calling Flutter via FFI generated using [add-to-app](https://docs.flutter.dev/add-to-app)
We can quickly exclude the [JavaScriptCore](https://developer.apple.com/documentation/javascriptcore) option as a viable route, it’s simply not gonna be even remotely ergonomic.
You essentially import your JavaScript file, or put the code in a String. Then you evaluate it, and can get the output. There’s not much interop between the host language (Swift) and the guest language (JavaScript), providing for poor ergonomics in our usecase (some examples [here](https://stackoverflow.com/a/37435316) and [here](https://douglashill.co/javascript-in-swift/)).
-How does the Rust side of things then look like?
+Dart/Flutter does support calling [Objective-C/Swift from within Dart](https://dart.dev/interop/objective-c-interop#callbacks-and-multithreading-limitations), and with a bit of work it does seem to be able to generate FFI bindings via Flutter to go the other way as well, with a few examples given in [their documentation](https://docs.flutter.dev/add-to-app).
+
+How does the Rust side of things then look like? Very much like Dart/Flutter actually!
1. Use the [uniffi crate](https://crates.io/crates/uniffi)
2. Annotate the functions, enums, records, etc that you want to export with a macro
@@ -146,56 +152,67 @@ How does the Rust side of things then look like?
5. Import the generated Swift and `.xcframework` into XCode
6. Done!
-I’ve written up a guide on how exactly to do that in detail here [Setting up UniFFI for iOS, Simulators, and watchOS](/posts/2024-06-24-setting-up-uniffi-for-ios-simulators-and-watchos.html).
+I’ve written up a guide on how exactly to do that in detail here: [Setting up UniFFI for iOS, Simulators, and watchOS](/posts/2024-06-24-setting-up-uniffi-for-ios-simulators-and-watchos.html).
Once you’ve done the initial project setup (for building the targets and generating the `.xcframework` file) you don’t really touch that again, and you only need to concern yourself with which things to expose to Swift.
Let’s make a small example of code using `uniffi` would look like:
-https://gist.github.com/Tehnix/2a7c3059816f04fe2ccbc5390c96e3ed.js?file=uniffi-example.rs
+
+```rust ,linenos
+uniffi::setup_scaffolding!();
+
+#[derive(uniffi::Enum)]
+pub enum Fruits {
+ Watermelon,
+ Cranberry,
+ Cherry,
+}
+
+#[uniffi::export]
+pub fn eat_fruit(fruit: Fruits) -> String {
+ match fruit {
+ Fruits::Watermelon => "Eating Watermelon".to_string(),
+ Fruits::Cranberry => "Eating Cranberry".to_string(),
+ Fruits::Cherry => "Eating Cherry".to_string(),
+ }
+}
+```
How do we call the generated code? As you would any other Swift code!
-https://gist.github.com/Tehnix/2a7c3059816f04fe2ccbc5390c96e3ed.js?file=Example.swift
+
+```swift ,linenos
+// Calling our Rust function with our Rust enum as an argument.
+eatFruit(fruit: Fruits.watermelon)
+```
-https://gist.github.com/Tehnix/2a7c3059816f04fe2ccbc5390c96e3ed.js?file=Example.swift%20(**Code%20Sharing**).swift
+
+```swift ,linenos
+// It'll work everywhere you'd expect it to, e.g. in String interpolation here.
+Text("Ready? \(eatFruit(fruit: Fruits.watermelon))")
+```
-Final result in the iOS Simulator, running our Web App as a Mobile App:
+Final result in the iOS and Android Simulators, running our Web App as a Mobile App:
-Final result in the iOS Simulator, running our Web App as a Mobile App:
+{{ images(paths=["mobile-a-different-way-ios-simulator.png", "mobile-a-different-way-android-simulator.png"], captions=["Final result in the iOS Simulator", "Final result in the Android Simulator"], widths=[400, 400]) }}
-
-
-In the Android Simulator:
-
-
-
Same application on the Web:
-
+{{ image(path="mobile-a-different-way-web.png", caption="Final result on the Web", width=500) }}
The Widget Extension in the XCode Preview:
-
+{{ image(path="mobile-a-different-way-widget-preview.png", caption="Final result as a Widget Preview in Xcode", width=600) }}
The watchOS App in the XCode Preview:
-
+{{ image(path="mobile-a-different-way-watchos-preview.png", caption="Final result as a watchOS Preview in Xcode", width=600) }}
**Choice**: Rust with [UniFFI](https://mozilla.github.io/uniffi-rs/latest/)
-**Alternative**: None
+**Alternative**: Dart/Flutter
## Conclusion
@@ -211,4 +228,8 @@ Let’s revisit our original goals with our final results:
- **Code Sharing**: If we must drop into native code (Swift/Kotlin) we should be able to use code from our “core” language
- Expose our Rust code to our native Swift code using UniFFI
+Why not Dart/Flutter? My main holdback stems from the maturity the language and ecosystem has on the Backend, which seems to be quite lacking. Rust definitely has it beat here, with an extremely vibrant ecosystem.
+
I’m personally pretty happy with the solution. Most people probably won’t need the watchOS and Widgets, so they won’t have to touch Swift code, but it’s at least nice to know that you haven’t closed off that option for yourself down the road, as some options would leave you.
+
+[👉 Let me know what you think over on Medium](https://codethoughts.medium.com/mobile-a-different-way-using-rust-0e7b1dfa8cbf) or down in the comments below.
diff --git a/resources/images/mobile-a-different-way-android-simulator.png b/content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-android-simulator.png
similarity index 100%
rename from resources/images/mobile-a-different-way-android-simulator.png
rename to content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-android-simulator.png
diff --git a/resources/images/mobile-a-different-way-ios-simulator.png b/content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-ios-simulator.png
similarity index 100%
rename from resources/images/mobile-a-different-way-ios-simulator.png
rename to content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-ios-simulator.png
diff --git a/resources/images/mobile-a-different-way-watchos-preview.png b/content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-watchos-preview.png
similarity index 100%
rename from resources/images/mobile-a-different-way-watchos-preview.png
rename to content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-watchos-preview.png
diff --git a/resources/images/mobile-a-different-way-web.png b/content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-web.png
similarity index 100%
rename from resources/images/mobile-a-different-way-web.png
rename to content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-web.png
diff --git a/resources/images/mobile-a-different-way-widget-preview.png b/content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-widget-preview.png
similarity index 100%
rename from resources/images/mobile-a-different-way-widget-preview.png
rename to content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-widget-preview.png
diff --git a/resources/images/mobile-a-different-way-widget-screenshot.jpeg b/content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-widget-screenshot.jpg
similarity index 100%
rename from resources/images/mobile-a-different-way-widget-screenshot.jpeg
rename to content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-widget-screenshot.jpg
diff --git a/resources/images/mobile-a-different-way-xcode-adding-targets.png b/content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-xcode-adding-targets.png
similarity index 100%
rename from resources/images/mobile-a-different-way-xcode-adding-targets.png
rename to content/posts/2024-06-25-mobile-a-different-way/mobile-a-different-way-xcode-adding-targets.png
diff --git a/resources/images/how-i-structure-my-work-as-a-solor-founder-goals.png b/content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/how-i-structure-my-work-as-a-solor-founder-goals.png
similarity index 100%
rename from resources/images/how-i-structure-my-work-as-a-solor-founder-goals.png
rename to content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/how-i-structure-my-work-as-a-solor-founder-goals.png
diff --git a/resources/images/how-i-structure-my-work-as-a-solor-founder-initiatives.png b/content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/how-i-structure-my-work-as-a-solor-founder-initiatives.png
similarity index 100%
rename from resources/images/how-i-structure-my-work-as-a-solor-founder-initiatives.png
rename to content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/how-i-structure-my-work-as-a-solor-founder-initiatives.png
diff --git a/resources/images/how-i-structure-my-work-as-a-solor-founder-issues.png b/content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/how-i-structure-my-work-as-a-solor-founder-issues.png
similarity index 100%
rename from resources/images/how-i-structure-my-work-as-a-solor-founder-issues.png
rename to content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/how-i-structure-my-work-as-a-solor-founder-issues.png
diff --git a/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder.md b/content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/index.md
similarity index 84%
rename from posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder.md
rename to content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/index.md
index 7e1df24..39d3d45 100644
--- a/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder.md
+++ b/content/posts/2024-06-26-how-i-structure-my-work-as-a-solor-founder/index.md
@@ -1,8 +1,9 @@
----
-title: "How I structure work as a Solo Founder"
-tags: project management, solo founder, codetalk
-date: 2024-06-26
----
++++
+title = "How I structure work as a Solo Founder"
+date = 2024-06-26
+[taxonomies]
+tags = ["project management", "solo founder", "codetalk"]
++++
This is for sure something that will keep evolving over time, but I got inspired by [this Hacker News post](https://news.ycombinator.com/item?id=40742831) to share in more detail how I organize myself and my work (you can see my short answer [here](https://news.ycombinator.com/item?id=40743669)).
@@ -16,9 +17,11 @@ You could rephrase this slightly to: How do I run *my own* project management?
For most Engineers, this is not actually something they’ve needed to do often, especially if working in larger corporations. The people that have worked at Startups are a mixed bag, where some learn the value of structure, and others managed to get by without since the fast-paced environment changes things around all the time anyways.
-I’ve personally landed in the camp of: Structure is valuable, even crucial, but it should never get in my way. Best of both worlds sorta.
+I’ve personally landed in the camp of: **Structure is valuable, even crucial, but it should never get in my way. Best of both worlds sorta.**
-
+
+
+{{ toc() }}
## What is my context?
@@ -64,9 +67,7 @@ At the time of writing, I currently have two focuses for the month of July:
In Linear, I’ve structure the monthly goals using their new [Initiatives](https://linear.app/docs/initiatives), which gives me a great way to write a high-level summary of the goal, and gather the related projects that will get me there into each of them.
-
+{{ image(path="how-i-structure-my-work-as-a-solor-founder-initiatives.png", caption="A view of my Initiatives in Linear for July 2024", width=700) }}
A view of my Initiatives in Linear for July 2024.
@@ -90,9 +91,7 @@ I then use this prioritization as the input for my daily goals, and it also make
In Linear I’ve set up a Custom View with a simple filter on the Initiatives I have active at the moment, named “Initiative Issues” in the screenshot below.
-
+{{ image(path="how-i-structure-my-work-as-a-solor-founder-issues.png", caption="A Custom View with a filter on the currently active Initiatives", width=700) }}
A Custom View with a filter on the currently active Initiatives.
@@ -102,9 +101,7 @@ Arriving at the final step, I start from the “Initiative Issues” list and pi
These issues then go into another Custom View called “Goal of the day” which just gathers all issues with the `Goal` label on them, as well as having myself assigned to them.
-
+{{ image(path="how-i-structure-my-work-as-a-solor-founder-goals.png", caption="A Custom View with a filter on “Goals", width=700) }}
A Custom View with a filter on “Goals” label being added.
@@ -150,4 +147,6 @@ Honestly, most things would work, and you can even just do it with pen-and-paper
As an alternative to Linear, I’d probably recommend Notion. You will need to set up the structure more yourself, but you can wrangle their [Projects](https://www.notion.so/product/projects) functionality into something that will allow you a very similar setup.
-I always love to get inspiration, so I’d love to hear from you about your own preferred way of organizing yourself. [Let me know what you think over on Medium.](https://codethoughts.medium.com/how-i-structure-work-as-a-solo-founder-4944afcfc2f2)
+I always love to get inspiration, so I’d love to hear from you about your own preferred way of organizing yourself.
+
+[👉 Let me know what you think over on Medium](https://codethoughts.medium.com/how-i-structure-work-as-a-solo-founder-4944afcfc2f2) or in the comments below.
diff --git a/content/posts/_index.md b/content/posts/_index.md
new file mode 100644
index 0000000..d2f87a6
--- /dev/null
+++ b/content/posts/_index.md
@@ -0,0 +1,8 @@
++++
+title = "Posts"
+sort_by = "date"
+template = "posts.html"
+page_template = "post-page.html"
+transparent = true
+insert_anchor_links = "heading"
++++
diff --git a/deploy_rsa.enc b/deploy_rsa.enc
deleted file mode 100644
index c9e7747..0000000
Binary files a/deploy_rsa.enc and /dev/null differ
diff --git a/justfile b/justfile
index ae54085..d20787e 100644
--- a/justfile
+++ b/justfile
@@ -12,49 +12,41 @@ code:
[linux]
install-tooling:
@ just _install-tooling-all-platforms
- # Install imagemagick for mogrify.
- command -v mogrify >/dev/null 2>&1 || sudo apt install imagemagick
- gh release download --clobber --pattern "hakyll-site" --dir ./dist
- chmod +x ./dist/hakyll-site
+ echo "Currently unsupported, see https://www.getzola.org/documentation/getting-started/installation/ for installation instructions."
# Install tooling for working with the codetalk blog.
[macos]
install-tooling:
@ just _install-tooling-all-platforms
- # Install imagemagick for mogrify.
- command -v mogrify >/dev/null 2>&1 || brew install imagemagick
+ # Install zola.
+ brew install zola
_install-tooling-all-platforms:
- # Install stack.
- command -v stack >/dev/null 2>&1 || curl -sSL https://get.haskellstack.org/ | sh
- # Install ghcup.
- command -v ghcup >/dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
- # Install sass.
- command -v sass >/dev/null 2>&1 || npm i -g sass
+ echo "Setting up tools..."
-# Setup dependencies and build the hakyll executable.
-setup:
- stack build
-
-# Deploy the blog to S3 and invalidate CloudFront cache.
-deploy:
- #!/usr/bin/env bash
- set -euxo pipefail
- just build
- # Sync files to S3.
- aws s3 sync _site s3://codetalk.io --delete
- # Invalidate Cloudflare cache.
- curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/purge_cache" \
- -H "Authorization: Bearer ${CLOUDFLARE_TOKEN}" \
- -H "Content-Type: application/json" \
- --data '{"purge_everything":true}'
-
-# Run hakyll development server in watch mode.
+# Run zola development server in watch mode.
dev:
- stack exec -- hakyll-site watch --port 4566
+ bunx concurrently --names tailwind,zola "bunx tailwindcss --input styles/input.css --output static/styles/output.css --watch" "zola serve --port 4566"
# Build blog artifacts and static files.
build:
- ./dist/hakyll-site clean || stack exec -- hakyll-site clean
- ./dist/hakyll-site clean || stack exec -- hakyll-site build
- sass resources/scss/app.scss:_site/app.css --style compressed
+ bunx tailwindcss --input styles/input.css --output static/styles/output.css --minify
+ zola build
+
+# Generate posts with Gists instead of code blocks.
+generate-gists:
+ cd tools && bun run index.ts
+
+# TODO: Move deployment to CloudFlare Pages?
+# Deploy the blog to S3 and invalidate CloudFront cache.
+# deploy:
+# #!/usr/bin/env bash
+# set -euxo pipefail
+# just build
+# # Sync files to S3.
+# aws s3 sync _site s3://codetalk.io --delete
+# # Invalidate Cloudflare cache.
+# curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/purge_cache" \
+# -H "Authorization: Bearer ${CLOUDFLARE_TOKEN}" \
+# -H "Content-Type: application/json" \
+# --data '{"purge_everything":true}'
diff --git a/resources/bookmarks/April.html b/resources/bookmarks/April.html
deleted file mode 100644
index c8e9a50..0000000
--- a/resources/bookmarks/April.html
+++ /dev/null
@@ -1,307 +0,0 @@
- April
-