<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Hasan Akalp — Writing</title><description>Notes from building small tools and larger systems, in English and sometimes Turkish.</description><link>https://akalp.co</link><language>en</language><item><title>Ten Hours, One Click, Almost Gone: Obsidian Plugin, iCloud Recovery, and a Small Lesson</title><link>https://akalp.co/writing/ten-hours-one-click</link><guid isPermaLink="true">https://akalp.co/writing/ten-hours-one-click</guid><description>How a quick delete in Obsidian wiped a plugin project, how iCloud Data Recovery saved it, and the workflow changes I made immediately after.</description><pubDate>Sat, 20 Dec 2025 00:41:00 GMT</pubDate><content:encoded>&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;Last week I was writing an Obsidian plugin. After about ten hours of work, I was close to a version I could actually use.&lt;/p&gt;
&lt;p&gt;Then a small distraction turned into a big problem.&lt;/p&gt;
&lt;p&gt;In a few seconds, my plugin folder disappeared. No drama, no complex bug — just a couple of quick clicks made while my brain was in “fix it fast” mode.&lt;/p&gt;
&lt;p&gt;This is the story of what happened, how iCloud Data Recovery saved me, and why I changed my workflow right after.&lt;/p&gt;
&lt;h2&gt;What I was building&lt;/h2&gt;
&lt;p&gt;I’ve been using Obsidian for a long time. Besides regular note-taking, I also track a personal metric related to health and breathing quality.&lt;/p&gt;
&lt;p&gt;My idea was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store daily metric values inside &lt;strong&gt;daily notes&lt;/strong&gt;, in the &lt;strong&gt;frontmatter&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Run periodic analysis and generate simple recommendations&lt;/li&gt;
&lt;li&gt;Draw a custom &lt;strong&gt;Bases view&lt;/strong&gt; that fits my workflow better than a generic table&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I started building a plugin around this. I kept it private, because it’s personal, and I don’t want to over-polish it or publish it yet. But technically, it was already “real”: TypeScript, a build pipeline, and a working prototype.&lt;/p&gt;
&lt;p&gt;And then I made a change that triggered everything.&lt;/p&gt;
&lt;h2&gt;Why it went wrong&lt;/h2&gt;
&lt;p&gt;In Obsidian plugins, &lt;code&gt;manifest.json&lt;/code&gt; is not just a random config file. It’s part of how Obsidian identifies and loads a plugin.&lt;/p&gt;
&lt;p&gt;During quick experiments, I changed the plugin &lt;strong&gt;name&lt;/strong&gt; in &lt;code&gt;manifest.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When I went back to Obsidian, my plugin appeared twice. At that moment I assumed it was just a temporary state problem — something like “Obsidian UI got confused” while I was iterating quickly.&lt;/p&gt;
&lt;p&gt;So I did what many of us do when we see duplicate entries: I clicked &lt;strong&gt;Delete&lt;/strong&gt; on one of them.&lt;/p&gt;
&lt;p&gt;That was the real mistake.&lt;/p&gt;
&lt;p&gt;Because I was following the Developer Docs–style setup, my whole development codebase lived inside the vault under &lt;code&gt;.obsidian/plugins/...&lt;/code&gt;. So deleting “the plugin” was not just uninstalling a build artifact. &lt;mark&gt;It removed the actual source folder.&lt;/mark&gt;&lt;/p&gt;
&lt;p&gt;I commit often, but I hadn’t pushed yet.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;So it felt like watching ten hours fly away.&lt;/p&gt;
&lt;h2&gt;The recovery moment: iCloud.com Data Recovery&lt;/h2&gt;
&lt;p&gt;First, I checked the Trash. Nothing.&lt;/p&gt;
&lt;p&gt;Then I checked iCloud “Recently Deleted”. Still nothing.&lt;/p&gt;
&lt;p&gt;I was stressed and moving fast, clicking around iCloud.com with that mix of panic and anger — and I randomly found a section called &lt;strong&gt;Data Recovery&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://akalp.co/_astro/ten-hours-one-click-files.DyadUXZF.png&quot; alt=&quot;Thousands of files&quot;&gt;&lt;/p&gt;
&lt;p&gt;It showed a message like “thousands of recoverable files”. I opened it, and I saw entries marked as deleted just minutes ago.&lt;/p&gt;
&lt;p&gt;That was it. My files were there.&lt;/p&gt;
&lt;p&gt;But there was another problem: the list wasn’t just my plugin code.&lt;/p&gt;
&lt;p&gt;Because my plugin folder also contained &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.git&lt;/code&gt;, the recovery list was huge. Thousands of files. And I only cared about maybe 5–10 real artifacts.&lt;/p&gt;
&lt;h2&gt;The semi-technical trick: scripting a virtualized list&lt;/h2&gt;
&lt;p&gt;I decided to speed things up with a small script in Safari DevTools.&lt;/p&gt;
&lt;p&gt;My first plan was full automation: detect the items under my plugin path and auto-select them. But it didn’t work reliably.&lt;/p&gt;
&lt;p&gt;The reason is how Apple built that recovery UI: it looks like a long list, but it’s not actually rendering thousands of DOM nodes at the same time. It uses a common performance pattern called &lt;strong&gt;list virtualization&lt;/strong&gt;.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;In short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The modal only keeps a small set of rows in the DOM&lt;/li&gt;
&lt;li&gt;When you scroll, the UI &lt;em&gt;reuses the same row elements&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The “path text” and other values update dynamically inside those reused nodes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So a script that “selects everything matching X” can fail because the element you marked might later represent a totally different file after scrolling.&lt;/p&gt;
&lt;p&gt;Once I realized this, I changed the approach.&lt;/p&gt;
&lt;p&gt;Instead of trying to select items programmatically, I turned the script into a visual helper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Files under my target prefix got a &lt;strong&gt;green border&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Everything else got a &lt;strong&gt;red border&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://akalp.co/_astro/ten-hours-one-click-after-script.s68aSadw.png&quot; alt=&quot;Green/Red Borders&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then I manually scrolled through the list and checked only the green ones. It wasn’t perfect, but it was stable. After 15–20 minutes of scrolling and selecting, I restored the files.&lt;/p&gt;
&lt;p&gt;And the code was back (without the heavy folders, which I didn’t need anyway).&lt;/p&gt;
&lt;h2&gt;What I changed immediately&lt;/h2&gt;
&lt;p&gt;After recovery, I didn’t continue development. First, I fixed the workflow.&lt;/p&gt;
&lt;p&gt;I moved the project out of the vault into my normal workspace and treated the vault as a &lt;mark&gt;runtime target, not a source of truth&lt;/mark&gt;.&lt;/p&gt;
&lt;p&gt;I also added a packaging flow so only the small output set ends up in the vault.&lt;/p&gt;
&lt;p&gt;My scripts ended up like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
  &quot;build&quot;: &quot;tsc -noEmit -skipLibCheck &amp;#x26;&amp;#x26; node esbuild.config.mjs production&quot;,
  &quot;build:dev&quot;: &quot;tsc -noEmit -skipLibCheck &amp;#x26;&amp;#x26; node esbuild.config.mjs development&quot;,
  &quot;package&quot;: &quot;npm run package:prod&quot;,
  &quot;package:prod&quot;: &quot;npm run build &amp;#x26;&amp;#x26; node package-plugin.mjs&quot;,
  &quot;package:dev&quot;: &quot;npm run build:dev &amp;#x26;&amp;#x26; node package-plugin.mjs&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the packaging script basically copies only what matters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { promises as fs } from &quot;fs&quot;;
import path from &quot;path&quot;;
import { fileURLToPath } from &quot;url&quot;;

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

async function ensureDir(dir) {
  await fs.mkdir(dir, { recursive: true });
}

async function removeDir(dir) {
  await fs.rm(dir, { recursive: true, force: true });
}

async function fileExists(filePath) {
  try {
    await fs.access(filePath);
    return true;
  } catch {
    return false;
  }
}

async function copyFile(source, destination) {
  await fs.copyFile(source, destination);
}

