MicroPosts

  • AI is not about working faster

    This is why I care so much about the productivity boost I get from LLMs so much: it's not about getting work done faster, it's about being able to ship projects that I wouldn't have been able to justify spending time on at all.

    Source: Here's how I use LLMs to help me write code

  • Exploring codebases with AI

    Good LLMs are great at answering questions about code.

    This is also very low stakes: the worst that can happen is they might get something wrong, which may take you a tiny bit longer to figure out. It's still likely to save you time compared to digging through thousands of lines of code entirely by yourself.

    The trick here is to dump the code into a long context model and start asking questions. My current favorite for this is the catchily titled gemini-2.0-pro-exp-02-05, a preview of Google's Gemini 2.0 Pro which is currently free to use via their API.

    Source: Here's how I use LLMs to help me write code

  • Write the dumbest possible code

    The AI took me too literally 😅

    Screenshot: Why this is dumb: 1. No error handling - If sudo fails, the script just continues. 2. Blindly appends the line - Doesn't check if /etc/hosts is writable. 3. Relies on sudo inside system - Instead of checking permissions, it just assumes the user has sudo access and will enter a password. 4. Potential security issue - If someone modifies the script, it could be abused for other sudo commands. It gets the job done with minimal thought! 4
  • Render Html, Css, and Js from a GitHub gist

    You can use https://gistpreview.github.io/?GIST_ID to render a gist:

  • Why I don't feel threatened by AI

    My perspective on this as a developer who's been using these systems on a daily basis for a couple of years now is that I find that they enhance my value. I am so much more competent and capable as a developer because I've got these tools assisting me. I can write code in dozens of new programming languages that I never learned before.

    But I still get to benefit from my 20 years of experience.

    Take somebody off the street who's never written any code before and ask them to build an iPhone app with ChatGPT. They are going to run into so many pitfalls, because programming isn't just about can you write code - it's about thinking through the problems, understanding what's possible and what's not, understanding how to QA, what good code is, having good taste.

    There's so much depth to what we do as software engineers.

    I've said before that generative AI probably gives me like two to five times productivity boost on the part of my job that involves typing code into a laptop. But that's only 10 percent of what I do. As a software engineer, most of my time isn't actually spent with the typing of the code. It's all of those other activities.

    The AI systems help with those other activities, too. They can help me think through architectural decisions and research library options and so on. But I still have to have that agency to understand what I'm doing.

    So as a software engineer, I don't feel threatened. My most optimistic view of this is that the cost of developing software goes down because an engineer like myself can be more ambitious, can take on more things. As a result, demand for software goes up - because if you're a company that previously would never have dreamed of building a custom CRM for your industry because it would have taken 20 engineers a year before you got any results... If it now takes four engineers three months to get results, maybe you're in the market for software engineers now that you weren't before.

    Accessibility and Gen AI Ep 6 - Simon Willison - Creator, Datasette

  • Install an old version of a Brew formula

    HOMEBREW_NO_INSTALL_FROM_API=1 brew tap --force homebrew/core
    cd $(brew --repository homebrew/core)
    git log --oneline --follow Formula/g/gdb.rb
    git checkout <sha>
    brew install ./Formula/g/gdb.rb
    

    Source: https://gist.github.com/mike-myers-tob/9a6013124bad7ff074d3297db2c98247?permalink_comment_id=5407666#gistcomment-5407666

  • AI allucination

    Me: The imagemap gem does not exist. WTF? AI: You're right! The imagemap gem doesn't exist-it looks like I made an assumption there. But no
