Japan Recommendations (updated)

Everything here is somewhere I’ve actually been, across five trips (2017, 2023, 2024, spring 2025 and a month in late 2025). Starred items (★) are the ones I’d build a day around. Entries without a note are places I went but didn’t write anything about.

Tokyo

Fine dining and special meals

  • ★ L’Effervescence (Nishi-Azabu). Went twice. Every dish is incredible, the service is impeccable, and the interior is beautiful. Book well ahead.
  • ★ Métis (Roppongi). Chef cooks over fire in front of you, and you can talk with him while he cooks. Even the mocktail pairing was incredible. My favourite dining experience in Tokyo.
  • ★ Hommage (Asakusa). Incredible: beautiful presentation, a great pairing and such good food.
  • ★ VERT (Kagurazaka). A tea-and-dessert tasting course tucked in a winding alley. The lighting and atmosphere are incredible, and the neighbourhood is worth wandering before or after.
  • ★ Sakurai Japanese Tea Experience (Minami-Aoyama). A tea tasting that goes much deeper on the tea itself than VERT, including fermented teas. Do both if you like tea.
  • CYCLE (Otemachi). Went twice.
  • JO (Nishi-Azabu).
  • Tapas Molecular Bar (Mandarin Oriental, Nihonbashi).
  • Pizza Bar on 38th (Mandarin Oriental, Nihonbashi). Amazing pizza with a view.
  • Sushi Tokyo Ten (NEWoMan Shinjuku). A lunch omakase you can book the day before on TableCheck.
  • Aman Tokyo (Otemachi). Lunch.

Casual food

  • ★ Fuunji (Shinjuku). Tsukemen (dipping ramen). There’s a line, and it’s totally worth it. The sauce is incredible.
  • ★ Abe chan (Azabu-Juban). Local izakaya with the best chicken skewers I’ve ever had. Try the veggie sides, skip the beef skirt, and note that some combo options are only on the Japanese menu.
  • ★ Imakatsu (Roppongi). The best chicken katsu of my life. I waited an hour, so go early.
  • Ramen Kamo to Negi (Ueno). Duck ramen with a simple broth and incredible yuzu kosho.
  • Misa Ramen (Kanda). Spicy and really good; always a line.
  • Nippon Curry (Kanda/Akihabara). Karaage or hamburg curry. Really good and cheap.
  • Savoy (Azabu-Juban). The best pizza crust I’ve ever had, though it’s more of a crust experience than a cheese-and-sauce one.
  • Chinese Cafe Eight (Akasaka). Great atmosphere. Some dishes are excellent (wide noodles, pea shoots, spare ribs), others are ordinary. People smoke in the stairwell.
  • Oniku Karyu (Ginza). Meat.
  • Butagumi (Nishi-Azabu). Tonkatsu in an old house; you pick the breed of pork.
  • Ichifuji (Ginza). Counter restaurant; went twice.
  • Bistro Simba (Ginza). French bistro.
  • Japanese Spice Curry Wacca (Hatchobori).
  • Erick South (Yaesu, by Tokyo Station). South Indian curry; good lunch at Tokyo Station.
  • Mensho Taketora (Roppongi). Ramen.
  • Ramen Daisenso (Kanda).
  • Gyukatsu Motomura (Akihabara). Beef cutlet.
  • Shake Tree Diner (Ryogoku). Burgers.
  • Ichiran (Shinbashi). Ramen in solo booths.
  • Ippudo (Ueno). Ramen.
  • Tsukiji Outer Market for sushi and street food.
  • Renga-Tei (Ginza). Not worth the wait or the cost.

Drinks

  • Nonbei Yokocho (Shibuya). A tiny alley of bars.

Cafés and sweets

  • ★ LIT Coffee & Tea Stand (Shiba). A fantastic matcha latte and excellent matcha pudding, on a charming side street.
  • Motosue egg bread (Azabu-Juban). Fluffy and creamy, like a better Japanese cheesecake.
  • Le Petit Mec (Hibiya). The best pastries I had in Tokyo.
  • Parklet (Nihonbashi). Cute café with great matcha.
  • Blue Bottle. The matcha latte is surprisingly good.
  • Boulange (Shinjuku). Great baked goods.
  • Pâtisserie Ryoco (Takanawa). Pre-order on TableCheck, which opens a week ahead.
  • Cokuun. A coffee omakase.
  • Miyanoyu (Nezu). Matcha and espresso in a converted bathhouse.
  • Design Pan (Bunkyo). Bakery.
  • Hatoya (Asakusa). Matcha stand.
  • Asakusa Taiyaki Guraku / Taiyaki Kobo (Asakusa).
  • The Revival House (Asakusa). Small coffee spot near Kaminarimon.
  • I’m Donut? (Omotesando).
  • Atelier Matcha (Ginza).
  • Ginza Manneken. Belgian waffles.
  • Pie Mania.
  • Uniqlo Coffee (Ginza).
  • The Matcha Tokyo (Shibuya/Shinjuku). Overrated and pricey.

See and do

  • ★ Imperial Palace loop. The classic running route in the city.
  • ★ Yoyogi Park and the Koishikawa Botanical Garden in autumn: amazing fall colours.
  • Jingu Gaien ginkgo avenue (Aoyama). Super pretty in late November, and insanely busy.
  • Shiba Park. The southwest corner has a patch of ginkgo trees with a view of Tokyo Tower.
  • Zojo-ji. Nice view of Tokyo Tower, but there are better temples.
  • Tokyo International Forum at night for reflection photos, then Akihabara for neon.
  • Kagurazaka. Wander the back alleys, and stop at Akagi Shrine.
  • Tokyo Metropolitan Government Building. A free observation deck.
  • Azabudai Hills. Observation floor (pricey ticket) and the Christmas market in season.
  • Ueno Park. Come at golden hour.
  • Shinjuku Gyoen. Confusing to navigate at night, so go by day.
  • Hama-rikyu Gardens.
  • Asakusa and Senso-ji.
  • Tokyo Skytree.
  • Shibuya Crossing.
  • Toyosu fish market. Be there by about 4:30 AM for the tuna auction.
  • teamLab Planets (Toyosu) and teamLab Borderless (Azabudai Hills). Buy tickets ahead.
  • Museums: the Tokyo National Museum (Ueno), the Nezu Museum and its garden (Aoyama), and Small Worlds (a miniature-world museum in Ariake).
  • Tokyo DisneySea. The Teddy Roosevelt Lounge was one of my highlights. I also ate at Ristorante di Canaletto and the S.S. Columbia Dining Room.
  • Tokyo Disneyland. Skip the Crystal Palace buffet.

Workshops

  • ★ Glass carving workshop at the Little Museum of Glass Art Books (near Asakusa). I carved a Mt Fuji into a glass. Cheap and fun.
  • Chopstick-making workshop (Kuramae). Pairs well with the glass carving the same afternoon.

Shopping

  • ★ Kappabashi Kitchen Street. My favourites were Nami, Baise, Hashitou (chopsticks) and Minochiya (great prices on glasses). Dr.Goods is nicely presented but about 50% pricier than equivalent stores.
  • ★ Standard Products (Ginza). Daiso’s upscale line, and an amazing store.
  • Itoya (Ginza) for stationery.
  • OK Supermarket (Ginza). My favourite discount grocery store. Get the semi-dried persimmons if they’re in stock.
  • Daimaru basement food hall at Tokyo Station. Grab a bento before any bullet train. Tokyo Gift Palette at the station is good for snack gifts.
  • Found MUJI (Aoyama) and Wood Like Company (Aoyama) for beautiful furniture and wood goods.
  • Mokuhankan (Asakusa) for woodblock prints.
  • Department stores and malls: Takashimaya Nihonbashi, Mitsukoshi Ginza, GINZA SIX, Tokyu Plaza Ginza, COREDO Muromachi, Keio Shinjuku, Lumine Est Shinjuku, and the ceramics floors.
  • Electronics and gear: Bic Camera, Yodobashi Akiba, the Sony store in Ginza, Montbell, Hands Ginza.
  • The ones I hit every trip: MUJI, Uniqlo, Don Quijote, Hibiya Chanter.

Day trips from Tokyo

  • Kamakura.
  • Meigetsu-in is the highlight: a beautiful walk from the station along the river, with a famous round window.
  • The Great Buddha (Kotoku-in) is cool, but the other temples are nicer if you’re short on time.
  • Koshomochi for sweets.
  • Lake Kawaguchiko (Mt Fuji). Stay a night at Ubuya, a ryokan with incredible lake and Fuji views.
  • Yokohama. The Kirin Brewery factory tour (book ahead), plus the Costco there for the novelty.
  • Nikko for autumn colours.
  • The Tamozawa Imperial Villa.
  • The Kirifuri Falls viewpoint.
  • Lunch at Manten Chicken Grill.

Kyoto

