[{"data":1,"prerenderedAt":720},["ShallowReactive",2],{"posts-1":3,"total-posts":87},[4,590],{"id":5,"title":6,"author":7,"body":8,"date":575,"description":576,"extension":577,"meta":578,"navigation":579,"path":580,"seo":581,"stem":582,"tags":583,"__hash__":589},"content/posts/th-linux-lcd.md","Reverse Engineering a Thermaltake LCD Using Claude: From USB Traces to a Native Linux App","Klatooine",{"type":9,"value":10,"toc":563},"minimark",[11,15,18,23,26,34,37,40,43,46,50,53,56,137,140,144,151,237,255,267,277,281,287,295,298,304,319,323,326,339,342,348,359,370,377,381,384,400,411,415,434,440,452,471,475,484,487,493,500,503,530,534,540,543,546,549,553,556,559],[12,13,14],"p",{},"I have a Thermaltake TH420V2 Ultra cooler. It has a 480×480 circular LCD on the pump head that can display animations and images. There is no Linux support. No community project, no open-source controller, nothing. The official software is Windows-only, bloated, and closed-source.",[12,16,17],{},"I wanted a clean, low-footprint native Linux app.",[19,20,22],"h2",{"id":21},"the-first-attempt","The first attempt",[12,24,25],{},"Before any AI was involved, I had a go at it myself. I set up a Windows VM, passed the USB device through with QEMU, fired up Wireshark on the Arch host, and started recording what the official software was sending.",[12,27,28,29,33],{},"The animation streaming was the obvious part — the traffic pattern is unmistakable once you see it. Bulk packets, 1024 bytes each, all starting with ",[30,31,32],"code",{},"0x08",", cycling at frame rate. I wrote a quick Python script to intercept one frame and dump it to disk. The bytes starting at offset 4 decoded as a valid JPEG. That felt like a win.",[12,35,36],{},"And then I hit the wall.",[12,38,39],{},"The upload traffic for boot animations and standby images was a different story. 440-byte control packets with structured headers, multi-chunk protocols, a proprietary binary container embedded in the payload — with a header that didn't obviously decode to anything, a checksum I couldn't derive, and a magic string I wasn't sure was real or something I was misreading. Understanding it required sitting down and systematically working through multiple captures, cross-referencing offsets, deriving formulas, and tracking what was confirmed vs. hypothetical.",[12,41,42],{},"I didn't have that time. I set it aside.",[12,44,45],{},"A few months later I came back to it with Claude as a collaborator. Having an AI that could hold the full context of the capture data, keep track of what each byte range meant, validate checksum formulas, and work through the protocol incrementally — that changed the equation. What felt like a week of focused work compressed into a focused session.",[19,47,49],{"id":48},"capturing-usb-traffic-from-a-windows-vm","Capturing USB traffic from a Windows VM",[12,51,52],{},"My setup: a Windows VM running the official Thermaltake software, with the USB device passed through via QEMU, and Wireshark running on the Arch host capturing the traffic.",[12,54,55],{},"This gives you a clean capture of every USB packet the Windows app sends to the device, without needing a second physical machine.",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-bash shiki shiki-themes github-light github-dark","tshark -r capture.pcapng \\\n  -Y \"usb.idVendor==0x264a\" \\\n  -T fields -e frame.number -e usb.endpoint_address \\\n  -e usb.transfer_type -e usb.data_len -e usb.capdata\n","bash","",[30,64,65,85,96,118],{"__ignoreMap":62},[66,67,70,74,78,82],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"sScJk","tshark",[66,75,77],{"class":76},"sj4cs"," -r",[66,79,81],{"class":80},"sZZnC"," capture.pcapng",[66,83,84],{"class":76}," \\\n",[66,86,88,91,94],{"class":68,"line":87},2,[66,89,90],{"class":76},"  -Y",[66,92,93],{"class":80}," \"usb.idVendor==0x264a\"",[66,95,84],{"class":76},[66,97,99,102,105,108,111,113,116],{"class":68,"line":98},3,[66,100,101],{"class":76},"  -T",[66,103,104],{"class":80}," fields",[66,106,107],{"class":76}," -e",[66,109,110],{"class":80}," frame.number",[66,112,107],{"class":76},[66,114,115],{"class":80}," usb.endpoint_address",[66,117,84],{"class":76},[66,119,121,124,127,129,132,134],{"class":68,"line":120},4,[66,122,123],{"class":76},"  -e",[66,125,126],{"class":80}," usb.transfer_type",[66,128,107],{"class":76},[66,130,131],{"class":80}," usb.data_len",[66,133,107],{"class":76},[66,135,136],{"class":80}," usb.capdata\n",[12,138,139],{},"You get one row per packet. Staring at the patterns long enough, the protocol starts to emerge.",[19,141,143],{"id":142},"what-i-found","What I found",[12,145,146,147,150],{},"The device (",[30,148,149],{},"264a:233c",") has two interfaces with four relevant endpoints:",[152,153,154,173],"table",{},[155,156,157],"thead",{},[158,159,160,164,167,170],"tr",{},[161,162,163],"th",{},"Interface",[161,165,166],{},"Endpoint",[161,168,169],{},"Direction",[161,171,172],{},"Purpose",[174,175,176,193,208,223],"tbody",{},[158,177,178,182,187,190],{},[179,180,181],"td",{},"1",[179,183,184],{},[30,185,186],{},"0x03",[179,188,189],{},"OUT",[179,191,192],{},"Animation frame data",[158,194,195,197,202,205],{},[179,196,181],{},[179,198,199],{},[30,200,201],{},"0x84",[179,203,204],{},"IN",[179,206,207],{},"Drain after each frame (required)",[158,209,210,213,218,220],{},[179,211,212],{},"0",[179,214,215],{},[30,216,217],{},"0x01",[179,219,189],{},[179,221,222],{},"Control commands",[158,224,225,227,232,234],{},[179,226,212],{},[179,228,229],{},[30,230,231],{},"0x82",[179,233,204],{},[179,235,236],{},"ACK (440 bytes, echoes command byte)",[12,238,239,243,244,246,247,251,252,254],{},[240,241,242],"strong",{},"Animation streaming"," works by splitting each JPEG into 1024-byte packets. The first byte is always ",[30,245,32],{},". After all packets for a single frame are sent, you ",[248,249,250],"em",{},"must"," read from ",[30,253,201],{}," — even with a 1ms timeout — or the device locks up completely.",[12,256,257,260,261,263,264,266],{},[240,258,259],{},"Standby and boot uploads"," use a chunked protocol over the control interface. The device receives an INIT command declaring the total size and a \"kind\" byte (",[30,262,212],{}," for standby, ",[30,265,181],{}," for boot), then the data in 10240-byte chunks, each acknowledged before the next.",[12,268,269,270,273,274,276],{},"The boot animation has one extra step: after the last chunk, you send CMD ",[30,271,272],{},"0x14",", which triggers the device to burn the data to flash. This command is fire-and-forget — the device returns nothing on any endpoint. I confirmed this by scanning all endpoints after sending it. Then a final ",[30,275,231],{}," FINALIZE closes the transaction.",[19,278,280],{"id":279},"a-real-trace","A real trace",[12,282,283,284,286],{},"Here's what actual USB packets look like for a boot animation upload. Each line is one 440-byte packet on EP ",[30,285,217],{},", shown as the first 16 bytes:",[57,288,293],{"className":289,"code":291,"language":292},[290],"language-text","; ── Pre-finalize (clears any in-progress transfer) ──────────────────────────\nEP 0x01 →  82 01 00 80 00 00 00 00 00 00 00 00 00 00 00 00  [+424 zeros]\nEP 0x82 ←  82 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  [+424 zeros]\n\n; ── INIT (kind=1 boot, total_size=82496 bytes) ───────────────────────────────\nEP 0x01 →  0a 01 00 80 01 00 00 00 40 42 01 00 00 00 00 00  [+424 zeros]\n;                    ^^^^ kind=1  ^^^^^^^^^^^^ 82496 LE\nEP 0x82 ←  0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  [+424 zeros]\n\n; ── Chunk 0 header (10240 bytes, 24 packets, offset=0, progress=12%) ─────────\nEP 0x01 →  0b 18 00 80 00 00 00 00 00 28 00 00 0c 00 00 00  [+424 bytes: container start]\n;          ^^^^ 24 pkts  ^offset=0  ^chunk=10240   ^12%\nEP 0x01 →  0b 02 00 00 \u003Cnext 436 bytes of container data ...>\nEP 0x01 →  0b 03 00 00 \u003C...>\n; ... 21 more data packets ...\nEP 0x82 ←  0b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  [+424 zeros]  ; chunk ACK\n\n; (chunks 1–7 follow the same pattern with increasing offset and progress%)\n\n; ── Commit to flash (boot only — CMD 0x14, fire-and-forget) ─────────────────\nEP 0x01 →  14 01 00 80 62 00 00 00 00 00 00 00 00 00 00 00  [+424 zeros]\n; EP 0x82 returns nothing — no ACK\n\n; ── Finalize ─────────────────────────────────────────────────────────────────\nEP 0x01 →  82 01 00 80 00 00 00 00 00 00 00 00 00 00 00 00  [+424 zeros]\nEP 0x82 ←  82 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  [+424 zeros]\n","text",[30,294,291],{"__ignoreMap":62},[12,296,297],{},"And an animation frame — a 3-packet JPEG sent on Interface 1:",[57,299,302],{"className":300,"code":301,"language":292},[290],"; ── Frame packet 1 of 3 (first byte 0x08, third byte flags total count) ──────\nEP 0x03 →  08 03 00 80 ff d8 ff e0 00 10 4a 46 49 46 00 01\n;          ^^^^       JPEG SOI + JFIF APP0 marker ──────────\n           01 01 00 60 00 60 00 00 ff db 00 43 00 10 0b 0c\n           0e 0c 0a 10 0e 0d 0e 12 11 10 13 18 28 1a 18 16\n           ; ... 1004 more bytes of JPEG data ...\n\n; ── Frame packet 2 of 3 ───────────────────────────────────────────────────────\nEP 0x03 →  08 02 00 00 \u003Cnext 1020 bytes of JPEG>\n\n; ── Frame packet 3 of 3 ───────────────────────────────────────────────────────\nEP 0x03 →  08 03 00 00 \u003Cfinal 1020 bytes of JPEG, zero-padded if short>\n\n; ── Mandatory drain on EP 0x84 (1ms timeout — skip this and the device locks) ─\nEP 0x84 ←  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n",[30,303,301],{"__ignoreMap":62},[12,305,306,307,310,311,314,315,318],{},"The JPEG header in packet 1 is genuine — bytes ",[30,308,309],{},"FF D8 FF E0"," are the JPEG SOI + APP0 marker, followed by ",[30,312,313],{},"4A 46 49 46"," (",[30,316,317],{},"JFIF","). That's exactly what you see when you extract a frame from the trace and open it in an image viewer.",[19,320,322],{"id":321},"the-boot-animation-container","The boot animation container",[12,324,325],{},"The trickiest part was the boot animation. It's not a raw GIF — it's a proprietary binary container. I extracted the upload payload from the pcap trace, reassembled the chunks, and started parsing manually.",[12,327,328,329,332,333,338],{},"At bytes ",[30,330,331],{},"[16:32]"," there's a null-padded ASCII string: ",[240,334,335],{},[30,336,337],{},"Update_Boot_GIF",". That's what the firmware calls this format — found verbatim in the binary, not something I named it.",[12,340,341],{},"The full header structure:",[57,343,346],{"className":344,"code":345,"language":292},[290],"[0:4]   checksum  = -(full_size + n_frames × 17) as int32 LE\n[4:8]   content_size = full_size - 16\n[8:12]  tbl_size = n_frames × 16\n[12]    0x10\n[13]    0x00\n[14]    n_frames\n[15]    timing = ms per frame  (80 = 12.5 fps)\n[16:32] \"Update_Boot_GIF\\0\"\n",[30,347,345],{"__ignoreMap":62},[12,349,350,351,354,355,358],{},"Followed by a frame table, the frame data (each prefixed with an 8-byte filename like ",[30,352,353],{},"000.jpg\\0","), and a 16-byte trailer (",[30,356,357],{},"0x00 × 15, 0x10",").",[12,360,361,362,365,366,369],{},"I verified the checksum formula by comparing multiple containers with different frame counts. The ",[30,363,364],{},"× 17"," factor turned out to encode ",[30,367,368],{},"1 (n_frames byte) + 16 (frame table entry size)"," — classic embedded firmware integrity check.",[12,371,372,373,376],{},"One thing I discovered the hard way: the firmware ",[240,374,375],{},"silently rejects"," containers with a timing byte below 80ms (more than 12.5 fps). The upload succeeds, all ACKs look fine, but after power cycle — no animation. Cap the fps at 12.5 or the device ignores everything.",[19,378,380],{"id":379},"the-jpeg-subsampling-quirk","The JPEG subsampling quirk",[12,382,383],{},"JPEG chroma subsampling tripped me up. The device is inconsistent about it:",[385,386,387,394],"ul",{},[388,389,390,393],"li",{},[240,391,392],{},"Streaming animations",": 4:4:4 subsampling. Use anything else and the bottom half of the display renders as corrupted garbage.",[388,395,396,399],{},[240,397,398],{},"Boot animation frames",": 4:2:0 subsampling. Opposite requirement, no obvious reason why.",[12,401,402,403,406,407,410],{},"In Python/Pillow: ",[30,404,405],{},"subsampling=0"," for 4:4:4, ",[30,408,409],{},"subsampling=2"," for 4:2:0. Getting this wrong is subtle — the upload completes successfully either way.",[19,412,414],{"id":413},"a-related-device","A related device",[12,416,417,418,427,428,431,432,358],{},"While digging into the USB VID, I found another project: ",[419,420,424],"a",{"href":421,"rel":422},"https://github.com/bekindpleaserewind/ttlcd",[423],"nofollow",[30,425,426],{},"ttlcd"," — a Linux controller for the Thermaltake LCD Panel Kit, a flat display for the Tower 200 Mini chassis. Its USB ID is ",[30,429,430],{},"264a:233d"," — one product ID away from this device (",[30,433,149],{},[12,435,436,437,439],{},"The protocol structure is the same family: 1024-byte frame packets with ",[30,438,32],{}," prefix, 440-byte control packets, mandatory drain after each frame. The core packet format is consistent between the two devices, which is a useful cross-validation — it means the animation protocol I reverse-engineered wasn't idiosyncratic to my unit.",[12,441,442,443,445,446,448,449,451],{},"The differences are where things get interesting. Their device is a 480×128 flat panel; ours is a 480×480 circle. More importantly, ",[30,444,426],{}," has no boot animation support, no standby image upload, and no flash write operation — those features, along with the ",[30,447,337],{}," container format and CMD ",[30,450,272],{},", appear to be unique to the pump-head LCD variant.",[12,453,454,455,458,459,462,463,466,467,470],{},"The actual OEM manufacturer, by the way, is not Thermaltake. The USB descriptor's ",[30,456,457],{},"iManufacturer"," string reads ",[240,460,461],{},"HKC OVERSEAS LIMITED",", with ",[30,464,465],{},"iProduct"," set to ",[30,468,469],{},"2.1 inch Round TFT LCD Display Module-B",". Thermaltake rebadges it. There are no public protocol docs from HKC for this module — which is why USB sniffing was the only path in.",[19,472,474],{"id":473},"the-app","The app",[12,476,477,478,483],{},"For the UI, I went with ",[419,479,482],{"href":480,"rel":481},"https://github.com/emilk/egui",[423],"egui"," — an immediate-mode Rust GUI library. The binary is ~3MB, starts in ~150ms, and has zero system GUI library dependencies beyond OpenGL.",[12,485,486],{},"The architecture is a daemon + GUI split:",[57,488,491],{"className":489,"code":490,"language":292},[290],"GUI process (egui)\n  └─ Unix socket (JSON IPC)\n       └─ Daemon process\n            ├─ Socket thread (one per connection)\n            └─ USB thread (owns the rusb handle, streams frames)\n",[30,492,490],{"__ignoreMap":62},[12,494,495,496,499],{},"The daemon runs as a systemd user service. The GUI auto-launches it if the socket isn't present. IPC is newline-delimited JSON — simple and debuggable with ",[30,497,498],{},"nc",".",[12,501,502],{},"Four features working:",[504,505,506,512,518,524],"ol",{},[388,507,508,511],{},[240,509,510],{},"GIF streaming"," — up to 60fps, rotation, live preview",[388,513,514,517],{},[240,515,516],{},"Standby image"," — JPEG/PNG/GIF upload to flash",[388,519,520,523],{},[240,521,522],{},"Boot animation"," — custom animation before OS connects",[388,525,526,529],{},[240,527,528],{},"Screen control"," — on/off",[19,531,533],{"id":532},"claude","Claude",[12,535,536,537,539],{},"My first solo pass got me the animation streaming — saw the 1024-byte packets, extracted a JPEG frame from the trace, confirmed the ",[30,538,32],{}," prefix pattern. That part was readable without much RE background.",[12,541,542],{},"The upload protocol was where it fell apart. The 440-byte command packets had structured headers I couldn't decode quickly. The boot container had a checksum I couldn't derive. I'd have needed a long, focused session with the hex dumps to work through it systematically, and I didn't have that. So I shelved it.",[12,544,545],{},"Coming back with Claude changed the dynamic. I'd feed in the raw hex, describe what I observed, and we'd work through it together — what each byte range likely meant, how to test a hypothesis, how to validate a formula across multiple captures. The AI kept the analysis structured across a long session and caught things I'd have missed staring at hex at midnight.",[12,547,548],{},"The USB sniffing and empirical device testing were hands-on. The protocol reconstruction was collaborative.",[19,550,552],{"id":551},"fair-warning","Fair warning",[12,554,555],{},"This is homemade, untested-on-anything-but-my-own-device software. It writes directly to flash memory. There is no warranty, no official support, and a non-zero chance it could brick your screen. Run it under your own responsibility.",[12,557,558],{},"If you have the same cooler and want to dig into the protocol — the full reverse engineering notes, container format spec, and USB command documentation are part of the project. I'll share the repo once I figure out where to host it publicly.",[560,561,562],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":62,"searchDepth":87,"depth":87,"links":564},[565,566,567,568,569,570,571,572,573,574],{"id":21,"depth":87,"text":22},{"id":48,"depth":87,"text":49},{"id":142,"depth":87,"text":143},{"id":279,"depth":87,"text":280},{"id":321,"depth":87,"text":322},{"id":379,"depth":87,"text":380},{"id":413,"depth":87,"text":414},{"id":473,"depth":87,"text":474},{"id":532,"depth":87,"text":533},{"id":551,"depth":87,"text":552},"2026-04-10","How I built a native Linux controller for the TH420V2 Ultra LCD by sniffing USB traffic from a Windows VM and reverse engineering the protocol from scratch, using Claude as a collaborator.","md",{},true,"/posts/th-linux-lcd",{"title":6,"description":576},"posts/th-linux-lcd",[584,585,586,587,482,588],"rust","reverse-engineering","linux","usb","hardware","ZrEk6UhKZ0UXYP6IStXLqxORJSgqSBA_Y10I388-mR0",{"id":591,"title":592,"author":7,"body":593,"date":708,"description":709,"extension":577,"meta":710,"navigation":579,"path":711,"seo":712,"stem":713,"tags":714,"__hash__":719},"content/posts/post-1.md","Commit --amend: Re-entering the Digital Stream",{"type":9,"value":594,"toc":703},[595,598,605,608,612,615,619,622,642,646,649,690,697,700],[12,596,597],{},"Hello world, again.",[12,599,600,601,604],{},"If you’ve been here before, you might have noticed the dust settling on the archives. I’ve always wanted ",[240,602,603],{},"devsstuff"," to be a living repository of my learnings, but \"lack of time\" is a formidable boss battle.",[12,606,607],{},"Today, I’m hitting the reset button.",[19,609,611],{"id":610},"why-now","Why now?",[12,613,614],{},"The barrier to entry for consistent documentation has shifted. While I used to get bogged down in the friction of formatting and structuring my thoughts, I’m now leveraging AI to act as a collaborative editor. This doesn't mean the content is \"autopilot\"—it means I can finally get my research and opinions out of my head and onto the screen without the usual overhead.",[19,616,618],{"id":617},"what-to-expect","What to expect",[12,620,621],{},"This blog remains a space for:",[385,623,624,630,636],{},[388,625,626,629],{},[240,627,628],{},"Trainings & Learnings:"," Deep dives into new frameworks or languages.",[388,631,632,635],{},[240,633,634],{},"Research & Findings:"," Notes from the rabbit holes I fall into.",[388,637,638,641],{},[240,639,640],{},"Evolving Opinions:"," Tech moves fast; my takes today might be deprecated by tomorrow.",[19,643,645],{"id":644},"the-new-workflow","The New Workflow",[12,647,648],{},"To stay consistent, I'm sticking to a clean, markdown-first approach. It keeps the focus on the code and the logic, rather than the fluff.",[57,650,652],{"className":59,"code":651,"language":61,"meta":62,"style":62},"# My new mantra for 2026\ngit commit -m \"Restarting the engine with AI augmentation\"\ngit push origin master --force\n\n",[30,653,654,660,674],{"__ignoreMap":62},[66,655,656],{"class":68,"line":69},[66,657,659],{"class":658},"sJ8bj","# My new mantra for 2026\n",[66,661,662,665,668,671],{"class":68,"line":87},[66,663,664],{"class":72},"git",[66,666,667],{"class":80}," commit",[66,669,670],{"class":76}," -m",[66,672,673],{"class":80}," \"Restarting the engine with AI augmentation\"\n",[66,675,676,678,681,684,687],{"class":68,"line":98},[66,677,664],{"class":72},[66,679,680],{"class":80}," push",[66,682,683],{"class":80}," origin",[66,685,686],{"class":80}," master",[66,688,689],{"class":76}," --force\n",[12,691,692,693,499],{},"I’m excited to share what I’m working on. If you see something that looks wrong or want to debate a point, my inbox is open at ",[419,694,696],{"href":695},"mailto:contact@klatooine.com","contact@klatooine.com",[12,698,699],{},"Stay tuned for the next update from the grid.",[560,701,702],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":62,"searchDepth":87,"depth":87,"links":704},[705,706,707],{"id":610,"depth":87,"text":611},{"id":617,"depth":87,"text":618},{"id":644,"depth":87,"text":645},"2026-02-22","A fresh start for devsstuff, exploring how AI is changing my workflow and why I'm back to writing.",{},"/posts/post-1",{"title":592,"description":709},"posts/post-1",[715,716,717,718],"meta","productivity","AI","development","0Y0HtL0oo3rbAsd_qiM8d_I10dYbQZbU4WJ1JapBTwI",1775981900696]