worries! You can still create image maps in Rails without a dedicated gem.

  • Human-AI Symbiosis

    • Start with your idea and use AI on top
    • Don't replace people or you lose wisdom: purpose, mission, culture
    • A chess game with AI vs AI+Human would see the latter win 10x
    • Automating with AI means you now have the time for the things you keep postponing (and not less work to do)
    • How to develop creativity? Make people work on problems that can only be solved by being creative
    • You need to know the domain to prompt properly an LLM

    Source: Lia DiBello and Neil Sahota on Human-AI Symbiosis

  • The 70% AI problem

    • AI is most useful to senior people
    • AI gets you 70% of the way, but you need to know what you are doing to complete the remaining 30%
    • Use AI to accelerate the known and/or explore possibilities

    Source: https://addyo.substack.com/p/the-70-problem-hard-truths-about

  • Better questions

    I'm not sure... what should I do? -> What would I do if I weren't afraid?, How can I make this succeed? -> What would make this certainly fail? (Charlie Munger, inversion technique), I can't decide - which path is the right path to pick? -> Which path makes for the best story?

    Source: https://x.com/ShaanVP/status/1867650344060420418

  • Rails console tricks

    hash = { a: 1, b: "string", c: [1, 2, 3] }
    
    y hash
    
    ---
    :a: 1
    :b: string
    :c:
    - 1
    - 2
    - 3
    
    class User
      def initialize
        @ivar = 1
      end
      def imet
      end
      def self.cmet
      end
    end
    
    # ---
    
    ls User
    
    User.methods: cmet
    #<Class:Object>#methods: yaml_tag
    User#methods: imet
    
    # ---
    
    ls User.new
    
    User#methods: imet
    instance variables: @ivar
    
    app.root_path
    # or any other routes
    
  • Bash script prelude

    #!/usr/bin/env bash
    
    set -euo pipefail
    IFS=$'\n\t'
    set -x
    

    Explanation:

    #!/usr/bin/env bash: interpret the subsequent lines with bash


    set -e: exit immediately with error

    Script:

    echo 1
    not-existing-command
    echo 2
    

    Out:

    1
    not-existing-command: command not found
    2
    

    Script:

    set -e
    
    echo 1
    not-existing-command
    echo 2
    

    Out:

    1
    not-existing-command: command not found
    

    set -u: exit immediately with unbound variable

    Script:

    echo 1
    echo $NOT_EXISTING_VARIABLE
    echo 2
    

    Out:

    1
    
    2
    

    Script:

    set -u
    
    echo 1
    echo $NOT_EXISTING_VARIABLE
    echo 2
    

    Out:

    1
    NOT_EXISTING_VARIABLE: unbound variable
    

    set -o pipefail: make errors fall through pipelines

    Script:

    not-existing-command | sort
    echo $?
    

    Out:

    not-existing-command: command not found
    0
    

    Script:

    set -o pipefail
    
    not-existing-command | sort
    echo $?
    

    Out:

    not-existing-command: command not found
    127
    

    IFS=$'\n\t': Set input field separator (default: $' \n\t')

    Script:

    string="1 2 3"
    
    for i in $string; do
      echo "$i"
    done
    
    IFS=$'\n\t'
    for i in $string; do
      echo "$i"
    done
    

    Out:

    1
    2
    3
    1 2 3
    

    Script:

    array=(
    "a b"
    "c d"
    )
    
    for x in ${array[@]}; do
      echo "$x"
    done
    
    IFS=$'\n\t'
    for x in ${array[@]}; do
      echo "$x"
    done
    

    Out:

    a
    b
    c
    d
    a b
    c d
    

    set -x: print lines as they are executed

    Script:

    set -x
    
    echo 0
    echo 1
    

    Out:

    + echo 0
    0
    + echo 1
    1
    
  • Delete open Nvim buffers

    List all open buffers:

    :ls
    

    Close all open buffers:

    %bd!|e#|bd#
    
    • %bd! foreach buffer delete
    • e# open the last buffer for editing
    • bd# delete the [No Name] buffer

    Source: https://stackoverflow.com/questions/4545275/vim-close-all-buffers-but-this-one

  • Global find/replace in Nvim

    Fill the quickfix list with something like vim-grepper and:

    cdo %s/BEFORE/AFTER/gc
    

    cdo executes what comes after for each entry in the quickfix list (or use cfdo to run once per file).

  • AI bias

    Comment in a PR; "I use do end but I've missed that here due to Cursor Al generating this code"

    I shared in Why Good Solutions Block Better Ones why the first idea that comes to your mind may make you ignore other ones.

    And here we are!

  • form_with form object

    The form_with helper accepts an ActiveModel::Model:

    <%= form_with model: TeamMemberForm.new do |f| %>
      <%= f.text_field :full_name %><br />
      <%= f.email_field :email %><br />
      <%= f.submit %>
    <% end %>
    

    You can create a form object like the following:

    class TeamMemberForm
      validates ...
    
      def self.model_name
        ActiveModel::Name.new(self, nil, 'TeamMember')
      end
    end
    

    By using ActiveModel::Name, Rails will take care of filling names and ids as follows:

    <input type="text" name="team_member[full_name]" id="team_member_full_name">
    <input type="email" name="team_member[email]" id="team_member_email">
    

    So in the controller you can:

    params.require(:team_member).permit(:full_name, :email)
    
  • Dialog with Rails & Hotwire

    Clicking on Invite User opens a dialog that contains a form and an X button in the top-right corner to dismiss it

    At the end of <body>:

    <dialog data-controller="modal-dialog" class="tw-w-[calc(100%-0.5rem)] tw-max-w-xl tw-rounded-lg">
      <div class="tw-p-8">
        <button
          data-action="click->modal-dialog#close"
          class="tw-absolute tw-top-6 tw-right-8 tw-text-5xl tw-opacity-40 hover:tw-opacity-90 tw-select-none"
        >&times;</button>
    
        <%= turbo_frame_tag 'modal_dialog', data: { action: 'turbo:frame-load->modal-dialog#open' } %>
      </div>
    </dialog>
    

    Stimulus controller:

    import { Controller } from "@hotwired/stimulus";
    
    export default class extends Controller {
      connect() {
        this.element.addEventListener("close", () => {
          this.element.classList.remove('open')
        });
      }
    
      disconnect() {
        this.close();
      }
    
      open() {
        this.element.showModal();
        this.element.classList.add('open')
      }
    
      close() {
        this.element.close();
        this.element.classList.remove('open')
      }
    }
    

    CSS:

    // This scrolls the page to the top, which is a problem if the
    // user opens the dialog not at the top of the page.
    //
    // The following would work but causes an horizontal layout
    // shift when the scrollbar disappears:
    //   .no-scroll {
    //     overflow: hidden;
    //   }
    //
    // Keep an eye on the following thread for better solutions:
    // https://github.com/whatwg/html/issues/7732
    body:has(dialog[open]) {
      overflow-y: scroll;
      position: fixed;
      width: 100%;
    }
    
    dialog {
      opacity: 0;
      transition: opacity 0.3s ease-out;
      box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.8);
    }
    
    dialog.open {
      opacity: 1;
    }
    
    dialog::backdrop {
      display: none;
    }
    

    Button that opens the dialog:

    <%= link_to(
      fa_icon('plus', text: 'Invite user'),
      new_dashboard_organization_team_member_path(@organization),
      class: '!tw-py-2 !tw-px-3 !tw-text-base btn btn-default',
      data: { turbo: true, turbo_frame: 'modal_dialog' }
    ) %>
    

    Form that gets rendered into the dialog:

    <div class="container tw-py-20">
      <div class="tw-max-w-xl">
        <p class="text-muted">
          <span data-toggle="tooltip" title="Organization name"><%= @organization.name %></span> / <%= link_to 'Team members', dashboard_organization_team_members_path(@organization) %>
        </p>
    
        <%= turbo_frame_tag 'modal_dialog' do %>
          <h1 class="tw-m-0 tw-mb-6">Invite team member</h1>
    
          <%= render partial: 'form', locals: {
            team_member_form: @team_member_form,
            form_method: :post,
              action_url: dashboard_organization_team_members_path,
              submit_value: 'Invite'
            } %>
        <% end %>
      </div>
    </div>
    

    Controller:

    def create
      team_member_form = TeamMemberForm::NewForm.new(new_team_member_params.merge(organization: organization, inviter_email: current_user.email))
    
      if team_member_form.save
        flash[:success] = 'The user has been added to the team members.'
    
        respond_to do |format|
          format.turbo_stream do
            render turbo_stream: turbo_stream.action(:redirect, dashboard_organization_team_members_path(organization))
          end
          format.html do
            redirect_to dashboard_organization_team_members_path(organization)
          end
        end
      else
        @team_member_form = team_member_form
        render :new
      end
    end
    

    Redirect:

    // Workaround for redirecting from within a Turbo Frame:
    // https://github.com/hotwired/turbo-rails/pull/367
    Turbo.StreamActions.redirect = function () {
      Turbo.visit(this.target);
    };
    
  • endoflife.date

    End-of-life (EOL) and support information is often hard to track, or very badly presented. endoflife.date documents EOL dates and support lifecycles for various products.

    Example:

    Need to check what's the latest version of Ruby? Or until when it's supported?

    endoflife.date/ruby

  • stdout vs stderr

    Use stdout for the output of your program: the log in git log, the matching files with find, or the list from ls.

    Use stderr for errors AND ANYTHING ELSE: the progress messages, the warns/info, and the jokes (if you really need to).

    For example, given the following output (on stdout):

    I, [2024-12-10T18:59:45.134882 #7388]  INFO -- : Crystalball starts to glow...
    W, [2024-12-10T18:59:45.138463 #7388]  WARN -- : Maps are outdated!
    I, [2024-12-10T18:59:45.311953 #7388]  INFO -- : Starting RSpec.
    {"version":"3.13.2","messages":["No examples found."],"seed":41594,"examples":[],"summary":{"duration":6e-05,"example_count":0,"failure_count":0,"pending_count":0,"errors_outside_of_examples_count":0},"summary_line":"0 examples, 0 failures"}
    

    I had to remove manually the first lines to parse the JSON:

    command | tail -n1 | jq
    
  • Ruby: default gems

    Ruby's stdlib is composed of 3 different elements:

    • Standard libraries that are embedded in the language's source
    • Default gems that can be required, are maintained by the core team, and cannot be removed
    • Bundled gems that come installed with Ruby and can be removed

    Full list on stdgems.org.

  • Ruby gem activation

    This is how you use a Ruby gem usually:

    require 'foo'
    Foo.bar
    

    For the require to work, the gem needs to be available on the LOAD_PATH:

    ruby -e 'puts $LOAD_PATH'
    

    The act of adding a gem to the LOAD_PATH is called "activating a gem".

    In a vanilla Ruby situation, you can add a directory to the LOAD_PATH with:

    ruby -I ./foo/lib -e "puts(require 'foo')"
    # true
    

    More often, you deal with a bundle that contains a Gemfile:

    gem 'foo'
    

    To activate the gems from the Gemfile (taking into account the versions from Gemfile.lock):

    require 'bundler/setup'
    require 'foo'
    

    or (equivalent):

    require 'bundler'
    Bundler.setup
    require 'foo'
    

    Notice that the setup step will first clear the LOAD_PATH and activate only the gems in the bundle.

    For example, Rails does it here and here.

    If you've ever wondered what the hell is up with bundle exec ..., the answer is simple: Bundler activates the gems in the bundle with something like

    RUBYOPT="-rbundler/setup" ruby ...
    

    Of course, things are more complex than that. But this should give you a starting point.

    Bundler: How to use Bundler with Ruby

  • Hi, thanks, bye

    When travelling, strive to learn how to say three words: hi, thanks, bye.

    You'll be surprised of what happens when you stop being "just another tourist."

  • Convert RDA to CSV with a script

    Throw the following into rda2csv.r:

    #!/usr/bin/env Rscript
    
    argv <- commandArgs(TRUE)
    inFile <- toString(argv[1])
    print(paste("Reading:", inFile))
    
    outFile <- gsub(".rda$", ".csv", inFile)
    print(paste("Writing:", outFile))
    
    inData <- get(load(inFile))
    write.csv(inData, file=outFile)
    

    Make it executable: chmod +x rda2csv.r

    And run it on an RDA file: ./rda2csv.r path/to/file.rda

  • Risking millions on coin flips

    Using the Binomial Distribution to tilt the odds in your favor.

    I found this pearl in Naked Statistics (chapter 5):

    In 1981, the Joseph Schlitz Brewing Company spent $1.7 million for what appeared to be a shockingly bold and risky marketing campaign for its flagging brand, Schlitz. At halftime of the Super Bowl, in front of 100 million people around the world, the company broadcast a live taste test pitting Schlitz Beer against a key competitor, Michelob. Bolder yet, the company did not pick random beer drinkers to evaluate the two beers; it picked 100 Michelob drinkers.

    It turns out, Schlitz's marketing department was not a bunch of idiots. And here's why.

    If you toss a coin twice you can get either one of the following:

    1. heads, heads
    2. tails, tails
    3. heads, tails
    4. tails, heads

    Therefore, the probability of getting one tails out of two flips is 2/4 (ie, case 3. and 4.).

    That is described by the Binomial Distribution where n is the number of trials (coins) and k is the number of successes (tails):

    p = n! / [k! * (n - k)!] * (1/2)^n
        2! / [1! * (2 - 1)!] * (1/2)^2
        2  / 1               *  1/4
        1/2 (same as 2/4 from above)
    

    The statisticians at Schlitz considered that:

    • commercial beers taste the same
    • in a blind tasting, a Michelob drinker would say that Schlitz tastes better 50% of the times (it's a coin flip)
    • getting 40% or more of Michelob drinkers to say Schlitz is better is worth $1.7 million

    But what's the probability?

    If you take 10 Michelob drinkers, you need at least 4 successes. In other words, you want to sum the probability of 4, 5, 6, 7, 8, 9, and 10 tails with coin flips:

    p = 10! / [4! * (10 - 4)!] * (1/2)^10 +
        10! / [5! * (10 - 5)!] * (1/2)^10 +
        ...
        10! / [10! * (10 - 10)!] * (1/2)^10
      = 0,82
    

    So there's a 82% chance of succeeding.

    And if you raise the number of drinkers to 100, you get to a shocking 98%!

  • The "opposite" test

    When "positioning" something, if the opposite of what you claim is bullshit, what you claim is bullshit.

    If you've ever heard any of the following statements:

    • Our company is customer-first
    • Our product is fast & easy-to-use

    Ask yourself:

    • Would any company pick customer-last as a strategy?
    • Would any product go for slow & hard-to-use?

    Nope. Because that's bullshit!

    Inspiration: https://longform.asmartbear.com/opposite-test/

  • How big things get done

    How big things get done published the following breakdown:

    • 100% of the mega-projects analyzed
    • 47.9% finished on budget
    • 8.5% finished on bugdet and time
    • 0.5% finished on budget, time, and produced the expected benefits

    Later, the book analyzes what are the prerequisites to get into the 0.5% Olympus:

    • Know why all the time: is this code rewrite benefitting the product?
    • Think slow, act fast:
      • Make mistakes in the planning phase when screw-ups are cheap
      • The more time the execution phase takes, the more you expose yourself to black swans (ie, catastrophic and unexpected events)
    • Mitigate risks:
      • Spike code to understand if and how something can be done
      • Ask people who worked on similar features what went wrong/could have been done better
    • Estimate using anchors:
      • Hofstadter’s Law: “It always takes longer than you expect, even when you take into account Hofstadter’s Law.”
      • We always estimate with the happy case in mind and, even the most paranoid, cannot prepare for unknown unknowns
      • Ask others what was their experience building a similar thing and use their numbers as a starting point
    • Lean on experience:
      • Involve people who've worked on similar features in the past
      • Learn from other similar projects—and no, you are not working on anything so unique that cannot draw from experience
      • Use boring tech: "remove the words custom and bespoke from your vocabulary"
    • Build a team
    • Iterate:
      • "we're terrible at getting things right the first time"
      • "I have not failed ten thousand times," Thomas Edison said. "I’ve successfully found ten thousand ways that will not work."
      • "If something works, you keep it in the plan. If it doesn’t, you “fail fast,"
      • When talking about the success of the Empire State Building, "workers didn’t build one 102-story building, they built 102 one-story buildings."

    The following sentence made me stop and think about my fuck-ups:

    When delivery fails, efforts to figure out why tend to focus exclusively on delivery. That’s understandable, but it’s a mistake, because the root cause of why delivery fails often lies outside delivery, in forecasting, years before delivery was even begun.

  • Learning requires sweating

    Learning is not supposed to be fun. It doesn't have to be actively not fun either, but the primary feeling should be that of effort.

    If you are not sweating, it's entertainment, not learning.

    Source: Andrej Karpathy on X

  • Horizontally scroll to center an element

    <div class="width-full overflow-x-auto">
      <div>...</div>
      <div>element</div>
      <div>...</div>
    </div>
    
    const viewportWidth =
      Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
    
    container.scrollLeft =
      element.offsetLeft // scroll to the left edge of element
      + element.offsetWidth / 2 // scroll to center of element
      - viewportWidth / 2 // scroll back half the viewport
    
  • Comment your regular expressions

    # ❌
    usa_postal_code_pattern = /\A\d{5}(-\d{4})?\z/
    
    # ✅
    usa_postal_code_pattern = %r{
      \A # Beginning of string
      \d{5} # 5 digits
      ( # ZIP+4
        - # Hyphen
        \d{4} # 4 digits
      )? # ZIP+4 is optional
      \z # End of string
    }x
    

    Source: Comment your regular expressions

  • Dreamweaving unforgettable experiences

    Will Guidara hires Dreamweavers in his restaurant.

    Their job is to pull legends: creating, automating, and scaling unforgettable experiences.

    Source: The Hospitality Solution in Your Business

  • The 95/5 rule

    Balancing out what's best for the user vs what's best for the business

    Every decision I made seemed to expose the natural tensions between improving the quality of the experience the guests were having and doing what was best for the business. Restaurant-smart meant leading with trust—including allowing the people who worked for me to do what they felt was best for the guests. Corporate-smart meant running a tight ship. Which was right?

    Will Guidara's answers his own question:

    Manage 95% of your financials down to the penny while spending the remaining 5% “foolishly”, and “splurging” the 5% on the guest experience to create an unforgettable experience.

    Source: Will Guidara: Restaurant Smart vs Corporate Smart

  • Find unused indexes in Postgres

    How to use the `pg_stat_user_indexes` table

    SELECT * FROM pg_stat_user_indexes WHERE relname = 'TABLE_NAME':

    • idx_scan: how many times the index was used
    • idx_tup_read: how many index entries were returned using the index
    • idx_tup_fetch: how many rows were returned using the index

    You want:

    • idx_scan to move up (ie, the index is used)
    • idx_tup_read/idx_tup_fetch to move up but not too much (ie, the index is highly selective)

    Source: sgerogia.github.io/Postgres-Index-And-Queries/

  • How to find content ideas

    A cheap trick for unlimited inspiration

    1. Find a relevant subreddit
    2. Google: "how to" site:reddit.com/r/SUBREDDIT/

    Here are the top results for "how to" site:https://www.reddit.com/r/javascript/:

    • "How to actually Learn from the Documentation?"
    • "How do I practice JavaScript?"
    • "How did you learn Javascript?"
    • "How to become a more well rounded dev"
    • "How to develop android apps with javascript?"
  • Html `<template>`

    Hidden and re-usable pieces of Html

    In some situations, you may want to render some hidden Html that can be later accessed by JavaScript to use as a template.

    For example, a form to create a survey with a title and multiple questions:

    <form>
      <input type="text" name="title" placeholder="Enter title" />
      <button type="button" onclick="addQuestion()">Add question</button>
      <p>Question</p>
      <input type="text" name="question" placeholder="Enter question" />
    </form>
    
    <template>
      <p>Question</p>
      <input type="text" name="question" placeholder="Enter question" />
    </template>
    
    <script>
      function addQuestion() {
        const template = document.querySelectorAll("template")[0].content;
        const clone = template.cloneNode(true);
        document.querySelectorAll("form")[0].appendChild(clone);
      }
    </script>
    

    What I used in the past is less elegant:

    <form>
      <input type="text" name="title" placeholder="Enter title" />
      <button type="button" onclick="addQuestion()">Add question</button>
      <p>Question</p>
      <input type="text" name="question" placeholder="Enter question" />
    </form>
    
    <div class="template" style="display: none;">
      <p>Question</p>
      <input type="text" name="question" placeholder="Enter question" />
    </div>
    
    <script>
      function addQuestion() {
        const template = document.querySelectorAll(".template")[0];
        const clone = template.cloneNode(true);
        clone.style.display = "block";
        document.querySelectorAll("form")[0].appendChild(clone);
      }
    </script>
    
  • Logarithm laws

    A quick recap of some fundamental properties of logarithms

    Power law

    logbpn = n * logbp

    Proof:

    logbp = a -> ba = p

    Raise to k -> bk*a = pk

    Apply logb -> k * a = logbpk

    Replace a with logbp -> k * logbp = logbpk

    Product law

    logbm*n = logbm + logbn

    Proof:

    logbm = i -> bi = m & logbn = j -> bj = n

    Multiply -> bi * bj = m * n

    Simplify -> bi+j = m * n

    Apply logb -> i + j = logbm*n

    Replace i and j -> logbm + logbn = logbm*n

    Quotient law

    logbm/n = logbm - logbn

    Proof:

    logbm = i -> bi = m & logbn = j -> bj = n

    Divide -> bi / bj = m / n

    Simplify -> bi-j = m / n

    Apply logb -> i - j = logbm/n

    Replace i and j -> logbm - logbn = logbm/n

  • Video to Gif oneliner

    Using `ffmpeg` to transform an .mp4 into a .gif

    This is how I created the demos for 3v0k4/exit.nvim:

    • Recorded the screen with QuickTime into an .mp4
    • Transformed into a .gif with ffmpeg
    ffmpeg \
      -i input.mp4 \
      -vf "fps=10,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
      -loop 0 \
      output.gif
    

    Source: SuperUser

PinkLetter

It's one of the selected few I follow every week – Mateusz

Tired of RELEARNING webdev stuff?

  • A 100+ page book with the best links I curated over the years
  • An email once a week full of timeless software wisdom
  • Your recommended weekly dose of pink
  • Try before you buy? Check the archives.