See and do

  • ★ Arashiyama Bamboo Grove plus the park viewpoint. Go at dawn, because it’s packed by 7:30. The real highlight is the park on the left after you exit the grove: climb to the observation point, then follow the ridge path for a view over the city. Probably my favourite thing I did in Japan.
  • ★ Fushimi Inari. Go early and hike to the peak, since the crowds thin as you climb. It’s also great at night.
  • ★ Kifune Shrine in the morning. Mist rises off the mountains and there are far fewer people. Skip the night-illumination train, which was packed.
  • ★ Daigo-ji night illuminations in autumn (and Sanbo-in by day). Beautiful, but it’s out of the way, so plan your ride back.
  • Kiyomizu-dera. Go at 6 AM or for the night light-up. It’s spectacular and very busy (7.5/10).
  • Rurikoin (spring and autumn). The autumn viewing is by lottery and it’s cash only. It’s beautiful but still crowded, so look for angles beyond the famous shot.
  • Kyoto Railway Museum. Awesome; you can sit inside a bullet-train car.
  • Tenryu-ji and Otagi Nenbutsu-ji (Arashiyama). Otagi has quirky stone statues.
  • Gion: Hanami-koji, Ninenzaka, and a dawn bike ride before anyone’s up.
  • Kamogawa River for walks and runs.
  • Kyoto University Museum.
  • Nishiki Market. Overpriced but fun. It gets better value toward the far end, where there’s a good pottery shop.
  • Kinkaku-ji (Golden Pavilion). Midday is a crowded one-way shuffle; 6/10.

Workshops and experiences

  • ★ Ringram. Make your own silver rings in about two hours. You do almost all of it yourself, and they engrave English letters for free. Highly recommend.
  • Wagashi Issho. A small Japanese-sweets class. The instructor was lovely and the sweets are beautiful, though they’re more about looks than taste.
  • Origami and Japanese gift-wrapping class.
  • Matsui Sake Brewery tour and tasting.

Food

  • ★ LURRA° (Higashiyama). A vegetable-forward tasting menu cooked over wood fire. The non-alcoholic pairing was the best I’ve ever had. Great if you like experimental food.
  • Gion Kappa. A local counter izakaya with cheap small plates.
  • Mizuki at the Ritz-Carlton Kyoto. Kaiseki for a special occasion.
  • Tempura Kawatatsu.
  • Hyatt Regency Kyoto breakfast buffet. Totally worth it even if you’re not staying there.
  • Yakiniku Hiro. You grill the wagyu yourself.
  • Nikuya Suzu (Shichijo). Wagyu yakiniku and cheap wagyu lunch bowls.
  • Senmonten (Gion). Bite-size gyoza.
  • Inoichi. Ramen.
  • Namaste Dhaulagiri (Higashiyama). Nepalese.
  • Arashiyama beef sandwiches. Pretty good but very overpriced.

Cafés and sweets

  • Ippodo Tea Kaboku tearoom (Teramachi).
  • Shinshindo (Teramachi). A French-style bakery café from 1913.
  • Ace Hotel Kyoto lobby café.
  • Shizuya. Bakery.
  • Kyoto Station sweets: Malebranche (matcha langues de chat) and Kyoto Kashi Koji.

Shopping

  • Standard Products. Same as the Tokyo one; I came out with seven bags.
  • Kyukyodo (Teramachi). A stationery store about 400 years old.
  • Good Nature Station (Kawaramachi). A food hall and market; I went back on a second trip.

Osaka and around

  • ★ Wada knife-sharpening workshop (Sakai). Learn from a master craftsman and tour the knife-history museum. It’s a trek from Kyoto and pricey, but worth it. Buy stainless steel to avoid rust spots.
  • ★ Himeji Castle. An awesome castle and grounds, 30–40 minutes by bullet train from Shin-Osaka. Better inside than Matsumoto. Have matcha at the Sojuan tea house in Koko-en garden next door. Cafe Santamaria Nagomi is nearby for coffee.
  • Nara. An easy half day from Osaka or Kyoto: Kasuga Taisha shrine and a few hours on foot around Nara Park and the deer.
  • Osaka food:
  • Wagyu Idaten (Namba) for a charcoal-seared A5 wagyu box.
  • Yakiniku Shokudo Takumi.
  • Wareware for breakfast.
  • WILLcha bubble tea.
  • Harukas 300 observation deck at sunset.
  • Dotonbori and the Ebisubashi-suji arcade in the evening.
  • Hankyu Umeda and LUCUA for shopping.

Japan Alps and countryside

Karuizawa

  • ★ A walkable, slightly bougie mountain town with great fall colours.
  • Kumoba Pond for reflections, especially on a dawn run.
  • Hiroshi Senju Museum. The mix of architecture and art is unlike anything I’ve seen.
  • Wild Bird Sanctuary trail (Hoshino).
  • ★ Tombo-no-yu onsen, with an outdoor bath in cool mountain air. My classic onsen moment.
  • Restaurant Suju. Shinshu cooking; I really loved it.
  • Tanari for a rose-petal matcha latte, my favourite of the trip.
  • Apple Pie Lab, Sawamura bakery and Workshop café.
  • Coriss. A Japan-made lifestyle shop in a former church.
  • Shiraito Falls. Small, and not worth the trip out from town.

Matsumoto and Azumino

  • ★ Matsumoto Castle at sunrise. It’s more beautiful from outside than Himeji. Peek through the holes inside for city views.
  • Grape farm outside town, by loaner bike at dawn. Awesome grapes.
  • Daio Wasabi Farm (Azumino). Free to enter, with beautiful scenery. Trains are infrequent, so plan ahead. Skip the wasabi ice cream.

Takayama and Shirakawa-go

  • ★ Kitchen Hida. Hida beef; get the side sausages.
  • ★ Hida Kokubun-ji. The giant 1,250-year-old ginkgo there was the most beautiful one I saw.
  • Sanmachi old street and sake tasting at Harada Sake Brewery.
  • Miyagawa morning market. Cool to see.
  • June Coffee.
  • Hida Takayama Retro Museum. In and out in six minutes; skip it.
  • ★ Shirakawa-go. Definitely worth the trip. The bus runs hourly, so plan around golden hour. The set-meal place with special beef was worth the 40-minute wait.
  • Solo Pizza Napoletana (Nagoya Station), if you route back through Nagoya. Very good.

Nagano, Shibu Onsen and Bessho Onsen

  • ★ Jigokudani snow monkeys. One of my favourite experiences in Japan, with 100+ monkeys up close. It’s a decent hike from the bus stop.
  • ★ Shibu Onsen. Stay overnight and do the public-bath crawl around town. Shinmeidaki-no-yu (No. 8) was one of my favourites. Koishiya Ryokan’s café is a nice space, but the food is ordinary.
  • Nagano city.
  • Nagano-ken Choju-shokudo: an incredible meal, and everything on the menu was great.
  • Zenko-ji, and Yawataya Isogoro next to it for shichimi spice.
  • Togakushi Shrine. Beautiful in the mountains and worth it if you’re in the area, but not a trip on its own.
  • Bessho Onsen. Uematsuya is a friendly ryokan with an outdoor bath and a great breakfast.

Seto Inland Sea and Wakayama (cycling)

  • ★ Shimanami Kaido, the island-hopping bike route from Onomichi to Shikoku.
  • Shimanami Dolce gelato in Setoda.
  • BetoLabo for banh mi.
  • Onomichi. Aozora Papad for curry, Little Mermaid bakery and Fruits Lab for fresh juice.
  • Wakayama coast bikepacking (Shirahama to Yuasa) for citrus farms and ocean views. Yuasa is the birthplace of soy sauce.

Where I’ve stayed

  • Tokyo:
  • Andaz Tokyo, my favourite (great lobby and evening drinks).
  • Grand Hyatt Tokyo.
  • Comfort Hotel Kanda: a tiny room, but a great location and cheap.
  • Comfort Inn Roppongi.
  • APA Higashi-Nihonbashi.
  • Moxy Kinshicho.
  • Akihabara Washington.
  • An Airbnb in Tatekawa (Sumida).
  • Tokyo Hostel Fuji (Nakano), on my first trip.
  • Kyoto:
  • Hotel Mume, a small boutique hotel in Gion with super helpful staff.
  • The Ritz-Carlton Kyoto.
  • Hyatt Regency Kyoto.
  • Hyatt Place Kyoto.
  • A traditional-house Airbnb in northern Kyoto (see the tea ceremony above).
  • Osaka:
  • Moxy Osaka Honmachi.
  • Karaksa Hotel Grande Shin-Osaka.
  • Hearton Nishi-Umeda.
  • Karuizawa: Kyukaruizawa Kikyo.
  • Matsumoto: Hotel M, a capsule hostel. Avoid it if you value sleep (the free loaner bike was the one plus).
  • Takayama: hotel around TAKAYAMA (Ascend Collection).
  • Fuji: Ubuya.
  • Nikko: Hotel Harumoto.
  • Shibu Onsen: Kaneki Hotel.
  • Nagano: Hotel Metropolitan Nagano, right by the station.
  • Bessho Onsen: Uematsuya.
  • Wakayama: an Airbnb farm stay in Inami, midway through the coast ride.
  • Onomichi: an Airbnb, a good base for the Shimanami Kaido.