async function main() {
  const manifestPath = path.join(__dirname, &quot;manifest.json&quot;);
  const manifest = JSON.parse(await fs.readFile(manifestPath, &quot;utf8&quot;));
  const pluginId = manifest.id;

  if (!pluginId || typeof pluginId !== &quot;string&quot;) {
    throw new Error(&quot;Unable to determine plugin id from manifest.json&quot;);
  }

  const releaseRoot = path.join(__dirname, &quot;release&quot;);
  const outputDir = path.join(releaseRoot, pluginId);

  await removeDir(outputDir);
  await ensureDir(outputDir);

  const artifacts = [
    { file: &quot;manifest.json&quot;, required: true },
    { file: &quot;main.js&quot;, required: true },
    { file: &quot;styles.css&quot;, required: false },
  ];

  for (const artifact of artifacts) {
    const source = path.join(__dirname, artifact.file);
    if (!(await fileExists(source))) {
      if (artifact.required) {
        throw new Error(
          `Missing required artifact: ${artifact.file}. Did you run npm run build?`
        );
      }
      continue;
    }

    const destination = path.join(outputDir, artifact.file);
    await copyFile(source, destination);
  }

  console.log(`Plugin packaged at ${path.relative(__dirname, outputDir)}`);
  console.log(
    &quot;Copy this folder into &amp;#x3C;vault&gt;/.obsidian/plugins to test the build.&quot;
  );
}