Practical tips

  • Booking:
  • Most restaurant reservations go through TableCheck, which works in English and sends a reconfirmation link a week ahead. Click it.
  • Workshops and day passes are on Klook.
  • Email your hotel concierge before you arrive. It paid off every time.
  • Bullet trains: use the SmartEX app for Tokyo–Kyoto and Eki-net for Tokyo–Karuizawa/Nagano. I bought point-to-point tickets rather than a JR Pass.
  • Crowds: go to popular temples at 6–7 AM. Night illuminations are worth it.
  • Getting around:
  • Get a Suica card and use Uber for taxis.
  • LUUP e-scooters and e-bikes are everywhere in Tokyo and Kyoto.
  • Rural trains and buses can be hourly or worse, so plan around them.
  • Cash: carry some for temples and small places. 7-Eleven ATMs take foreign cards.
  • Luggage: ship it ahead between hotels (takkyubin) and travel light.

Allow Claude Code Access to Claude Design

This wasn’t obvious to me (you can’t do it via a conversation in Claude Code CLI for example).

You need to go to https://claude.ai/design, click on your account icon in the top right, and flip the toggle to allow access between Claude Code and Claude Design, there’s no other way to do it that I could find (including in primary settings at claude.ai).

Chime on Replit Completion

// ==UserScript==
// @name         Replit Chat Completion Ping (Stop/Working FSM + Airy Harp Chime)
// @namespace    nicholas.tools
// @version      1.4.0
// @description  Chimes when Replit chat finishes. Detects streaming via Stop or "Working..". Uses Sound #4 (Airy harp up-gliss). Rich console logs + control panel (window.ReplitPing).
// @match        https://replit.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(() => {
  "use strict";

  /* =========================
   * Config
   * ========================= */
  let DEBUG = true;          // high-level logs (state changes, detections)
  let TRACE_SCAN = false;    // very chatty: log detector scans (toggle at runtime)
  const STABILITY_MS_DEFAULT = 200; // require this much time of "no Stop & no Working" before DONE
  let STABILITY_MS = STABILITY_MS_DEFAULT;
  const POLL_MS = 250;

  // Matchers (case-insensitive)
  const STOP_TEXTS = ["stop"];        // exact match (normalized)
  const WORKING_TOKENS = ["working"]; // starts-with; allows Working., Working.., Working…

  /* =========================
   * Pretty Console Logging
   * ========================= */
  const tag = (lvl) => [
    "%cREPLIT-PING%c " + lvl + "%c",
    "background:#121212;color:#00e5ff;padding:1px 4px;border-radius:3px",
    "color:#999",
    "color:inherit"
  ];
  const cI = (...a) => DEBUG && console.log(...tag("ℹ️"), ...a);
  const cS = (...a) => DEBUG && console.log(...tag("✅"), ...a);
  const cW = (...a) => DEBUG && console.log(...tag("⚠️"), ...a);
  const cE = (...a) => DEBUG && console.log(...tag("⛔"), ...a);
  const cT = (...a) => (DEBUG && TRACE_SCAN) && console.log(...tag("🔎"), ...a);

  const nowStr = () => new Date().toLocaleTimeString();

  /* =========================
   * DOM / Text Helpers
   * ========================= */
  const isEl = (n) => n && n.nodeType === 1;
  const isVisible = (el) => {
    if (!isEl(el)) return false;
    const rect = el.getBoundingClientRect?.();
    if (!rect || rect.width === 0 || rect.height === 0) return false;
    const st = getComputedStyle(el);
    if (st.display === "none" || st.visibility === "hidden" || parseFloat(st.opacity || "1") < 0.05) return false;
    return true;
  };

  const norm = (s) => (s || "")
    .replace(/\u2026/g, "...") // ellipsis → three dots
    .replace(/[.\s]+$/g, "")   // trim trailing dots/spaces
    .trim()
    .toLowerCase();

  function findVisibleEquals(tokens) {
    const hits = [];
    const nodes = document.querySelectorAll("span,button,[role='button'],div");
    let scanned = 0;
    for (const el of nodes) {
      scanned++;
      if (!isVisible(el)) continue;
      const txt = norm(el.textContent || "");
      if (tokens.some(tok => txt === tok)) {
        hits.push(el.closest("button,[role='button']") || el);
      }
    }
    cT(`findVisibleEquals scanned=${scanned} hits=${hits.length} tokens=${tokens.join(",")}`);
    return Array.from(new Set(hits));
  }

  function findVisibleStartsWith(tokens) {
    const hits = [];
    const nodes = document.querySelectorAll("span,div,button,[role='button']");
    let scanned = 0;
    for (const el of nodes) {
      scanned++;
      if (!isVisible(el)) continue;
      const txt = norm(el.textContent || "");
      if (tokens.some(tok => txt.startsWith(tok))) {
        hits.push(el);
      }
    }
    cT(`findVisibleStartsWith scanned=${scanned} hits=${hits.length} tokens=${tokens.join(",")}`);
    return Array.from(new Set(hits));
  }

  const isStopVisible    = () => findVisibleEquals(STOP_TEXTS).length > 0;
  const isWorkingVisible = () => findVisibleStartsWith(WORKING_TOKENS).length > 0;
  const isStreamingNow   = () => isStopVisible() || isWorkingVisible();

  /* =========================
   * SOUND #4 — Airy Harp Up-Gliss (HTMLAudio WAV + WebAudio fallback)
   *  - Four soft “pluck” notes rising: C5 → E5 → G5 → C6
   *  - Triangle oscillators with fast attack & gentle decay
   *  - Subtle global low-pass to keep it airy
   * ========================= */
  function makeReplitChimeWavDataURL() {
    const sr = 44100;
    const N  = Math.floor(sr * 1.05); // ~1.05s buffer
    const data = new Float32Array(N);

    // Pluck events
    const freqs = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
    const starts = [0.00, 0.06, 0.12, 0.18];        // seconds
    const A = 0.008;  // Attack seconds
    const D = 0.22;   // Total pluck duration (~decay to near zero)
    const amp = 0.62; // per-note peak

    const tri = (x) => (2 / Math.PI) * Math.asin(Math.sin(x));

    for (let n = 0; n < freqs.length; n++) {
      const f = freqs[n];
      const startIdx = Math.floor(starts[n] * sr);
      const len = Math.floor(D * sr);
      for (let i = 0; i < len && (startIdx + i) < N; i++) {
        const t = i / sr;
        // Envelope: quick attack → gentle exponential decay
        let env;
        if (t < A) {
          env = t / A;
        } else {
          const tau = 0.10; // decay constant
          env = Math.exp(-(t - A) / tau);
        }
        // Triangle pluck
        const s = tri(2 * Math.PI * f * t);
        data[startIdx + i] += amp * env * s;
      }
    }

    // Simple global low-pass (1st-order IIR) for softness (~4.5 kHz)
    const fc = 4500;
    const alpha = (2 * Math.PI * fc) / (2 * Math.PI * fc + sr);
    let y = 0;
    for (let i = 0; i < N; i++) {
      const x = data[i];
      y = y + alpha * (x - y);
      data[i] = y;
    }

    // Gentle soft clip
    for (let i = 0; i < N; i++) {
      const v = Math.max(-1, Math.min(1, data[i]));
      data[i] = Math.tanh(1.05 * v);
    }

    // Pack to 16-bit PCM WAV (mono)
    const bytes = 44 + N * 2;
    const dv = new DataView(new ArrayBuffer(bytes));
    let off = 0;
    const wStr = (s) => { for (let i = 0; i < s.length; i++) dv.setUint8(off++, s.charCodeAt(i)); };
    const w32 = (u) => { dv.setUint32(off, u, true); off += 4; };
    const w16 = (u) => { dv.setUint16(off, u, true); off += 2; };

    wStr("RIFF"); w32(36 + N * 2); wStr("WAVE");
    wStr("fmt "); w32(16); w16(1); w16(1); w32(sr); w32(sr * 2); w16(2); w16(16);
    wStr("data"); w32(N * 2);
    for (let i = 0; i < N; i++) {
      const v = Math.max(-1, Math.min(1, data[i]));
      dv.setInt16(off, v < 0 ? v * 0x8000 : v * 0x7FFF, true);
      off += 2;
    }

    // Base64 encode
    const u8 = new Uint8Array(dv.buffer);
    let b64 = "";
    for (let i = 0; i < u8.length; i += 0x8000) {
      b64 += btoa(String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000)));
    }
    return `data:audio/wav;base64,${b64}`;
  }

  const CHIME_URL = makeReplitChimeWavDataURL();
  const primeAudioEl = new Audio(CHIME_URL);
  primeAudioEl.preload = "auto";

  const AudioCtx = window.AudioContext || window.webkitAudioContext;
  let ctx;
  const ensureCtx = () => (ctx ||= new AudioCtx());

  async function playChime(reason) {
    // Primary: HTMLAudio
    try {
      const a = primeAudioEl.cloneNode();
      a.volume = 1.0;
      await a.play();
      cS(`🔊 Chime (Airy Harp, HTMLAudio) reason=${reason} @ ${nowStr()}`);
      return;
    } catch (e1) {
      cW("HTMLAudio failed; trying WebAudio (Airy Harp)", e1);
    }

    // Fallback: WebAudio version of Sound #4 (Airy Harp)
    try {
      const AC = window.AudioContext || window.webkitAudioContext;
      const c = window.__rp_ctx || new AC();
      window.__rp_ctx = c;
      if (c.state !== "running") await c.resume();

      const t0 = c.currentTime + 0.02;

      // Output chain: low-pass for softness
      const out = c.createGain(); out.gain.setValueAtTime(0.85, t0);
      const lp  = c.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 4500; lp.Q.value = 0.6;
      out.connect(lp); lp.connect(c.destination);

      const pluck = (time, f) => {
        const o = c.createOscillator(); o.type = "triangle"; o.frequency.setValueAtTime(f, time);
        const g = c.createGain();
        // Envelope: fast attack, gentle decay
        g.gain.setValueAtTime(0.0001, time);
        g.gain.exponentialRampToValueAtTime(0.6,  time + 0.008);
        g.gain.exponentialRampToValueAtTime(0.001, time + 0.22);
        o.connect(g); g.connect(out);
        o.start(time); o.stop(time + 0.25);
      };

      const freqs = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6
      let cur = t0;
      for (const f of freqs) { pluck(cur, f); cur += 0.06; }

      cS(`🔊 Chime (Airy Harp, WebAudio) reason=${reason} @ ${nowStr()}`);
    } catch (e2) {
      cE("WebAudio play failed", e2);
    }
  }

  // Unlock audio on first interaction/visibility
  const unlock = async () => {
    try { await primeAudioEl.play(); primeAudioEl.pause(); primeAudioEl.currentTime = 0; cI("Audio unlocked via HTMLAudio"); } catch {}
    try { if (AudioCtx) { const c = ensureCtx(); if (c.state !== "running") await c.resume(); cI("AudioContext resumed"); } } catch {}
    window.removeEventListener("pointerdown", unlock, true);
    window.removeEventListener("keydown", unlock, true);
  };
  window.addEventListener("pointerdown", unlock, true);
  window.addEventListener("keydown", unlock, true);
  document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") unlock(); });

  /* =========================
   * FSM (Stop/Working-driven)
   * ========================= */
  let sid = 0;
  let s = null;
  let pollId = 0;
  let lastStop = false;
  let lastWork = false;

  const STATE = { IDLE: "IDLE", STREAMING: "STREAMING", DONE: "DONE" };

  function startPoll() {
    if (pollId) return;
    pollId = window.setInterval(tick, POLL_MS);
  }
  function stopPoll() {
    if (pollId) { clearInterval(pollId); pollId = 0; }
  }

  function armStreaming(origin) {
    if (s && s.state !== STATE.DONE) return;
    s = { id: ++sid, state: STATE.STREAMING, lastStableStart: 0, sawStreaming: true };
    cI(`▶️ STREAMING s#${s.id} (origin=${origin}) @ ${nowStr()}`);
    startPoll();
  }

  function maybeComplete() {
    if (!s || s.state === STATE.DONE) return;
    const streaming = isStreamingNow();
    const now = performance.now();

    // Visibility transition logs
    const curStop = isStopVisible();
    const curWork = isWorkingVisible();
    if (curStop !== lastStop) {
      cI(`Stop visibility: ${curStop ? "ON" : "OFF"}`);
      lastStop = curStop;
    }
    if (curWork !== lastWork) {
      cI(`Working visibility: ${curWork ? "ON" : "OFF"}`);
      lastWork = curWork;
    }

    if (!streaming) {
      if (!s.lastStableStart) {
        s.lastStableStart = now;
        cI(`⏳ Stability window started (${STABILITY_MS}ms)`);
      }
      const elapsed = now - s.lastStableStart;
      if (elapsed >= STABILITY_MS) {
        s.state = STATE.DONE;
        cS(`DONE s#${s.id} (no Stop & no Working for ${Math.round(elapsed)}ms) @ ${nowStr()}`);
        playChime(`s#${s.id}`);
        stopPoll();
        s = null;
      }
    } else {
      if (s.lastStableStart) cW("Stability reset (streaming reappeared)");
      s.lastStableStart = 0;
    }
  }

  function tick() {
    const streaming = isStreamingNow();
    if (streaming && (!s || s.state === STATE.DONE)) {
      armStreaming("tick");
    }
    if (s && s.state === STATE.STREAMING) {
      maybeComplete();
    }
  }

  /* =========================
   * Observers & Event hooks
   * ========================= */
  const obs = new MutationObserver((mutations) => {
    let nudge = false;
    for (const m of mutations) {
      if (m.type === "childList") {
        for (const n of [...m.addedNodes, ...m.removedNodes]) {
          if (isEl(n)) {
            const txt = norm(n.textContent || "");
            if (STOP_TEXTS.some(s => txt.includes(s)) || WORKING_TOKENS.some(w => txt.startsWith(w))) { nudge = true; break; }
          }
        }
      } else if (m.type === "attributes") {
        const el = m.target;
        if (!isEl(el)) continue;
        const txt = norm(el.textContent || "");
        if (STOP_TEXTS.some(s => txt.includes(s)) || WORKING_TOKENS.some(w => txt.startsWith(w))) { nudge = true; }
      }
      if (nudge) break;
    }
    if (nudge) {
      cT("Mutation nudged tick()");
      tick();
    }
  });

  function start() {
    if (!document.body) {
      document.addEventListener("DOMContentLoaded", start, { once: true });
      return;
    }
    obs.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["class", "style", "aria-hidden"]
    });
    startPoll();
    cI("Armed (Stop/Working FSM). Will chime when both disappear (stable).");
    // Initial state snapshot
    lastStop = isStopVisible();
    lastWork = isWorkingVisible();
    cI(`Initial: Stop=${lastStop} Working=${lastWork}`);
    if (lastStop || lastWork) armStreaming("initial");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start, { once: true });
  } else {
    start();
  }

  /* =========================
   * Runtime Controls (Console)
   * =========================
   * window.ReplitPing.setDebug(true|false)
   * window.ReplitPing.setTrace(true|false)
   * window.ReplitPing.setStability(ms)
   * window.ReplitPing.status()
   * window.ReplitPing.test()
   */
  window.ReplitPing = {
    setDebug(v){ DEBUG = !!v; cI(`DEBUG=${DEBUG}`); return DEBUG; },
    setTrace(v){ TRACE_SCAN = !!v; cI(`TRACE_SCAN=${TRACE_SCAN}`); return TRACE_SCAN; },
    setStability(ms){ STABILITY_MS = Math.max(0, Number(ms)||STABILITY_MS_DEFAULT); cI(`STABILITY_MS=${STABILITY_MS}`); return STABILITY_MS; },
    status(){
      const streaming = isStreamingNow();
      const state = s ? s.state : STATE.IDLE;
      const info = {
        state,
        sessionId: s?.id ?? null,
        stopVisible: isStopVisible(),
        workingVisible: isWorkingVisible(),
        streaming,
        stabilityMs: STABILITY_MS,
        pollActive: !!pollId,
        time: nowStr(),
      };
      cI("Status", info);
      return info;
    },
    async test(){ await playChime("manual-test"); return true; }
  };

})();

A tool for importing from Google Docs that doesn’t suck (Mammoth .docx converter)

I was tired of WordPress’s crappy editor so I decided to write a post in Google Docs instead.

However, when copying the content back in. All the images I carefully added did not carry back over. After trying several terrible options (Seraphinite Post .DOCX Source, WordPress.com for Google Docs, docswrite.com — this didn’t work when I tried but seems to function fine after going back and forth with their CEO) I finally tried out Mammoth and it just works which is great.

  1. Add a post
  2. Look for the Mammoth .docx converter box at the bottom
  3. Upload your file
  4. Wait for it to parse
  5. Click on Insert into editor
  6. And then wait for it to finish adding all the content into the editor (my docx took a few minutes since it had quite a few high resolution images)

Japan Recommendations

A quick note on the below content. All of the things I’ve included I personally enjoyed greatly and would enjoy to anyone who shares my tastes. Have fun in Japan!

Tokyo

Tokyo food

  • L’Effervescence


  • Oniku Karyu
  • Cokuun (Coffee Omakase)
  • I’m Donut ?
  • Pizza Bar


  • Cycle
  • IPPUKU&MATCHA

  • Blue Bottle Cafe (surprisingly good Matcha Latte)

  • Parklet Bakery


  • Iki Expresso


  • Le Petit Mec Hibiya (the best pastries I’ve had in Tokyo)