main().catch((error) =&gt; {
  console.error(error);
  process.exit(1);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I just moved the packaged folder into the vault to test it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mv release/&amp;#x3C;plugin_name&gt; &amp;#x3C;vault&gt;/.obsidian/plugins/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And yes — I pushed everything immediately after that.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This whole story was not about a hard technical problem.&lt;/p&gt;
&lt;p&gt;It was about how a small distraction can have a heavy cost, especially when your development setup makes it easy to delete the wrong thing.&lt;/p&gt;
&lt;p&gt;I’ll keep the plugin private for now. It’s a personal project and I don’t want to turn it into a “public product” yet.&lt;/p&gt;
&lt;p&gt;But the lesson is already public in my head:&lt;/p&gt;
&lt;p&gt;Even if you’re careful, “just one quick click” can be enough. So &lt;mark&gt;don’t let your development flow depend on luck&lt;/mark&gt;.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;the most expensive “later” I’ve ever said. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;the same trick that keeps long feeds fast. great for users, terrible for my script. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;even when I forget, my agents don’t :) &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><dc:language>en</dc:language><category>obsidian</category><category>icloud</category><category>data-recovery</category><category>workflow</category><category>plugins</category></item><item><title>SlidePlay: A Local Quiz App for the Living Room</title><link>https://akalp.co/writing/slideplay-quiz-app</link><guid isPermaLink="true">https://akalp.co/writing/slideplay-quiz-app</guid><description>A small Node.js + Socket.IO app I built to turn a TV and a few phones into a private, Kahoot-style quiz for family birthdays and living-room events.</description><pubDate>Sun, 30 Nov 2025 17:40:00 GMT</pubDate><content:encoded>&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;GitHub repo: &lt;a href=&quot;https://github.com/akalp/SlidePlay&quot;&gt;akalp/SlidePlay&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;SlidePlay is a small web app I built as &lt;mark&gt;a birthday surprise for my dad&lt;/mark&gt;. It runs only on a local network and turns a TV plus a few phones into a private quiz game.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Built with &lt;strong&gt;Node.js&lt;/strong&gt;, &lt;strong&gt;Express&lt;/strong&gt;, and &lt;strong&gt;Socket.IO&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Uses three roles: &lt;strong&gt;Player&lt;/strong&gt;, &lt;strong&gt;Host&lt;/strong&gt;, and &lt;strong&gt;Admin&lt;/strong&gt;, all running in the browser.&lt;/li&gt;
&lt;li&gt;All content comes from a single &lt;strong&gt;&lt;code&gt;slides.json&lt;/code&gt;&lt;/strong&gt; file, so new events are mostly “edit JSON, restart, play”.&lt;/li&gt;
&lt;li&gt;Supports &lt;strong&gt;content&lt;/strong&gt;, &lt;strong&gt;question&lt;/strong&gt;, &lt;strong&gt;scoreboard&lt;/strong&gt;, &lt;strong&gt;video&lt;/strong&gt;, and &lt;strong&gt;final&lt;/strong&gt; slides.&lt;/li&gt;
&lt;li&gt;Adds a scoring model based on &lt;strong&gt;correctness + speed bonus&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Implements &lt;strong&gt;remote video control&lt;/strong&gt; from the admin view (play / pause / mute / seek / restart).&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;chokidar&lt;/code&gt; to &lt;strong&gt;watch &lt;code&gt;slides.json&lt;/code&gt; and avatar assets&lt;/strong&gt;, so changes appear without restarting.&lt;/li&gt;
&lt;li&gt;In practice it feels like &lt;strong&gt;“a private Kahoot meets Google Slides, served from my laptop over Wi-Fi.”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of this post is about why I built it, what it does, and how it works under the hood.&lt;/p&gt;
&lt;h2&gt;Why I built my own tiny quiz app&lt;/h2&gt;
&lt;p&gt;I built SlidePlay because I wanted my dad’s birthday to feel a bit more interactive than “let’s click through a slideshow”.&lt;/p&gt;
&lt;p&gt;I had a few constraints in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We’d be in the &lt;strong&gt;living room&lt;/strong&gt;, with a TV, a laptop, and a bunch of phones/tablets.&lt;/li&gt;
&lt;li&gt;I wanted people to join from their &lt;strong&gt;own devices&lt;/strong&gt;, with their own names and avatars.&lt;/li&gt;
&lt;li&gt;I wanted a mix of &lt;strong&gt;photos, short videos, and quiz questions&lt;/strong&gt; about him.&lt;/li&gt;
&lt;li&gt;I didn’t want to depend on an external site or account — just &lt;strong&gt;run it on my machine&lt;/strong&gt; over the home Wi-Fi.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are plenty of existing quiz tools, but they often assume:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Classrooms or big events.&lt;/li&gt;
&lt;li&gt;Cloud accounts and public links.&lt;/li&gt;
&lt;li&gt;A generic brand/theme that doesn’t feel personal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a small family evening, that felt wrong. I wanted a mini game show that lived entirely on my laptop and on our TV.&lt;/p&gt;
&lt;p&gt;So SlidePlay started as a one-off birthday surprise that I could also reuse for future family or friend events.&lt;/p&gt;
&lt;h2&gt;What SlidePlay is&lt;/h2&gt;
&lt;p&gt;At a high level, SlidePlay is a &lt;strong&gt;real-time slideshow and quiz system&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Node.js server&lt;/strong&gt; runs on my laptop and listens on the local network.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Host view&lt;/strong&gt; runs on the TV (or any big screen) and shows slides, questions, and scoreboards.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Admin view&lt;/strong&gt; runs on my laptop and lets me control the flow: next/previous slide, reveal answers, jump to a specific slide, control video playback, reload slides.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Player views&lt;/strong&gt; run on phones and tablets; everyone joins with a name and avatar and answers questions from there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three views are standard web pages, and I keep them in sync using &lt;strong&gt;Socket.IO&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In my head, the pitch is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A small, self-hosted “Kahoot + Google Slides” hybrid that only lives on your Wi-Fi.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I don’t use a database or any external service. All state lives in memory for the duration of the session, and all content comes from a single JSON file.&lt;/p&gt;
&lt;h2&gt;How it works under the hood&lt;/h2&gt;
&lt;h3&gt;Express + Socket.IO as the real-time backbone&lt;/h3&gt;
&lt;p&gt;The backend is a single Node.js process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Express&lt;/strong&gt; serves static files from &lt;code&gt;public/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Socket.IO&lt;/strong&gt; handles live events between players, host, and admin.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On each new Socket.IO connection, I:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Send down the current state (&lt;code&gt;slides&lt;/code&gt;, &lt;code&gt;settings&lt;/code&gt;, &lt;code&gt;current slide&lt;/code&gt;, &lt;code&gt;players&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Listen for admin commands (next slide, previous slide, goto, reveal, video control).&lt;/li&gt;
&lt;li&gt;Listen for player events (join, answer).&lt;/li&gt;
&lt;li&gt;Broadcast updates to everyone (player list, slide changes, results, scoreboards).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In simplified pseudo-code, the main socket wiring looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;io.on(&quot;connection&quot;, (socket) =&gt; {
  // Initial state for a new client
  socket.on(&quot;getState&quot;, () =&gt; {
    socket.emit(&quot;state&quot;, {
      slides,
      settings,
      currentSlideId,
      players,
    });
  });

  // Player joins
  socket.on(&quot;join&quot;, ({ name, avatar }) =&gt; {
    players[socket.id] = { name, avatar, score: 0 };
    io.emit(&quot;playersUpdated&quot;, players);
  });

  // Player answers a question
  socket.on(&quot;answer&quot;, ({ slideId, choiceIndex }) =&gt; {
    handleAnswer({ socketId: socket.id, slideId, choiceIndex });
  });

  // Admin controls
  socket.on(&quot;admin:next&quot;, () =&gt; goToNextSlide());
  socket.on(&quot;admin:prev&quot;, () =&gt; goToPreviousSlide());
  socket.on(&quot;admin:goto&quot;, (slideId) =&gt; goToSlide(slideId));
  socket.on(&quot;admin:reveal&quot;, () =&gt; revealCurrentAnswer());

  // Clean up on disconnect
  socket.on(&quot;disconnect&quot;, () =&gt; {
    delete players[socket.id];
    io.emit(&quot;playersUpdated&quot;, players);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;server.js&lt;/code&gt; keeps in-memory state for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Loaded slides and settings.&lt;/li&gt;
&lt;li&gt;The current slide ID.&lt;/li&gt;
&lt;li&gt;The list of players and scores.&lt;/li&gt;
&lt;li&gt;The active question state (who has answered, who is still pending).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Slides as data: &lt;code&gt;slides.json&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;I didn’t want to rebuild the app for every event. Instead, I treat slides as pure data.&lt;/p&gt;
&lt;p&gt;All content is defined in &lt;code&gt;public/slides.json&lt;/code&gt;. At the top, there is a &lt;code&gt;settings&lt;/code&gt; object for game behavior. Then there is a &lt;code&gt;slides&lt;/code&gt; array of slide objects.&lt;/p&gt;
&lt;p&gt;A simplified version looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;settings&quot;: {
    &quot;pointsPerCorrect&quot;: 100,
    &quot;maxSpeedBonus&quot;: 50,
    &quot;speedBonusWindowSec&quot;: 10,
    &quot;revealDelayMs&quot;: 3000,
    &quot;autoAdvanceAfterReveal&quot;: false
  },
  &quot;slides&quot;: [
    {
      &quot;id&quot;: 1,
      &quot;type&quot;: &quot;content&quot;,
      &quot;title&quot;: &quot;Welcome&quot;,
      &quot;text&quot;: &quot;Happy Birthday!&quot;
    },
    {
      &quot;id&quot;: 2,
      &quot;type&quot;: &quot;video&quot;,
      &quot;title&quot;: &quot;Memory Lane&quot;,
      &quot;video&quot;: &quot;assets/video.mp4&quot;
    },
    {
      &quot;id&quot;: 3,
      &quot;type&quot;: &quot;question&quot;,
      &quot;title&quot;: &quot;Favourite dessert?&quot;,
      &quot;choices&quot;: [&quot;Option A&quot;, &quot;Option B&quot;, &quot;Option C&quot;],
      &quot;answerIndex&quot;: 1
    },
    {
      &quot;id&quot;: 4,
      &quot;type&quot;: &quot;scoreboard&quot;,
      &quot;title&quot;: &quot;Scores so far&quot;
    },
    {
      &quot;id&quot;: 5,
      &quot;type&quot;: &quot;final&quot;,
      &quot;title&quot;: &quot;Thanks for playing&quot;,
      &quot;text&quot;: &quot;🎉&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The supported slide types are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;content&lt;/code&gt; – basic text/image slides.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;video&lt;/code&gt; – video slides that I can remote-control from the admin page.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;question&lt;/code&gt; – multiple choice questions with a single correct answer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scoreboard&lt;/code&gt; – a sorted list of players and scores.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final&lt;/code&gt; – a closing slide at the end.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To build a new event, I usually just:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Replace images and videos in &lt;code&gt;public/assets/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;public/slides.json&lt;/code&gt; to match the new story.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm start&lt;/code&gt; and open the URL on the TV and phones.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I don’t have to touch JavaScript for normal content changes.&lt;/p&gt;
&lt;h3&gt;Scoring: correctness + speed bonus&lt;/h3&gt;
&lt;p&gt;I wanted the game to feel like a quiz show, not just a static poll. SlidePlay keeps a score for each player and uses a simple model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a question starts, I snapshot which players are currently connected.&lt;/li&gt;
&lt;li&gt;Each of those players can answer &lt;strong&gt;once&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A correct answer gets base points plus a speed bonus.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;A wrong answer gives zero points.&lt;/li&gt;
&lt;li&gt;When everyone in that snapshot has answered (or disconnected), the server reveals the answer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core scoring function is roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function gradeAnswer(slide, choiceIndex, elapsedMs) {
  const correct = choiceIndex === slide.answerIndex;
  if (!correct) return { correct: false, delta: 0 };

  let delta = settings.pointsPerCorrect;

  const windowMs = settings.speedBonusWindowSec * 1000;
  const clamped = Math.max(0, Math.min(windowMs, elapsedMs));
  const bonusRatio = 1 - clamped / windowMs;
  const bonus = Math.round(bonusRatio * settings.maxSpeedBonus);

  delta += bonus;
  return { correct: true, delta };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a question is done, I:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reveal the correct answer on the host and player screens.&lt;/li&gt;
&lt;li&gt;Update scores and broadcast them.&lt;/li&gt;
&lt;li&gt;Optionally auto-advance to the next slide after &lt;code&gt;revealDelayMs&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s not a complex system, but it’s enough to create some friendly competition.&lt;/p&gt;
&lt;h3&gt;Three browser roles: Player, Host, Admin&lt;/h3&gt;
&lt;p&gt;SlidePlay splits the UI into three separate pages with different responsibilities.&lt;/p&gt;
&lt;h4&gt;Player (phones and tablets)&lt;/h4&gt;
&lt;p&gt;The player view (&lt;code&gt;/&lt;/code&gt;) is what everyone uses on their phones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Join with a &lt;strong&gt;name and avatar&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;See the current slide in a simple layout.&lt;/li&gt;
&lt;li&gt;For questions, tap one of the answer buttons.&lt;/li&gt;
&lt;li&gt;After reveal, see whether the answer was correct and how many points they got.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Host (TV or projector)&lt;/h4&gt;
&lt;p&gt;The host view (&lt;code&gt;/host.html&lt;/code&gt;) is what I put on the TV:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Show the current slide in a large, clean format.&lt;/li&gt;
&lt;li&gt;For questions, highlight the correct choice after reveal and list who answered what (with timing).&lt;/li&gt;
&lt;li&gt;For scoreboard slides, show a sorted list of players and scores.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the “stage” view everyone in the room watches.&lt;/p&gt;
&lt;h4&gt;Admin (laptop, tablet or phone)&lt;/h4&gt;
&lt;p&gt;The admin view (&lt;code&gt;/admin.html&lt;/code&gt;) is my control panel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Buttons for &lt;strong&gt;Previous&lt;/strong&gt;, &lt;strong&gt;Next&lt;/strong&gt;, and &lt;strong&gt;Goto&lt;/strong&gt; slide.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Force Reveal&lt;/strong&gt; button to reveal early.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Reload JSON&lt;/strong&gt; button to re-read &lt;code&gt;slides.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remote video controls (Play, Pause, Restart, Mute/Unmute, Seek).&lt;/li&gt;
&lt;li&gt;A small live scoreboard panel.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under the hood, all three views are basic Socket.IO clients listening to a subset of events they care about.&lt;/p&gt;
&lt;h3&gt;Remote video control&lt;/h3&gt;
&lt;p&gt;I wanted to mix in short videos (old clips, fun moments) without losing control from the admin panel.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;video&lt;/code&gt; slides, I treat the admin as a remote control for the host:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Admin asks to control the video
socket.emit(&quot;admin:videoControl&quot;, { action: &quot;play&quot; });

// Server forwards to host clients
socket.on(&quot;admin:videoControl&quot;, (payload) =&gt; {
  if (!isValid(payload)) return;
  io.to(&quot;host&quot;).emit(&quot;host:videoControl&quot;, payload);
});

// Host applies the action
socket.on(&quot;host:videoControl&quot;, ({ action, time }) =&gt; {
  const video = document.getElementById(&quot;hostVideo&quot;);
  if (!video) return;

  switch (action) {
    case &quot;play&quot;:
      video.play();
      break;
    case &quot;pause&quot;:
      video.pause();
      break;
    case &quot;restart&quot;:
      video.currentTime = 0;
      video.play();
      break;
    case &quot;mute&quot;:
      video.muted = true;
      break;
    case &quot;unmute&quot;:
      video.muted = false;
      break;
    case &quot;seek&quot;:
      video.currentTime = time;
      break;
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the player view, I only show the text for the video slide. The actual playback happens on the host screen, which keeps everyone focused on the TV.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A small practical note here: Chrome’s autoplay rules matter. Muted autoplay is usually fine, but autoplay with sound is only allowed after the user has interacted with the site (click, tap, etc.), or if Chrome considers the user “engaged” with that domain, or the site is installed as a PWA In other words, if I try to call video.play() from a Socket.IO event before anyone has clicked anything on the host page, Chrome may silently block it. In practice I either start videos muted, or I require at least one manual click on the host view before I rely on remote play commands.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Avatars and live reload with &lt;code&gt;chokidar&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;For joining, I wanted something more fun than plain text names, so I added avatars.&lt;/p&gt;
&lt;p&gt;Avatars are just images in &lt;code&gt;public/assets/avatars/&lt;/code&gt;. On startup, the server scans that directory and builds a list of available avatar URLs. Players see this list and tap one to choose it.&lt;/p&gt;
&lt;p&gt;To avoid restarts while tweaking the event, I also wired in &lt;strong&gt;&lt;code&gt;chokidar&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;chokidar.watch(&quot;public/slides.json&quot;).on(&quot;change&quot;, () =&gt; {
  loadSlidesFromDisk();
  io.emit(&quot;slidesReloaded&quot;, { slides, settings });
});

if (fs.existsSync(AVATAR_DIR)) {
  chokidar
    .watch(AVATAR_DIR, { ignoreInitial: true })
    .on(&quot;add&quot;, loadAvatars)
    .on(&quot;unlink&quot;, loadAvatars);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Live slide reload when I edit &lt;code&gt;slides.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Live avatar updates when I add/remove avatar images.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a real event, this is great: I can fix typos, tweak wording, or swap an image a few minutes before we start, without touching the Node.js process.&lt;/p&gt;
&lt;h2&gt;The first living-room test&lt;/h2&gt;
&lt;p&gt;The first real test was on my home network:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I ran the Node.js server on my laptop.&lt;/li&gt;
&lt;li&gt;I opened the &lt;strong&gt;host view&lt;/strong&gt; in a browser and mirrored it to the TV.&lt;/li&gt;
&lt;li&gt;Five people joined as &lt;strong&gt;players&lt;/strong&gt; from phones and a tablet over Wi-Fi.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The funniest bug was not in my code but in the browser: I shared the local URL, people tapped it, and Chrome tried to be “smart” and turned it into &lt;code&gt;https://...&lt;/code&gt;. Of course, my small Node server was only listening on plain &lt;code&gt;http://&lt;/code&gt;, so nothing worked at first.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Once we realized what was going on and everyone typed the &lt;code&gt;http://&lt;/code&gt; URL manually, everything clicked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Players picked avatars and names.&lt;/li&gt;
&lt;li&gt;I walked through content, video, question, scoreboard, and final slides from the admin panel.&lt;/li&gt;
&lt;li&gt;Scores updated in real time, and the scoreboard slides made the quiz feel real.&lt;/li&gt;
&lt;li&gt;Most importantly, my dad got a custom, interactive birthday quiz built around his own story.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of it stayed on the local network. No public link, no external service, no uploads.&lt;/p&gt;
&lt;h2&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;SlidePlay is not a product I plan to ship. It’s a &lt;strong&gt;small template&lt;/strong&gt; that does one thing well:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Turn a TV and a few phones into a private quiz show that lives entirely on your laptop.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;From a technical side, the interesting parts for me were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using &lt;strong&gt;Express + Socket.IO&lt;/strong&gt; as a simple real-time backbone.&lt;/li&gt;
&lt;li&gt;Treating slides as &lt;strong&gt;data in &lt;code&gt;slides.json&lt;/code&gt;&lt;/strong&gt;, not hard-coded components.&lt;/li&gt;
&lt;li&gt;Implementing a small but satisfying &lt;strong&gt;scoring system&lt;/strong&gt; (correctness + speed).&lt;/li&gt;
&lt;li&gt;Splitting the UI into &lt;strong&gt;Player / Host / Admin&lt;/strong&gt; roles.&lt;/li&gt;
&lt;li&gt;Adding &lt;strong&gt;remote video control&lt;/strong&gt; and &lt;strong&gt;live reload&lt;/strong&gt; with &lt;code&gt;chokidar&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever wanted a private “Kahoot meets Google Slides” for your own family, friends, or a small team, this pattern works well: &lt;mark&gt;keep everything local, keep the content in one JSON file&lt;/mark&gt;, and use WebSockets to glue a few simple web pages into something that feels like a live show in your living room.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;the speed bonus made everyone very competitive, very fast. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;next time: a QR code with the full http:// URL on the TV. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;the night he understood my job best, and he was really happy. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><dc:language>en</dc:language><category>nodejs</category><category>socketio</category><category>realtime</category><category>side-project</category><category>web</category></item><item><title>My First Oh-My-Zsh Plugin: caff</title><link>https://akalp.co/writing/my-first-omz-plugin-caff</link><guid isPermaLink="true">https://akalp.co/writing/my-first-omz-plugin-caff</guid><description>My first Oh My Zsh plugin: a smart wrapper around macOS caffeinate to keep long-running builds, tests, and AI-assisted workflows awake without Amphetamine headaches.</description><pubDate>Wed, 26 Nov 2025 18:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;GitHub repo: &lt;a href=&quot;https://github.com/akalp/caff&quot;&gt;akalp/caff&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As developers we constantly run long builds, tests, containers and AI-assisted workflows that easily outlive macOS’s default sleep behavior.&lt;/li&gt;
&lt;li&gt;Amphetamine is convenient from the menu bar, but especially in clamshell mode its sessions can end abruptly and push the machine toward sleep at awkward moments.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;caffeinate&lt;/code&gt; is scriptable and process-scoped, but you have to remember flag combos like &lt;code&gt;-disu&lt;/code&gt;, think in raw seconds via &lt;code&gt;-t &amp;#x3C;seconds&gt;&lt;/code&gt;, and it gives you no visual feedback.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;caff&lt;/code&gt; is my first Oh My Zsh plugin that wraps &lt;code&gt;caffeinate&lt;/code&gt; with nicer ergonomics: &lt;code&gt;caff disu 2h -- npm test&lt;/code&gt;, duration presets (&lt;code&gt;short&lt;/code&gt; / &lt;code&gt;medium&lt;/code&gt; / &lt;code&gt;long&lt;/code&gt; / &lt;code&gt;night&lt;/code&gt;), human-friendly units (&lt;code&gt;2h&lt;/code&gt;, &lt;code&gt;30m&lt;/code&gt;), an inline countdown or spinner, quiet mode, and clean exit-code propagation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For years I treated &quot;keeping my Mac awake&quot; as an annoying detail I only thought about when something &lt;em&gt;broke&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A long test suite died halfway through because the machine went to sleep.&lt;/li&gt;
&lt;li&gt;A big Docker image build got interrupted.&lt;/li&gt;
&lt;li&gt;An AI-assisted refactor or multi-agent workflow was still running when I left the desk — and silently paused when the screen turned off.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you do any kind of serious development on macOS, there&apos;s a good chance you&apos;ve run into the same thing: you start a long-running task, step away for a bit, and come back to discover that &lt;strong&gt;sleep won the race&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This blog post is about how I tried to solve that with Amphetamine, why that wasn&apos;t quite enough for my setup (especially in &lt;strong&gt;clamshell mode&lt;/strong&gt;&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;), and how that annoyance pushed me to write my first Oh My Zsh plugin: &lt;code&gt;caff&lt;/code&gt;, a smarter wrapper around macOS&apos; built‑in &lt;code&gt;caffeinate&lt;/code&gt; command.&lt;/p&gt;
&lt;h2&gt;Why a developer needs to keep the machine awake&lt;/h2&gt;
&lt;p&gt;Modern development is full of tasks that take longer than a coffee break:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Long builds&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frontend bundles in complex monorepos&lt;/li&gt;
&lt;li&gt;Native iOS/Android builds&lt;/li&gt;
&lt;li&gt;CI-like local scripts that run multiple steps&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Heavy test runs&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full integration or end-to-end test suites&lt;/li&gt;
&lt;li&gt;Load tests or benchmarks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Dev tooling and environments&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker image builds and multi-container setups&lt;/li&gt;
&lt;li&gt;Local Kubernetes clusters&lt;/li&gt;
&lt;li&gt;Database migrations and seed scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AI-assisted workflows&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Long-running codegen or refactor jobs&lt;/li&gt;
&lt;li&gt;Background documentation generation&lt;/li&gt;
&lt;li&gt;Multi-agent systems orchestrating many small steps&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern is the same: you run something that might take 10, 30, 60+ minutes. You &lt;em&gt;don&apos;t&lt;/em&gt; want to sit and wiggle the mouse every five minutes to keep the Mac awake. You probably want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kick off the task&lt;/li&gt;
&lt;li&gt;Maybe glance at logs&lt;/li&gt;
&lt;li&gt;Walk away, grab coffee, do something else&lt;/li&gt;
&lt;li&gt;Come back to a finished job — not a half-baked one paused in sleep&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On top of that, if you&apos;re using &lt;strong&gt;AI tooling&lt;/strong&gt; heavily, you might:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trigger a long refactor on a large codebase&lt;/li&gt;
&lt;li&gt;Ask an agent to generate many tests or docs&lt;/li&gt;
&lt;li&gt;Let a script iterate over files and call an API many times&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In those scenarios, you might &lt;em&gt;not&lt;/em&gt; be actively typing the whole time. The system sees &quot;no input&quot; and assumes it&apos;s safe to sleep, even though a lot of important work is still in progress.&lt;/p&gt;
&lt;p&gt;So we need something that tells macOS: &lt;strong&gt;&quot;Stay awake intentionally while I&apos;m doing this, then go back to normal.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Amphetamine and &lt;code&gt;caffeinate&lt;/code&gt;: the usual suspects&lt;/h2&gt;
&lt;p&gt;On macOS, there are two main approaches people use:&lt;/p&gt;
&lt;h3&gt;Amphetamine&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://apps.apple.com/us/app/amphetamine/id937984704&quot;&gt;Amphetamine&lt;/a&gt; is a popular macOS app for preventing sleep. It gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A nice &lt;strong&gt;menu bar UI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Presets like &quot;stay awake for X hours&quot; or &quot;while this app is running&quot;&lt;/li&gt;
&lt;li&gt;Quick toggles without touching the terminal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For many users, that&apos;s more than enough. Click, pick a duration, done.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;caffeinate&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Under the hood, macOS also ships a CLI tool:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;caffeinate [options] [command]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This utility can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prevent &lt;strong&gt;display&lt;/strong&gt;, &lt;strong&gt;idle&lt;/strong&gt;, &lt;strong&gt;system&lt;/strong&gt; sleep, or combinations&lt;/li&gt;
&lt;li&gt;Run for a fixed duration via &lt;code&gt;-t &amp;#x3C;seconds&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Stay awake &lt;strong&gt;while a specific command is running&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# Prevent system sleep for 1 hour
caffeinate -t 3600

# Prevent display + idle sleep while running tests
caffeinate -d -i -- npm test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s powerful and scriptable — but the UX is… very raw:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have to remember flag combinations (&lt;code&gt;-d -i -s -u&lt;/code&gt; etc.)&lt;/li&gt;
&lt;li&gt;Durations must be in seconds&lt;/li&gt;
&lt;li&gt;No visual feedback or countdown&lt;/li&gt;
&lt;li&gt;No handy presets like &quot;short&quot;, &quot;long&quot;, or &quot;overnight&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Still, as a developer living in the terminal, &lt;code&gt;caffeinate&lt;/code&gt; feels like the right primitive to build on.&lt;/p&gt;
&lt;h2&gt;The Amphetamine + clamshell problem&lt;/h2&gt;
&lt;p&gt;For a while, I used Amphetamine as my main sleep blocker. It mostly worked — &lt;em&gt;until&lt;/em&gt; I combined it with &lt;strong&gt;clamshell mode&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;My setup looks roughly like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MacBook Pro in clamshell&lt;/li&gt;
&lt;li&gt;External monitor(s)&lt;/li&gt;
&lt;li&gt;External keyboard + mouse&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In that setup, Amphetamine sessions behave a bit differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You start a timed session, e.g. &lt;strong&gt;2 hours&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You close the lid, work on the external display, everything is fine.&lt;/li&gt;
&lt;li&gt;When the &lt;strong&gt;timer ends&lt;/strong&gt; or you &lt;strong&gt;manually stop&lt;/strong&gt; the session, macOS immediately goes back to its default sleep behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The annoying part: in clamshell mode, that usually means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The external screen goes dark and the machine basically wants to sleep &lt;em&gt;right now&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This leads to a couple of UX problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you &lt;strong&gt;end&lt;/strong&gt; an Amphetamine session (or it expires) while still in clamshell, you can be kicked out of what you were doing.&lt;/li&gt;
&lt;li&gt;If you &lt;strong&gt;forget&lt;/strong&gt; the session is about to end, you might lose context or ongoing work on the external display.&lt;/li&gt;
&lt;li&gt;Managing sessions starts to feel brittle: &quot;Will my Mac instantly go to sleep if I hit stop?&quot; is not a good mental model while you&apos;re in the middle of a task.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For short tasks this is tolerable. But when you&apos;re constantly running builds, tests, and AI-assisted workflows, you don&apos;t want the &lt;strong&gt;sleep policy&lt;/strong&gt; to surprise you.&lt;/p&gt;
&lt;p&gt;I wanted something more &lt;strong&gt;predictable&lt;/strong&gt; and more &lt;strong&gt;terminal-native&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Why &lt;code&gt;caffeinate&lt;/code&gt; is a better fit for my workflow&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;caffeinate&lt;/code&gt; has a few built-in advantages if you live in the shell:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Process-scoped sessions&lt;/strong&gt;
You can say &quot;stay awake &lt;strong&gt;exactly&lt;/strong&gt; as long as this command runs&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;caffeinate -d -i -- npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the build is done, the process exits and the system returns to normal. There&apos;s no separate &quot;session&quot; to remember to stop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scriptability&lt;/strong&gt;
You can bake it into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shell aliases and functions&lt;/li&gt;
&lt;li&gt;Project scripts (&lt;code&gt;package.json&lt;/code&gt;, Makefiles, etc.)&lt;/li&gt;
&lt;li&gt;CI-like local runners&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fine-grained control&lt;/strong&gt;
You can choose exactly which sleep sources to block:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; — display sleep&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt; — idle sleep&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-s&lt;/code&gt; — system sleep&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-u&lt;/code&gt; — declare user is active&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Works well with clamshell mode&lt;/strong&gt;
When used with the right flags and/or durations, &lt;code&gt;caffeinate&lt;/code&gt; is just another process. It doesn&apos;t care whether the lid is open or closed — if it&apos;s running, it will keep the system awake according to the flags you gave it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In other words, &lt;code&gt;caffeinate&lt;/code&gt; behaves like a &lt;mark&gt;tool you can compose with other tools&lt;/mark&gt;. It feels like a better foundation for a dev-centric workflow than a mouse-driven menu bar app.&lt;/p&gt;
&lt;p&gt;The only issue: its &lt;strong&gt;ergonomics&lt;/strong&gt; are not great. Even though you can pack flags as &lt;code&gt;-disu&lt;/code&gt;, it still expects raw seconds via &lt;code&gt;-t &amp;#x3C;seconds&gt;&lt;/code&gt; and gives you no visual feedback.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I don&apos;t want to think in seconds.&lt;/li&gt;
&lt;li&gt;I&apos;d like some quick presets like &lt;code&gt;short&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;night&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I&apos;d like a simple visual indication when I&apos;m in &quot;awake mode&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I did what any annoyed developer would do: I wrote a wrapper.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Introducing &lt;code&gt;caff&lt;/code&gt;: a smart &lt;code&gt;caffeinate&lt;/code&gt; wrapper&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;caff&lt;/code&gt; is my first Oh My Zsh plugin — a small Zsh function that turns &lt;code&gt;caffeinate&lt;/code&gt; into something I actually enjoy using.&lt;/p&gt;
&lt;p&gt;At a high level, it gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shorthand flags using &lt;strong&gt;&lt;code&gt;disu&lt;/code&gt; syntax&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Human-friendly durations like &lt;code&gt;2h&lt;/code&gt;, &lt;code&gt;30m&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Named presets (&lt;code&gt;short&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;night&lt;/code&gt;/&lt;code&gt;overnight&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Optional command execution via &lt;code&gt;--&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;An interactive &lt;strong&gt;countdown bar&lt;/strong&gt; or &lt;strong&gt;spinner&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Start/finish messages, quiet mode, and proper exit codes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;A nicer mental model&lt;/h3&gt;
&lt;p&gt;Instead of this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# Using caffeinate directly
# (flags must include a leading dash, duration is in seconds)
caffeinate -disu -t 7200 -- npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can now write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;caff disu 2h -- npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;disu&lt;/code&gt; → &lt;code&gt;-d -i -s -u&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2h&lt;/code&gt; → &lt;code&gt;-t 7200&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The plugin also defines a short alias, so this works too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;cf disu 2h -- npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Human durations and presets&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;caff&lt;/code&gt; understands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;2h&lt;/code&gt; → 2 hours&lt;/li&gt;
&lt;li&gt;&lt;code&gt;30m&lt;/code&gt; → 30 minutes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;t=3600&lt;/code&gt; → raw seconds, like &lt;code&gt;-t 3600&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plus named presets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;short&lt;/code&gt; → 15 minutes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;medium&lt;/code&gt; → 1 hour&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long&lt;/code&gt; → 3 hours&lt;/li&gt;
&lt;li&gt;&lt;code&gt;night&lt;/code&gt; / &lt;code&gt;overnight&lt;/code&gt; → 8 hours&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So common patterns become very readable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# Short session while you grab a coffee
caff di short

# Keep the machine fully awake for the next 3 hours
caff disu long

# Overnight job with tests
caff disu night -- yarn test:e2e
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Command or no command&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;caff&lt;/code&gt; adjusts its behavior depending on whether you pass a command after &lt;code&gt;--&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;With a command&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;caff disu 1h -- npm test
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Runs &lt;code&gt;caffeinate&lt;/code&gt; + &lt;code&gt;npm test&lt;/code&gt; together&lt;/li&gt;
&lt;li&gt;Prints a start message with flags and duration&lt;/li&gt;
&lt;li&gt;No interactive UI&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;npm test&lt;/code&gt; finishes, &lt;code&gt;caff&lt;/code&gt; exits with the &lt;em&gt;same&lt;/em&gt; status code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes it safe to use in scripts and automations.&lt;/p&gt;
&lt;h4&gt;Without a command&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# Duration known → countdown bar
caff disu 45m

# Duration unknown → spinner
caff disu
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Known duration&lt;/strong&gt;: &lt;code&gt;caff&lt;/code&gt; starts &lt;code&gt;caffeinate&lt;/code&gt; in the background and shows a single-line progress bar with remaining time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unknown duration&lt;/strong&gt;: it shows a little spinner that keeps animating until you hit Ctrl+C.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In both cases, &lt;strong&gt;Ctrl+C&lt;/strong&gt; stops the UI and &lt;code&gt;caffeinate&lt;/code&gt;, and &lt;code&gt;caff&lt;/code&gt; exits with status &lt;code&gt;130&lt;/code&gt;.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; That makes it explicit that the session was interrupted.&lt;/p&gt;
&lt;h3&gt;Quiet mode for scripts&lt;/h3&gt;
&lt;p&gt;Sometimes you want zero noise — especially in scripts or logs. For that, there&apos;s a quiet mode:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;caff --quiet disu long -- ./deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or equivalently:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;caff -q disu long -- ./deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;No UI&lt;/li&gt;
&lt;li&gt;No start/finish messages&lt;/li&gt;
&lt;li&gt;Same exit-code propagation behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why this helps my clamshell workflow&lt;/h3&gt;
&lt;p&gt;In clamshell mode, my usage now looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;For a long-running command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;cf disu medium -- pnpm lint &amp;#x26;&amp;#x26; pnpm test
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;External monitor stays awake while the command runs.&lt;/li&gt;
&lt;li&gt;When it finishes, &lt;code&gt;caff&lt;/code&gt; exits, the system goes back to normal.&lt;/li&gt;
&lt;li&gt;No surprise sleep &lt;em&gt;in the middle&lt;/em&gt; of a task.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For a &quot;manual&quot; session:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;cf disu night
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;I get a countdown for 8 hours.&lt;/li&gt;
&lt;li&gt;If I’m done earlier, I hit Ctrl+C.&lt;/li&gt;
&lt;li&gt;If I forget, it ends after the preset duration and the system behaves normally again.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s predictable, scriptable, and — most importantly — lives where I already spend my time: in the terminal.&lt;/p&gt;
&lt;h2&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Writing &lt;code&gt;caff&lt;/code&gt; started as a tiny quality-of-life improvement: &lt;em&gt;&quot;I just want a nicer way to use &lt;code&gt;caffeinate&lt;/code&gt;.&quot;&lt;/em&gt; But it ended up being my:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First &lt;strong&gt;Oh My Zsh plugin&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;First little experiment in “&lt;mark&gt;turn an annoyance into a tool&lt;/mark&gt;”&lt;/li&gt;
&lt;li&gt;A reminder that many dev problems are solvable with a thin layer of ergonomics on top of existing primitives&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Work in clamshell mode&lt;/li&gt;
&lt;li&gt;Run long builds/tests/AI tasks&lt;/li&gt;
&lt;li&gt;Live in the terminal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…then &lt;code&gt;caffeinate&lt;/code&gt; is probably already the right tool for you. &lt;code&gt;caff&lt;/code&gt; just makes it &lt;strong&gt;nicer&lt;/strong&gt;.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;lid closed, external monitor, keyboard on the desk. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;even the name felt long. I went with caff, that felt long too, so I added a cf alias. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;128 + SIGINT (2). scripts can tell “interrupted” from “failed”. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><dc:language>en</dc:language><category>cli-tools</category><category>productivity</category></item><item><title>Yatay geçiş mi? O da ne?</title><link>https://akalp.co/writing/yatay-gecis</link><guid isPermaLink="true">https://akalp.co/writing/yatay-gecis</guid><description>İstediğin bölümünde veya üniversitede değil misin? Dert etme, hala bir şansın var.</description><pubDate>Sun, 26 Dec 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Evet üniversitedesin ama o da ne...&lt;/h2&gt;
&lt;p&gt;Burası hiç de senin beklediğin gibi değil 😬 Bu dert değil, hala istediğin bölüm veya üniversite için bir şansın var: Yatay geçiş. Peki neyin nesi bu, nasıl yapılır ve ne zaman yapabilabilir? Hadi hepsini cevaplayalım 😇&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Yatay Geçiş?&lt;/h2&gt;
&lt;p&gt;Yatay geçiş, üniversite sınav sonucuna veya not ortalamana göre üniversiteni, bölümünü veya her ikisini birden değiştirebildiğin bir uygulama aslında. Öğrenci için bir çıkış noktası diyebiliriz. Üniversiteden veya bölümden memnun kalmadıysan değiştirebilmen için bir yol. ✌🏻&lt;/p&gt;
&lt;h2&gt;Nasıl yapılır?&lt;/h2&gt;
&lt;p&gt;İki türlü geçiş var:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kurumlar arası&lt;/li&gt;
&lt;li&gt;Kurum içi&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ayrıca üst paragrafta belirttiğim gibi iki farklı şekilde geçiş yapılabiliyor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Üniversite sınavı sonucuna göre&lt;/li&gt;
&lt;li&gt;Genel not ortalana göre&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Kurum içi&lt;/h3&gt;
&lt;p&gt;Burayı okuyorsan üniversite tam sana göre ama bölüm tercihin yanlış demektir. Üzülmeye gerek yok 😇 Mevcut bölümünle eşdeğer düzeydeki bir diploma programına eğer sınav puan türü de aynı ise üniversitenin yönetim kurulunun belirlediği kontejanlar dahilinde yatay geçiş yapabilirsin. Burada yönetim kurulu bazı şartlar koyabiliyor. Takip etmende fayda var 😉 Geçmek istediğin bölüm farklı puan türüne sahip ise ilgili puan türünde yaptığın üniversite puanının geçmek istediğin bölümün sınav senendeki puanına eşdeğer veya yüksek olması gerekiyor. Eğer şartı sağlıyorsan bekleme ve başvur 🏅&lt;/p&gt;
&lt;h3&gt;Kurumlar arası&lt;/h3&gt;
&lt;p&gt;&quot;Bu üniversite de pek benlik değilmiş, değiştirebilir miyim acaba 🤔&quot; dediğini duyar gibiyim. Elbette değiştirebilirsin. Burada önemli olan hedefin. Üniversite değiştirmek istiyorsan eğer hem genel ortalaman ile hem de üniversite puanın ile geçiş yapabilirsin. Yok ben bölüm de değiştireceğim diyorsan burada kötü bir haberim var, sadece üniversite puanını kullanabilirsin 😢&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Üniversitem değişsin ama bölümüm aynı kalsın:&lt;/strong&gt; Üniversite sınavındaki puanın, geçmek istediğin üniversitedeki bölümünün sınav senendeki puanına eşdeğer veya o puandan yüksekse direkt başvurabilirsin. Bu şekilde birçok arkadaşım üniversite değişikliği yaptı,&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; bu sayede bulunduğun üniversitede ortalama kasmak zorunda kalmıyorsun. Bir öğrenci bundan başka ne isteyebilir ki 😏&lt;/p&gt;
&lt;p&gt;Eee, üniversite puanın üst paragraftaki şartı sağlamıyorsa ne olacak peki? 🤔 Burada cevap genel not ortalamanda saklı. Eğer genel not ortalaman geçmek istediğin üniversitenin belirlediği değere eşdeğerse veya puanın bu değerden yüksekse üzülme sen de başvurabilirsin 👑&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Üniversite yetmez, bölümüm de değişsin:&lt;/strong&gt; Burada genel not ortalaması çalışmıyor maalesef 🙁 Yukarıda bahsettiğim üniversite puanı yolu buradaki tek yol. Geçmek istediğin bölümün sınava girdiğin senedeki puanına denk veya o puandan daha fazlasına sahip olman lazım.&lt;/p&gt;
&lt;h2&gt;Ne zaman peki?&lt;/h2&gt;
&lt;p&gt;&lt;mark&gt;Henüz son iki sınıftan birinde değilsen ve birinci yılını tamamlamışsan&lt;/mark&gt; şanslısın, bu uygulamadan faydalanabilirsin. 🥇&lt;/p&gt;
&lt;h2&gt;Peki başka ne şartlar var?&lt;/h2&gt;
&lt;p&gt;Eğer geçmek istediğin bölüm İngilizce ise şimdiki bölümünün de İngilizce olması gerekiyor ayrıca geçmek istediğin üniversite senden İngilizce sınav belgesi isteyecektir. Şimdiye kadar sadece İzmir Yüksek Teknoloji Enstitüsü&apos;nün istemediğini duydum, onun dışında bildiğim tüm üniversiteler TOEFL gibi sınavların sonucunu istiyor ya da kendi İngilizce sınavlarından belli bir puanı almanı istiyor.&lt;/p&gt;
&lt;p&gt;Ayrıca, &lt;mark&gt;dönem kaybının olmaması gerekiyor&lt;/mark&gt;. Dönem kaybın varsa kabul alma şansın yoka yakın diyebiliriz. 🥲&lt;/p&gt;
&lt;h2&gt;Ee nasıl geçtiğimi anlayacağım?&lt;/h2&gt;
&lt;p&gt;Üniversitelerin belli bir yatay geçiş kontejanı oluyor. Sonuçları da bu kontejana göre açıkılıyorlar, yedeklerle birlikte. Eğer bu kontejana girebilirsen tebrikler, artık bölümünü ve/veya üniversiteni değiştirdin 🥇 Yedeklerdeysen de üzülme, asıl adaylar planlarını değiştirmiş ve/veya başka üniversiteden/bölümden kabul almış olabilirler, sakince bir sonraki duyuruyu takip et. 👀&lt;/p&gt;
&lt;p&gt;Kontejana dahil olma şansını artırmak için genel not ortalamanı yüksek tutman gerektiğini dipnot olarak düşmek istiyorum. Bunun başka yolu yok maalesef ama idealler uğruna bunu göze almalıyız, değil mi? 😊&lt;/p&gt;
&lt;h2&gt;Önceki derslerime ne olacak?&lt;/h2&gt;
&lt;p&gt;Gittiğin yerde ortak dersler varsa bu derslerini sayacaklar zaten, rahat ol 🙂 Peki farklı olan dersler? Burada bir şart var. Eğer farklı olan ders eski üniversitende/bölümünde zorunlu ders ise seçmeli ders olarak saydırabiliyorsun ama eğer seçmeli bir ders ise maalesef bu dersi saydıramıyorsun. Üniversitenin bu konuda rahatlık sunuyorsa ona bir şey diyemem tabi 😊 Araştırmak lazım bunları hep 🤓&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;2021’de yazdım; şartlar değişmiş olabilir, başvurmadan önce güncel yönetmeliğe bak. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;ben de bu yoldan geçtim. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><dc:language>tr</dc:language><category>universite</category></item><item><title>Bilgisayar Mühendisliği Kazandım, Ne Yapmalıyım?</title><link>https://akalp.co/writing/cs-okumak</link><guid isPermaLink="true">https://akalp.co/writing/cs-okumak</guid><description>Bir bilgisayar mühendisliği mezunu olarak nacizane önerilerim</description><pubDate>Thu, 23 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Bölümü kazanmak&lt;/h2&gt;
&lt;p&gt;Tebrikler! Zoru başardın ve üniversite kazandın 💪 Özellikle de istediğin bölümü kazanmanın verdiği haz gerçekten çok güzel. Tercih listemi hatırlıyorum da tamamen bilgisayar mühendisliği ve elektrik-elektronik mühendisliğinden oluşuyordu. Elektrik-elektronik mühendisliğini de çevremdeki bazı yazılımcıların bu bölümden mezun olmasından dolayı yazmıştım, tabii dönemimdeki popülaritesi de etkiliydi. Ama tam da istediğim gibi bilgisayar mühendisliğine yerleşmiştim. Peki bölüm tamam ama ya üniversite? Burada güzel bir haberim var, hala istediğin üniversiteye gitmek için şansın var. Ben de istediğim üniversiteyi kazanamamıştım fakat yatay geçiş yaparak istediğim üniversiteye gitmeye başladım. Yatay geçiş konusu ile ilgili yazımı da yakında paylaşacağım,&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; ara ara sosyal medya hesaplarımı ve burayı kontrol etmeyi unutma 😉&lt;/p&gt;
&lt;h2&gt;Kazandın, peki şimdi?&lt;/h2&gt;
&lt;p&gt;Kısaca bazı tavsiyelerde bulunacağım. Bir şeyler eklemek isteyenler veya yok arkadaş olmaz öyle iş diyecekler de benimle istedikleri gibi iletişime geçebilirler 😊&lt;/p&gt;
&lt;p&gt;Hadi başlayalım 🤘&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bilgisayar. Hemen gidip en güçlü bilgisayarı almalısın (!). Tabii ki böyle bir şeye ihtiyacın yok! Genel yapılan yanlışlardan biri asla ihtiyacın olmayacak güçte bir bilgisayara sahip olmaya çalışmak. Günümüz Türkiye şartlarında bilgisayarlar genel olarak pahalı cihazlar&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; fakat olabildiğince fiyat/performans bir bilgisayar işini fazlasıyla görecektir.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; Eğer bir bilgisayara sahipsen de bekle ve üniversitedeki durumunu inceleyerek bu alımı gerçekleştir.&lt;/li&gt;
&lt;li&gt;Yabancı dil. İngilizce alanımız için çok önemli. Eğer üniversitende hazırlık seçmeli ise mutlaka seç. Yabancı kaynaklar alanımızda vazgeçilmez. Tabii sadece hazırlık da yetmiyor, bolca içerik tüketmek İngilizce becerilerini geliştirmen için en gerekli şey.&lt;/li&gt;
&lt;li&gt;Kariyer. Kariyerinde nasıl ilerlemek istediğine erken dönemde karar vermek faydalı olacaktır. Bu karar bölümdeki 4 senende yol haritanı çıkarmak için vazgeçilmez bir unsur. Eğer buna karar vermezsen son sınıfta veya 4 senenin sonunda &quot;Eee şimdi ben ne yapacağım?&quot; derken bulabilirsin kendini. Ayrıca erken davranmak daha sonra seçimini sorgulaman ve gerekirse değiştirebilmen için de sana vakit tanıyacak. Şimdi kariyer hedefi kısmını ikiye böleceğim:
&lt;ol&gt;
&lt;li&gt;Akademi. Akademik ilerleme konusu biraz karmaşık. Hangi alanda yapmak istediğin konusunda başlangıçta fikrin olmayabilir. Bu durumda 3. sınıfta alacağın teknik dersler seçim konusunda sana yardımcı olabilir. Ayrıca bu alanda ilerlemek istiyorsan notlarına oldukça dikkat etmen gerekiyor. Burada verebileceğim nacizane öneri alanını seçtikten sonra bölümünde o alanda çalışan hoca ile görüşmen ve mümkünse hocanın yanında lisans araştırmacısı olarak çalışmaya başlaman. Bu sana çok fazla şey kazandıracak emin olabilirsin. Hocan ile birlikte yapacağın akademik çalışmalar da yurt dışındaki üniversitelere açılan kapın olabilir 😉 Eee sonradan karar değiştirdin ne olacak? Hiçbir şey! Akademik olarak kazandığın bilgiler sektörde de işine yarayabilir. Akademik ilerlemek sektör bilgisi olarak geride kalmak değildir 🤘&lt;/li&gt;
&lt;li&gt;Sektör. Bu durumda da tercih yapman gerekecek elbette. Seçebileceğin alan oldukça fazla. Şu an için aklıma gelenler: Backend, Frontend, Mobil, DevOps, Cyber Security, Gömülü Sistemler, Test, vb. Tam olarak neyden hoşlandığını elbette bilemezsin, bunun için biraz araştırmalısın. Bu alanlarda önde gelen kişileri takip edebilir ve İnternet alemindeki sayısız projeyi inceleyebilirsin. Peki alanımı seçtim, şimdilik, peki ya sonra? Basitten başlayarak proje üretimine başlayabilirsin, tabii bunları GitHub, GitLab, BitBucket gibi servislerde yayınlamayı da unutma. Bunlar senin en büyük referansların olacak.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; Bir diğer önerim de çevre edinebileceğin etkinliklere katılman, çevre demek fikir paylaşabileceğin, ortak çalışabileceğin kişiler demek. Unutma, &lt;mark&gt;insan çevresindekilerin ortalamasıdır&lt;/mark&gt; 💪 Bu arada unutmadan staj ve stajda yaptıkların da dikkate alınabiliyor. Son iki senende part-time çalışmak da genel olarak önerilen bir kendini geliştirme yolu. Part-time iş bulamasan da dert değil, kendi projelerine zaman ayırmaya devam 😉&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Topluluklar. Çevreni genişletebileceğin etkinlikler demiştim ya, topluluklar bunun için biçilmiş kaftan. Ben topluluğa katılmak yerine etkinliklerine katılmayı tercih ettim hep. Bunun kendimce sebepleri yok değil ama burada bahsetmeyeceğim 🤐 Topluluklar seninle aynı yolda olan insanlarla tanışman için güzel bir seçenek. Ayrıca toplulukların yönetiminde olmak sektör için güzel bir deneyim. Dikkat et de arada kaybolma sakın...&lt;/li&gt;
&lt;li&gt;Kitaplar. Bölümümüzde pek de kitap kullanılmıyor, en azından dersler için. Fakat sıkça önerilen kitapları okumanı öneririm, farklı bakış açıları kazanmanda ve bu alanın büyüklerinin deneyimleri hakkında bilgi edinmende faydalı olacaklar. İşte aklıma gelenler:
&lt;ul&gt;
&lt;li&gt;Clean Code: A Handbook of Agile Software Craftsmanship.&lt;/li&gt;
&lt;li&gt;The Pragmatic Programmer: From Journeyman to Master.&lt;/li&gt;
&lt;li&gt;Algorithms to Live By: The Computer Science of Human Decisions.&lt;/li&gt;
&lt;li&gt;Design Patterns: Elements of Reusable Object-Oriented Software.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Not:&lt;/strong&gt; Burada yazanlar benim bunları yaptığımı göstermez. &lt;mark&gt;Bazen tavsiyeler pişmanlıklardan ders çıkararak yapılır.&lt;/mark&gt;&lt;/p&gt;
&lt;p&gt;Okuduğun için teşekkür ederim, bir sonraki yazıda görüşmek üzere ✌️&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;paylaştım bile: &lt;a href=&quot;https://akalp.co/writing/yatay-gecis&quot;&gt;Yatay geçiş mi? O da ne?&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;2026 notu: Fiyatlar artık çok daha yüksek, o yüzden öğrenci indirimlerine, yenilenmiş (refurbished) ve ikinci el seçeneklere mutlaka bak. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;2026 notu: yapay zekâ araçları artık günlük işin parçası. En az 16 GB RAM al; modelleri kendi bilgisayarında denemek istersen 8 GB+ VRAM’li bir ekran kartı ya da 24 GB+ birleşik belleği olan bir Mac iyi bir başlangıç. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;2026 notu: &lt;a href=&quot;https://education.github.com/pack&quot;&gt;GitHub Student Developer Pack&lt;/a&gt; ile birçok araç ücretsiz, &lt;a href=&quot;https://gemini.google/students/&quot;&gt;Öğrenciler için Gemini&lt;/a&gt;’ye de mutlaka göz at. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><dc:language>tr</dc:language><category>bilgisayar-muhendisligi</category><category>universite</category></item></channel></rss>