Tokyo things to do

  • Nezu Museum

  • Hamarikyu Gardens



  • Ueno Park (come at golden hour)



  • Tokyo National Museum
  • Teamlabs Borderless
  • Teamlabs Planets

Kyoto

Kyoto Food

  • Akagakiya

Kyoto things to do

  • Kyoto Golden Temple
  • Otagi Nenbutsuji Temple
  • Arashiyama Bamboo Grove (make sure you hike up into the park by the Bamboo Grove for the Valley View!)
  • Mt Inari (go early and hike to the top)
  • Kiyomizu-dera Temple (this was the most spectacular temple that I visited)

  • Ruriko-in Temple

Osaka

Osaka Food

Osaka Things to do

  • Osaka Castle (go at sunrise, its spectacular!)

Other places

Lake Kawaguchi

Onomichi

  • Shinomani-Kaido

Himeji

  • Himeji Castle



Wakayama

  • Tatago Rock

Nara

  • Nara Park


Nikko

  • Kinfuri Falls


  • Shinkyo Bridge


  • Nikko Tamozawa Imperial Villa Memorial Park




  • Nikkō Tōshogū


Tokyo

Tokyo food

  • L’Effervescence

  • Oniku Karyu
  • Cokuun (Coffee Omakase)
  • I’m Donut ?
  • Pizza Bar

  • Cycle
  • IPPUKU&MATCHA

  • Blue Bottle Cafe (surprisingly good Matcha Latte)

  • Parklet Bakery

  • Iki Expresso

  • Le Petit Mec Hibiya (the best pastries I’ve had in Tokyo)

Tokyo things to do

  • Nezu Museum

  • Hamarikyu Gardens

  • Ueno Park (come at golden hour)

  • Tokyo National Museum
  • Teamlabs Borderless
  • Teamlabs Planets

Kyoto

Kyoto Food

  • Akagakiya

Kyoto things to do

  • Kyoto Golden Temple
  • Otagi Nenbutsuji Temple
  • Arashiyama Bamboo Grove (make sure you hike up into the park by the Bamboo Grove for the Valley View!)
  • Mt Inari (go early and hike to the top)
  • Kiyomizu-dera Temple (this was the most spectacular temple that I visited)

  • Ruriko-in Temple

Osaka

Osaka Food

Osaka Things to do

  • Osaka Castle (go at sunrise, its spectacular!)

Other places

Lake Kawaguchi

Onomichi

  • Shinomani-Kaido

Himeji

  • Himeji Castle

Wakayama

  • Tatago Rock

Nara

  • Nara Park

Nikko

  • Kinfuri Falls

  • Shinkyo Bridge

  • Nikko Tamozawa Imperial Villa Memorial Park

  • Nikkō Tōshogū

Tokyo

Tokyo food

  • L’Effervescence

  • Oniku Karyu
  • Cokuun (Coffee Omakase)
  • I’m Donut ?
  • Pizza Bar

  • Cycle
  • IPPUKU&MATCHA

  • Blue Bottle Cafe (surprisingly good Matcha Latte)

  • Parklet Bakery

  • Iki Expresso

  • Le Petit Mec Hibiya (the best pastries I’ve had in Tokyo)

Tokyo things to do

  • Nezu Museum

  • Hamarikyu Gardens

  • Ueno Park (come at golden hour)

  • Tokyo National Museum
  • Teamlabs Borderless
  • Teamlabs Planets

Kyoto

Kyoto Food

  • Akagakiya

Kyoto things to do

  • Kyoto Golden Temple
  • Otagi Nenbutsuji Temple
  • Arashiyama Bamboo Grove (make sure you hike up into the park by the Bamboo Grove for the Valley View!)
  • Mt Inari (go early and hike to the top)
  • Kiyomizu-dera Temple (this was the most spectacular temple that I visited)

  • Ruriko-in Temple

Osaka

Osaka Food

Osaka Things to do

  • Osaka Castle (go at sunrise, its spectacular!)

Other places

Lake Kawaguchi

Onomichi

  • Shinomani-Kaido

Himeji

  • Himeji Castle

Wakayama

  • Tatago Rock

Nara

  • Nara Park

Nikko

  • Kinfuri Falls

  • Shinkyo Bridge

  • Nikko Tamozawa Imperial Villa Memorial Park

  • Nikkō Tōshogū

ChatGPT Chime on Chat Completion (Tampermonkey Script)

Tampermonkey script

// ==UserScript==
// @name         ChatGPT Completion Ping (Composer FSM, background-safe, no-timeout)
// @namespace    nicholas.tools
// @version      5.4.0
// @description  Chime on completion even when window/tab isn't focused. No timeout; FSM: saw Stop → Stop gone + editor empty. Poll + resilient audio.
// @match        https://chat.openai.com/*
// @match        https://chatgpt.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(() => {
  "use strict";

  /* =========================
   * Logging
   * ========================= */
  const DEBUG = true;
  const log = (...a) => DEBUG && console.log("[COMP-PING]", ...a);
  const t = () => new Date().toLocaleTimeString();

  /* =========================
   * Selectors (composer only)
   * ========================= */
  const COMPOSER_EDITABLE    = '#prompt-textarea.ProseMirror[contenteditable="true"]';
  const COMPOSER_FALLBACK_TA = 'textarea[name="prompt-textarea"]';
  const SEND_BTN             = '#composer-submit-button[data-testid="send-button"]';
  const STOP_BTN             = '#composer-submit-button[data-testid="stop-button"]';

  /* =========================
   * Audio: HTMLAudio primary (WAV data URL), WebAudio fallback
   * ========================= */
  function makeChimeWavDataURL() {
    const sr = 44100, dur = 0.99;
    const notes = [
      { f: 987.77, d: 0.22 }, { f: 1318.51, d: 0.22 },
      { f: 1174.66, d: 0.20 }, { f: 1318.51, d: 0.30 },
    ];
    const gap = 0.055, amp = 0.28;
    const N = Math.floor(sr * dur);
    const data = new Float32Array(N).fill(0);
    let t0 = 0;
    for (const { f, d } of notes) {
      const nSamp = Math.floor(d * sr);
      const start = Math.floor(t0 * sr);
      for (let i = 0; i < nSamp && start + i < N; i++) {
        const env = i < 0.01*sr ? i/(0.01*sr) : (i > nSamp-0.03*sr ? Math.max(0, (nSamp - i)/(0.03*sr)) : 1);
        const s = Math.sin(2*Math.PI*f*(i/sr));
        const s2 = Math.sin(2*Math.PI*(f*1.005)*(i/sr)) * 0.6;
        data[start+i] += amp * env * (0.7*s + 0.3*s2);
      }
      t0 += d + gap;
    }
    const pcm = new DataView(new ArrayBuffer(44 + N*2));
    let off = 0;
    const wStr = (s) => { for (let i=0;i<s.length;i++) pcm.setUint8(off++, s.charCodeAt(i)); };
    const w32  = (u) => { pcm.setUint32(off, u, true); off+=4; };
    const w16  = (u) => { pcm.setUint16(off, u, true); off+=2; };
    wStr("RIFF"); w32(36 + N*2); wStr("WAVE");
    wStr("fmt "); w32(16); w16(1); w16(1); w32(sr); w32(sr*2); w16(2); w16(16);
    wStr("data"); w32(N*2);
    for (let i=0;i<N;i++) { const v = Math.max(-1, Math.min(1, data[i])); pcm.setInt16(off, v<0?v*0x8000:v*0x7FFF, true); off+=2; }
    const u8 = new Uint8Array(pcm.buffer);
    const b64 = btoa(String.fromCharCode(...u8));
    return `data:audio/wav;base64,${b64}`;
  }

  const CHIME_URL = makeChimeWavDataURL();
  const primeAudioEl = new Audio(CHIME_URL);
  primeAudioEl.preload = "auto";

  const AudioCtx = window.AudioContext || window.webkitAudioContext;
  let ctx;
  const ensureCtx = () => (ctx ||= new AudioCtx());

  async function playChime(reason) {
    try {
      const a = primeAudioEl.cloneNode();
      a.volume = 1.0;
      await a.play();
      log(`🔊 DONE (HTMLAudio) ${reason} @ ${t()}`);
      return;
    } catch {}
    try {
      const c = ensureCtx();
      if (c.state !== "running") await c.resume();
      const t0 = c.currentTime + 0.02;
      const master = c.createGain(); master.gain.setValueAtTime(0.9, t0); master.connect(c.destination);
      const lp = c.createBiquadFilter(); lp.type="lowpass"; lp.frequency.value=4200; lp.Q.value=0.6; lp.connect(master);
      const delay = c.createDelay(0.5); delay.delayTime.value=0.18;
      const fb = c.createGain(); fb.gain.value=0.22; delay.connect(fb); fb.connect(delay); delay.connect(master);
      const bus = c.createGain(); bus.gain.value=0.85; bus.connect(lp); bus.connect(delay);

      const seq = [
        { f: 987.77, d: 0.22 }, { f: 1318.51, d: 0.22 },
        { f: 1174.66, d: 0.20 }, { f: 1318.51, d: 0.30 },
      ];
      let cur = t0, gap = 0.055;
      for (const {f,d} of seq) {
        const o1=c.createOscillator(), g1=c.createGain(); o1.type="triangle"; o1.frequency.value=f;
        g1.gain.setValueAtTime(0.0001,cur); g1.gain.exponentialRampToValueAtTime(0.6,cur+0.01); g1.gain.exponentialRampToValueAtTime(0.001,cur+d);
        o1.connect(g1); g1.connect(bus); o1.start(cur); o1.stop(cur+d+0.02);

        const o2=c.createOscillator(), g2=c.createGain(); o2.type="sine"; o2.frequency.setValueAtTime(f*1.005,cur);
        g2.gain.setValueAtTime(0.0001,cur); g2.gain.exponentialRampToValueAtTime(0.35,cur+0.012); g2.gain.exponentialRampToValueAtTime(0.001,cur+d);
        o2.connect(g2); g2.connect(bus); o2.start(cur); o2.stop(cur+d+0.02);

        cur += d + gap;
      }
      log(`🔊 DONE (WebAudio) ${reason} @ ${t()}`);
    } catch {}
  }

  // Prime on user interaction
  const unlock = async () => {
    try { await primeAudioEl.play(); primeAudioEl.pause(); primeAudioEl.currentTime = 0; } catch {}
    try { if (AudioCtx) { const c = ensureCtx(); if (c.state !== "running") await c.resume(); } } catch {}
    window.removeEventListener("pointerdown", unlock, true);
    window.removeEventListener("keydown", unlock, true);
  };
  window.addEventListener("pointerdown", unlock, true);
  window.addEventListener("keydown", unlock, true);
  document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") unlock(); });

  /* =========================
   * Composer helpers
   * ========================= */
  const isEl = (n) => n && n.nodeType === 1;
  const visible = (sel) => { const el = document.querySelector(sel); return !!(el && el.offsetParent !== null); };
  const editorEl = () => document.querySelector(COMPOSER_EDITABLE) || document.querySelector(COMPOSER_FALLBACK_TA) || null;
  function editorEmpty() {
    const el = editorEl();
    if (!el) return true;
    if (el.matches('textarea')) return (el.value || '').replace(/\u200b/g,'').trim().length === 0;
    const txt = (el.textContent || '').replace(/\u200b/g,'').trim();
    return txt.length === 0;
  }
  const isStopVisible = () => visible(STOP_BTN);

  /* =========================
   * FSM + background-safe polling (NO TIMEOUT)
   * ========================= */
  let sid = 0;
  let s = null;
  let pollId = 0;
  const STATE = { IDLE:'IDLE', ARMED:'ARMED', CLEARED:'CLEARED', STREAMING:'STREAMING', DONE:'DONE' };

  function stopPoll() { if (pollId) { clearInterval(pollId); pollId = 0; } }

  function startPoll() {
    stopPoll();
    // steady 250ms poll; browsers may throttle in background which is fine
    pollId = window.setInterval(() => tick(true), 250);
  }

  function cancelSession(reason) {
    if (!s) return;
    log(`CANCEL s#${s.id} (${reason})`);
    stopPoll();
    s = null;
  }

  function arm(reason) {
    // Cancel any previous session (no timeout; avoid multiple active)
    if (s) cancelSession("re-ARM");
    s = {
      id: ++sid,
      state: STATE.ARMED,
      sawStop: false,
      sawCleared: editorEmpty(),
      lastStopGoneAt: 0
    };
    log(`ARM s#${s.id} (${reason}) empty=${s.sawCleared} stop=${isStopVisible()} @ ${t()}`);
    startPoll();
    tick();
  }

  function transition(newState, why) {
    if (!s || s.state === STATE.DONE) return;
    if (s.state !== newState) {
      s.state = newState;
      log(`${newState} s#${s.id} (${why}) empty=${editorEmpty()} stop=${isStopVisible()} @ ${t()}`);
    }
  }

  function evaluate() {
    if (!s || s.state === STATE.DONE) return;

    // Editor cleared after send
    if (!s.sawCleared && editorEmpty()) {
      s.sawCleared = true;
      transition(STATE.CLEARED, "editor cleared");
    }

    // Streaming seen
    if (!s.sawStop && isStopVisible()) {
      s.sawStop = true;
      transition(STATE.STREAMING, "stop visible");
    }

    // Stop disappears
    if (s.sawStop && !isStopVisible() && !s.lastStopGoneAt) {
      s.lastStopGoneAt = performance.now();
      log(`STOP-GONE s#${s.id} (detected)`);
    }

    // Completion: saw Stop once AND Stop gone AND editor empty (150ms stability)
    if (s.sawStop && !isStopVisible() && editorEmpty()) {
      const stable = s.lastStopGoneAt ? (performance.now() - s.lastStopGoneAt) : 999;
      if (stable >= 150) {
        transition(STATE.DONE, "stop gone + editor empty");
        playChime(`s#${s.id}`);
        stopPoll();
        s = null;
      }
    }
  }

  const tick = () => { evaluate(); };

  /* =========================
   * Events & Observers
   * ========================= */
  document.addEventListener("click", (e) => {
    const btn = isEl(e.target) ? e.target.closest(SEND_BTN) : null;
    if (!btn) return;
    arm("send-button click");
  }, true);

  document.addEventListener("keydown", (e) => {
    const ed = isEl(e.target) && (e.target.closest(COMPOSER_EDITABLE) || e.target.closest(COMPOSER_FALLBACK_TA));
    if (!ed) return;
    if (e.key !== "Enter" || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return;
    if (document.querySelector(SEND_BTN)) arm("keyboard Enter");
  }, true);

  const obs = new MutationObserver((mutations) => {
    if (!s) return;
    for (const m of mutations) {
      if (m.type === "attributes") {
        const el = m.target;
        if (isEl(el) && (el.id === "composer-submit-button" || el.matches(COMPOSER_EDITABLE) || el.matches(COMPOSER_FALLBACK_TA))) {
          tick();
        }
      }
      if (m.type === "childList") {
        for (const n of m.addedNodes) {
          if (!isEl(n)) continue;
          if (n.matches(STOP_BTN) || n.matches(SEND_BTN) ||
              n.querySelector?.(STOP_BTN) || n.querySelector?.(SEND_BTN) ||
              n.matches(COMPOSER_EDITABLE) || n.matches(COMPOSER_FALLBACK_TA) ||
              n.querySelector?.(COMPOSER_EDITABLE) || n.querySelector?.(COMPOSER_FALLBACK_TA)) {
            tick();
            break;
          }
        }
        for (const n of m.removedNodes) {
          if (!isEl(n)) continue;
          if (n.matches(STOP_BTN) || n.matches(SEND_BTN) ||
              n.matches(COMPOSER_EDITABLE) || n.matches(COMPOSER_FALLBACK_TA)) {
            tick();
            break;
          }
        }
      }
      if (m.type === "characterData") tick();
    }
  });

  function start() {
    obs.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["data-testid","id","class","style","contenteditable","value"],
      characterData: true
    });
    log("armed (composer FSM, background-safe, no-timeout). Completes on: saw Stop → Stop gone + editor empty.");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start, { once: true });
  } else {
    start();
  }
})();

Go actual full screen on Simplepractice.com video calls

When I’m on calls with my therapist it has bugged me that I can’t actually go fullscreen with the video so I gave CGPT the code for the page and wrote up a script that expands the fullscreen video to actually be fullscreen.

Use either the console script or Tampermonkey script and try it out for yourself!

Console script

(() => {
  const TOGGLE = 'tm-full-bleed-active';
  const ROOT_SEL = 'div.bbwWj.video';
  const PANEL_SEL = `${ROOT_SEL} > div.mljTO`;
  const STREAM_CONTAINER_SEL = `${ROOT_SEL} .stream-container`;
  const PARTICIPANT_SEL = `${ROOT_SEL} .participant-container`;
  const VIDEO_SEL = `${ROOT_SEL} video`;

  // --- style injection ---
  const css = `
    html.${TOGGLE}, body.${TOGGLE}{margin:0!important;padding:0!important;overflow:hidden!important}
    html.${TOGGLE} ${PANEL_SEL}{
      position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;
      margin:0!important;padding:0!important;border:0!important;background:#000!important;
      z-index:2147483647!important;box-shadow:none!important
    }
    html.${TOGGLE} ${STREAM_CONTAINER_SEL}, html.${TOGGLE} ${PARTICIPANT_SEL}{
      position:absolute!important;inset:0!important;width:100%!important;height:100%!important;
      margin:0!important;padding:0!important;border:0!important;max-width:none!important;max-height:none!important;overflow:hidden!important;background:transparent!important
    }
    html.${TOGGLE} ${VIDEO_SEL}{
      position:absolute!important;inset:0!important;width:100%!important;height:100%!important;
      object-fit:cover!important;object-position:center center!important;display:block!important;background:#000!important;border:0!important;box-shadow:none!important;transform:none!important
    }
    html.${TOGGLE} ${ROOT_SEL} .name, html.${TOGGLE} ${ROOT_SEL} .pin-button, html.${TOGGLE} ${ROOT_SEL} .more-button{
      display:none!important;visibility:hidden!important;pointer-events:none!important
    }
    .tm-fb-btn{
      position:fixed!important;right:14px;bottom:14px;z-index:2147483647!important;
      font:600 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
      padding:10px 12px;border-radius:999px;background:rgba(0,0,0,.6);color:#fff;
      border:1px solid rgba(255,255,255,.2);cursor:pointer;user-select:none;backdrop-filter:blur(4px)
    }
    .tm-fb-btn:hover{background:rgba(0,0,0,.75)}
  `;
  let style = document.getElementById('tm-fb-style');
  if (!style) {
    style = document.createElement('style');
    style.id = 'tm-fb-style';
    style.textContent = css;
    document.head.appendChild(style);
  }

  // --- helpers ---
  const $ = (s, r = document) => r.querySelector(s);
  const nap = (ms) => new Promise(r => setTimeout(r, ms));
  const waitFor = async (sel, tries = 200, step = 100) => {
    while (tries-- > 0) { const el = $(sel); if (el) return el; await nap(step); }
    return null;
  };

  // --- state ---
  let btn;

  const isActive = () =>
    document.documentElement.classList.contains(TOGGLE) ||
    document.body.classList.contains(TOGGLE);

  const setActive = (on) => {
    [document.documentElement, document.body].forEach(el => el && el.classList.toggle(TOGGLE, on));
    if (btn) btn.textContent = on ? 'Exit Full-Bleed' : 'Full-Bleed';
  };

  const toggle = () => setActive(!isActive());

  // --- UI button ---
  const mountButton = () => {
    if (btn && document.body.contains(btn)) return btn;
    btn = document.createElement('button');
    btn.className = 'tm-fb-btn';
    btn.type = 'button';
    btn.textContent = isActive() ? 'Exit Full-Bleed' : 'Full-Bleed';
    btn.addEventListener('click', toggle);
    document.body.appendChild(btn);
    return btn;
  };

  // --- key bindings ---
  const bindKeys = () => {
    window.addEventListener('keydown', (e) => {
      if (e.key === 'f' || e.key === 'F') { e.preventDefault(); toggle(); }
      else if (e.key === 'Escape' && isActive()) { e.preventDefault(); setActive(false); }
    }, { capture: true });
  };

  // --- dblclick on video ---
  const bindVideoDblClick = (v) => {
    if (!v || v.dataset.tmFbBound) return;
    v.addEventListener('dblclick', (e) => { e.preventDefault(); toggle(); }, { capture: true });
    v.dataset.tmFbBound = '1';
  };

  // --- observe dynamic DOM ---
  const observe = () => {
    const mo = new MutationObserver(() => {
      const v = $(VIDEO_SEL);
      if (v) bindVideoDblClick(v);
      if (!btn && document.body) mountButton();
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  };

  // --- bootstrap ---
  (async () => {
    await waitFor('body');
    bindKeys();
    observe();
    const v = await waitFor(VIDEO_SEL, 120, 150);
    if (v) bindVideoDblClick(v);
    mountButton();

    // Expose controls for manual use
    window.tmFullBleed = { toggle, on: () => setActive(true), off: () => setActive(false) };
    console.log('[tmFullBleed] Ready. Use tmFullBleed.toggle(), press F, double-click video, or use the button.');
  })();
})();

Tampermonkey script

// ==UserScript==
// @name         Full-Bleed Video (SimplePractice only)
// @namespace    nic.tools.video
// @version      1.0.1
// @description  Force main video to go true full-bleed (edge-to-edge) on SimplePractice video
// @author       you
// @match        https://video.simplepractice.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // ---------- small utils ----------
  const nap = (ms) => new Promise(r => setTimeout(r, ms));
  const q = (sel, root = document) => root.querySelector(sel);

  // Wait for an element or return null after tries*interval
  const waitFor = async (fnOrSel, tries = 200, interval = 100) => {
    while (tries-- > 0) {
      let el = null;
      try { el = typeof fnOrSel === 'function' ? fnOrSel() : q(fnOrSel); } catch {}
      if (el) return el;
      await nap(interval);
    }
    return null;
  };

  // ---------- selectors from provided markup ----------
  const ROOT_SEL = 'div.bbwWj.video';
  const PANEL_SEL = `${ROOT_SEL} > div.mljTO`;
  const STREAM_CONTAINER_SEL = `${ROOT_SEL} .stream-container`;
  const PARTICIPANT_SEL = `${ROOT_SEL} .participant-container`;
  const VIDEO_SEL = `${ROOT_SEL} video`;

  const TOGGLE_CLASS = 'tm-full-bleed-active';

  // ---------- inject styles ----------
  GM_addStyle(`
    /* Full-bleed mode root locks */
    html.${TOGGLE_CLASS}, body.${TOGGLE_CLASS} {
      margin: 0 !important;
      padding: 0 !important;
      overflow: hidden !important;
    }

    /* Pin the main panel to the viewport */
    html.${TOGGLE_CLASS} ${PANEL_SEL} {
      position: fixed !important;
      inset: 0 !important;
      width: 100vw !important;
      height: 100vh !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      background: #000 !important;
      z-index: 2147483647 !important;
      box-shadow: none !important;
    }

    /* Ensure no intermediate wrapper constrains size */
    html.${TOGGLE_CLASS} ${STREAM_CONTAINER_SEL},
    html.${TOGGLE_CLASS} ${PARTICIPANT_SEL} {
      position: absolute !important;
      inset: 0 !important;
      width: 100% !important;
      height: 100% !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      max-width: none !important;
      max-height: none !important;
      overflow: hidden !important;
      background: transparent !important;
    }

    /* Make the video fill and crop (letterboxing avoidance) */
    html.${TOGGLE_CLASS} ${VIDEO_SEL} {
      position: absolute !important;
      inset: 0 !important;
      width: 100% !important;
      height: 100% !important;
      object-fit: cover !important;
      object-position: center center !important;
      display: block !important;
      background: #000 !important;
      border: 0 !important;
      box-shadow: none !important;
      transform: none !important;
    }

    /* Hide overlays that might intrude (adjust as needed) */
    html.${TOGGLE_CLASS} ${ROOT_SEL} .name,
    html.${TOGGLE_CLASS} ${ROOT_SEL} .pin-button,
    html.${TOGGLE_CLASS} ${ROOT_SEL} .more-button {
      display: none !important;
      visibility: hidden !important;
      pointer-events: none !important;
    }

    /* On-screen toggle button */
    .tm-fb-btn {
      position: fixed !important;
      right: 14px;
      bottom: 14px;
      z-index: 2147483647 !important;
      font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
      padding: 10px 12px;
      border-radius: 999px;
      background: rgba(0,0,0,0.6);
      color: #fff;
      border: 1px solid rgba(255,255,255,0.2);
      cursor: pointer;
      user-select: none;
      backdrop-filter: blur(4px);
    }
    .tm-fb-btn:hover { background: rgba(0,0,0,0.75); }
  `);

  // ---------- toggle logic ----------
  const setActive = (on) => {
    const rootList = [document.documentElement, document.body].filter(Boolean);
    rootList.forEach(el => el.classList.toggle(TOGGLE_CLASS, on));
    if (btn) btn.textContent = on ? 'Exit Full-Bleed' : 'Full-Bleed';
  };

  const isActive = () =>
    document.documentElement.classList.contains(TOGGLE_CLASS) ||
    document.body.classList.contains(TOGGLE_CLASS);

  const toggle = () => setActive(!isActive());

  // ---------- button ----------
  let btn;
  const mountButton = () => {
    if (btn && document.body.contains(btn)) return btn;
    btn = document.createElement('button');
    btn.className = 'tm-fb-btn';
    btn.type = 'button';
    btn.textContent = isActive() ? 'Exit Full-Bleed (f)' : 'Full-Bleed (f)';
    btn.addEventListener('click', toggle);
    document.body.appendChild(btn);
    return btn;
  };

  // ---------- event bindings ----------
  const bindKeys = () => {
    window.addEventListener('keydown', (e) => {
      if (e.key === 'f' || e.key === 'F') {
        e.preventDefault();
        toggle();
      } else if (e.key === 'Escape' && isActive()) {
        e.preventDefault();
        setActive(false);
      }
    }, { capture: true });
  };

  const bindVideoDoubleClick = (video) => {
    if (!video || video.dataset.tmFbBound) return;
    video.addEventListener('dblclick', (e) => {
      e.preventDefault();
      toggle();
    }, { capture: true });
    video.dataset.tmFbBound = '1';
  };

  // ---------- observer to reapply on dynamic pages ----------
  const observe = () => {
    const mo = new MutationObserver(() => {
      const video = q(VIDEO_SEL);
      if (video) bindVideoDoubleClick(video);
      if (!btn && document.body) mountButton();
    });
    mo.observe(document.documentElement, { childList: true, subtree: true, attributes: false });
  };

  // ---------- bootstrap ----------
  (async () => {
    await waitFor(() => document.body);
    bindKeys();
    observe();

    const video = await waitFor(VIDEO_SEL, 120, 150);
    if (video) bindVideoDoubleClick(video);

    mountButton();

    // Optional: expose console controls
    window.tmFullBleed = { toggle, on: () => setActive(true), off: () => setActive(false) };
    console.log('[tmFullBleed] Ready on SimplePractice. Use tmFullBleed.toggle(), press F, double-click video, or use the button.');
  })();

})();

See my creation conversation with ChatGPT here:

https://chatgpt.com/share/68c89780-f700-8007-916b-2d707b1970a6

Fix for audio cutting out for 1-3 seconds randomly with MSI B650-S Wifi Motherboard

If your sound keeps cutting out seemingly randomly for your wirelessly headset with this motherboard then try plugging it into the other USB slots (not the blue ones, the red ones). It seems to have fixed my issue. Seems the other ones may not have enough power to support the power draw.

Found the fix here: Audio issues MSI Pro B650-S Wifi | MSI Global English Forum

Have you use another headphone to check for the symptom?

I think I found the solution to my issue :

The USB 3.2 Gen 1 ports connect to something called “Hub-1074” and I guess those aren’t good enough for USB headsets. I at first thought that it was an issue with the board because the chipset driver updates reduced the problem, but didn’t fix it. I then tried connecting with a different headset that uses 3.5mm jacks and it worked without issues, so now I have connected my USB headset to one of the USB 3.2 Gen 2 ports that connect to the CPU, and so far I haven’t had any audio issues.

My Favourite Podcasts!

Podcasts are one of my favourite ways to learn and entertain myself. Here are some of my faves from over the years.

  • Planet Money / The Indicator
    • Fairly light podcast on economics, very engaging hosts, interesting topics.
  • This American Life
    • A classic, beautiful stories on being human.
  • 99% Invisible
    • Learn awesome things about design and little details in life you wouldn’t have otherwise discovered.
  • Rumble Strip
    • Very direct, down to earth stories about everyday Vermonters. Especially love the ones with her farmer friend or town hall meetings. Note, I hate about 25% of the episodes as she’ll have random guest episodes and they can be awful.
  • What Roman Mars Can Learn About Con Law
    • Get into the minutia of constitutional law with Roman Mars and his law professor friend.
  • Twenty Thousand Hertz
    • The most beautifully podcast I’ve ever listened to. Amazing stories about audio in many different ways and a surprisingly genuine and warm. host.
  • The Big Dig
    • Ever wanted to find out exactly what went wrong in so many ways with a mega project in the United States? This podcast goes into the details on the many trials and tribulations of the Big Dig project in Boston. I learned a lot and it was super interesting!
  • Acquired
    • Listen to this if you’re interested in going in deep on companies. The hosts go on multi hour deep dives into the histories of the companies they cover. I find it to be super helpful for understanding how companies came to be but also what differentiates them from other companies.
  • Search Engine
    • In depth episodes on a very wide range of topics (the secret pool in Buckingham Palace, people who buy luggage at the airport, jawmaxxing) which I find very interesting and delightful in their unnecessary depth.
  • The Sporkful
    • Nice stories about food. The host’s speaking style isn’t my favourite and some of the episodes are a little meh but there are enough good ones in there to make it worthwhile.
  • Startup
    • Front row seats to creation and development over the years of Gimlet. Really fascinating glimpse into the birth of a company (and all the various things that come with it).
  • Reply All
    • One of Gimlet’s first successful podcasts, now retired due to a messy series of events but lots of fun and interesting episodes before that happened.
  • Heavyweight
    • Heartwarming and humanistic stories about the host trying to help a given guest try and resolve a deep seated relationship issue. Personally, I find the non-mainline episodes annoying but the standard episodes are great!
  • More Perfect
    • An awesomely in depth podcast series on how the Supreme Court works and some of their historic decisions. If you have any interest in the way the American legal system functions I wholeheartedly recommend this podcast.
  • The Disconnect
    • Want to spend 10+ hours learning about how Texas’s energy grid functions and why it failed? This is the podcast for you, :).
  • The End of the World (with Josh Clark)
    • Quite different from most podcasts out there. This one touches on a number of far far future scenarios and feels very sci fi at times. Excellent production and you’ll probably learn a few new concepts.
  • The Last Cup
    • Detailed storytelling surrounding Messi’s final world cup run and all the drama leading up to it. As a fan of Messi this was great to listen to plus it has a very satisfying end.
  • Gamecraft
    • One of Benchmark’s legendary GPs breaks down the economics and structure of the gaming industry piece by piece. Incredible knowledge contained within. Great if you’re interested in the business of gaming.
  • The Secret Life of Canada
    • Stories told about the often untold side of things from a indigenous perspective in Canada. A mix of humour and learning about horrific events you’ll likely learn a lot about things you didn’t know happened. The hosts have excellent chemistry with each other and do a fantastic job of covering the subject matter in a way that doesn’t shy away from the facts but also doesn’t leave you feeling irredeemably depressed.
  • Lenny’s Podcast
    • The best product podcast out there. Learn about a whole range of product related topics and from some of the strongest operators out there. When I feel like learning more about my craft I listen to this, :).
  • Containers
    • In depth coverage of the systems that power our ports (and how they’ve changed over time). The storytelling here is excellent and you’ll learn a lot about how goods move around the world.

How to get Tablepress tables to show in full width

  1. Go to Appearance –> Customize –> Additional CSS
    (If you’re in the right place, this should show after your site URL /wp-admin/customize.php)

2. Add in this CSS block modification

.content-area__wrapper {
–go–max-width: 100rem;
}

3. Now your tables will show at full width on your site!

If you want it to only apply to a specific page, publish your page, go to that page, right click to inspect and find out the page ID (it shows at the very top), then target your change to only apply to that page in your Additional CSS settings.

In my case, my page ID is 534 so I did the following

.page-id-534 .content-area__wrapper {
–go–max-width: 100rem;
}

Make sure you include a space between .page-id-534 and .content-area__wrapper

Here’s a screenshot of what my inspect view looked like.

How to build & check to see if your ads.txt file is correct for Google Ads (Adsense)

Your ads.txt file is composed of the following parts

For example: google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0

Where:

  • “google.com” is the advertising system domain
  • “pub-XXXXXXXXXXXXXXXX” is your unique publisher ID
  • “DIRECT” indicates a direct business relationship
  • “f08c47fec0942fa0” is Google’s AdSystem ID

So to create your own ads.txt, all you need to do is go to your account information page (Account information – Settings – Account – Google AdSense) and pull your publisher ID and slot it in.

Once you’ve got your ads.txt file set up on your website (I used Ads.txt Manager – WordPress plugin | WordPress.org for this).

When its added in, you can check to see if its showing up properly by visiting website.com/ads.txt (in my case pezant.ca/ads.txt)

Or if you want to, you can also use this tool which works well!

ads.txt Validator – Free ads.txt Validation Tool by ads.txt Guru (adstxt.guru)

Updating your wordpress PW without being able to password reset via email (GCP hosted WordPress instance)

Steps:

  1. Get to your VM instance page (https://console.cloud.google.com/compute/instances)
  2. Click on the SSH dropdown
  3. Enter the following commands

~$ cd /var/www/html
/var/www/html$ wp user update [username] –user_pass=[new_password]

Here’s an example if your user name was “admin” and you wanted the password to be “new_pass” you would enter the following after navigating to the right directory.

wp user update admin –user_pass=new_pass

Here’s the SSH button if you don’t know where to find it