browse the protocol like its 2008 ibex.desertthunder.dev
ubuntu atproto svelte
7

Configure Feed

Select the types of activity you want to include in your feed.

feat: gedit polish & resizing

Owais Jamil (Jun 8, 2026, 2:38 PM -0500) 12deae8e 4f9421df

+158 -20
+2
README.md
··· 1 1 # Intrepid Ibex 2 2 3 3 A recreation of Ubuntu 8.10 as an ATProto browser. Inspired by [docs.surf](https://docs.surf) 4 + 5 + ![Screenshot](./doc/screenshot.png)
doc/screenshot.png

This is a binary file and will not be displayed.

+3
package.json
··· 44 44 "@atcute/bluesky": "^4.0.6", 45 45 "@atcute/client": "^5.0.0", 46 46 "@atcute/lexicons": "^2.0.0", 47 + "@shikijs/core": "^4.2.0", 48 + "@shikijs/engine-javascript": "^4.2.0", 49 + "@shikijs/langs": "^4.2.0", 47 50 "shiki": "^4.2.0" 48 51 } 49 52 }
+9
pnpm-lock.yaml
··· 20 20 '@atcute/lexicons': 21 21 specifier: ^2.0.0 22 22 version: 2.0.0 23 + '@shikijs/core': 24 + specifier: ^4.2.0 25 + version: 4.2.0 26 + '@shikijs/engine-javascript': 27 + specifier: ^4.2.0 28 + version: 4.2.0 29 + '@shikijs/langs': 30 + specifier: ^4.2.0 31 + version: 4.2.0 23 32 shiki: 24 33 specifier: ^4.2.0 25 34 version: 4.2.0
+27
src/lib/components/AppWindow.svelte
··· 16 16 onminimize?: () => void; 17 17 onmaximize?: () => void; 18 18 resizable?: boolean; 19 + variant?: 'default' | 'native'; 19 20 onclose?: () => void; 20 21 }; 21 22 ··· 32 33 onminimize, 33 34 onmaximize, 34 35 resizable = true, 36 + variant = 'default', 35 37 onclose, 36 38 children 37 39 }: Props & { children: Snippet } = $props(); ··· 155 157 class:dragging={drag} 156 158 class:resizing={resize} 157 159 class:maximized 160 + class:no-menubar={!showMenubar} 161 + class:no-toolbar={!showToolbar} 162 + class:native-window={variant === 'native'} 158 163 aria-label={title} 159 164 role="group" 160 165 style:transform={maximized ? undefined : `translate(${x}px, ${y}px)`} ··· 249 254 .app-window.dragging, 250 255 .app-window.resizing { 251 256 user-select: none; 257 + } 258 + 259 + .app-window.no-menubar.no-toolbar { 260 + grid-template-rows: auto minmax(0, 1fr); 261 + } 262 + 263 + .app-window.no-menubar:not(.no-toolbar), 264 + .app-window.no-toolbar:not(.no-menubar) { 265 + grid-template-rows: auto auto minmax(0, 1fr); 252 266 } 253 267 254 268 .app-window.maximized { ··· 258 272 transform: none; 259 273 } 260 274 275 + .native-window .titlebar { 276 + color: #2e3436; 277 + background: linear-gradient(#eeeeec, #b3b7b0); 278 + border-bottom-color: #747772; 279 + text-shadow: 0 1px 0 rgb(255 255 255 / 0.75); 280 + } 281 + 261 282 .titlebar { 262 283 display: flex; 263 284 align-items: center; ··· 398 419 399 420 .content { 400 421 overflow: hidden; 422 + min-height: 0; 401 423 padding: var(--space-3); 402 424 background: var(--window-surface); 425 + } 426 + 427 + .native-window .content { 428 + height: 100%; 429 + padding: 0; 403 430 } 404 431 405 432 .resize-handle {
+91 -18
src/lib/components/Gedit.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from 'svelte'; 3 - import type { BundledLanguage, ThemeRegistration } from 'shiki'; 3 + import type { ThemeRegistration } from 'shiki'; 4 4 import type { RepoRecordSummary } from '$lib/atproto/repo.svelte'; 5 5 6 6 type Props = { record: RepoRecordSummary }; ··· 8 8 9 9 let { record }: Props = $props(); 10 10 let tokenLines = $state<TokenLine[]>([]); 11 + let wordWrap = $state(false); 12 + let copied = $state(false); 13 + let copyTimeout: ReturnType<typeof setTimeout> | null = null; 11 14 15 + const themeName = 'ubuntu-iterm2b24'; 12 16 const ubuntuTheme: ThemeRegistration = { 13 - name: 'ubuntu-iterm2b24', 17 + name: themeName, 14 18 type: 'dark', 15 19 colors: { 16 20 'editor.background': '#300a24', ··· 18 22 'editorLineNumber.foreground': '#747772', 19 23 'editor.selectionBackground': '#555753' 20 24 }, 21 - tokenColors: [ 25 + settings: [ 26 + { settings: { foreground: '#eeeeec', background: '#300a24' } }, 22 27 { scope: ['comment'], settings: { foreground: '#747772', fontStyle: 'italic' } }, 23 - { scope: ['string'], settings: { foreground: '#4e9a06' } }, 28 + { scope: ['string', 'string.quoted'], settings: { foreground: '#4e9a06' } }, 24 29 { scope: ['constant.numeric', 'constant.language'], settings: { foreground: '#c4a000' } }, 25 - { scope: ['keyword', 'storage'], settings: { foreground: '#729fcf' } }, 26 - { scope: ['entity.name', 'support.type'], settings: { foreground: '#34e2e2' } }, 27 - { scope: ['variable', 'support.variable'], settings: { foreground: '#ad7fa8' } }, 30 + { 31 + scope: ['support.type.property-name', 'meta.structure.dictionary.key.json string'], 32 + settings: { foreground: '#729fcf' } 33 + }, 28 34 { scope: ['punctuation'], settings: { foreground: '#b3b7b0' } }, 29 35 { scope: ['invalid'], settings: { foreground: '#ef2929' } } 30 36 ] 31 37 }; 32 38 39 + let highlighterPromise: Promise<{ 40 + codeToTokensBase: (code: string, options: { lang: 'json'; theme: string }) => TokenLine[]; 41 + }> | null = null; 42 + 43 + async function getJsonHighlighter() { 44 + highlighterPromise ??= Promise.all([ 45 + import('@shikijs/core'), 46 + import('@shikijs/engine-javascript'), 47 + import('@shikijs/langs/json') 48 + ]).then(async ([core, engine, json]) => { 49 + const highlighter = await core.createHighlighterCore({ 50 + themes: [ubuntuTheme], 51 + langs: [json.default], 52 + engine: engine.createJavaScriptRegexEngine() 53 + }); 54 + 55 + return { 56 + codeToTokensBase: (code: string, options: { lang: 'json'; theme: string }) => 57 + highlighter.codeToTokensBase(code, options) as TokenLine[] 58 + }; 59 + }); 60 + 61 + return highlighterPromise; 62 + } 63 + 33 64 async function highlight() { 34 - const { codeToTokensBase } = await import('shiki'); 65 + const highlighter = await getJsonHighlighter(); 66 + tokenLines = highlighter.codeToTokensBase(record.json, { lang: 'json', theme: themeName }); 67 + } 68 + 69 + async function copyRecord() { 70 + await navigator.clipboard.writeText(record.json); 71 + copied = true; 35 72 36 - tokenLines = await codeToTokensBase(record.json, { lang: 'json' satisfies BundledLanguage, theme: ubuntuTheme }); 73 + if (copyTimeout) clearTimeout(copyTimeout); 74 + copyTimeout = setTimeout(() => { 75 + copied = false; 76 + copyTimeout = null; 77 + }, 1400); 37 78 } 38 79 39 80 onMount(() => { ··· 48 89 <section class="gedit" aria-label={`${record.title} JSON record`}> 49 90 <header class="gedit-toolbar"> 50 91 <div class="tool-group" aria-label="Document actions"> 51 - <button type="button">Open</button> 52 - <button type="button">Save</button> 53 - <button type="button">Print</button> 92 + <button type="button" class:copied aria-live="polite" onclick={copyRecord}>{copied ? 'Copied!' : 'Copy'}</button> 93 + <button type="button" class:active={wordWrap} aria-pressed={wordWrap} onclick={() => (wordWrap = !wordWrap)}> 94 + Word Wrap 95 + </button> 54 96 </div> 55 97 <div class="document-path"> 56 98 <img src="/icons/humanity/mimes/text-x-generic.svg" alt="" width="18" height="18" /> ··· 58 100 </div> 59 101 </header> 60 102 61 - <div class="editor-shell"> 103 + <div class="editor-shell" class:word-wrap={wordWrap}> 62 104 <aside class="line-gutter" aria-hidden="true"> 63 105 {#each record.json.split('\n') as line, index (`line-${record.uri}-${index}`)} 64 - <span>{line ? index + 1 : index + 1}</span> 106 + <span aria-label={line}>{index + 1}</span> 65 107 {/each} 66 108 </aside> 67 109 68 110 <div class="code-pane"> 69 111 <pre class="gedit-code"><code 70 - >{#if tokenLines.length > 0}{#each tokenLines as line, lineIndex (`${record.uri}-${lineIndex}`)}{#each line as token, tokenIndex (`${record.uri}-${lineIndex}-${tokenIndex}`)}<span 71 - style:color={token.color}>{token.content}</span 72 - >{/each}{#if lineIndex < tokenLines.length - 1}{/if}{/each}{:else}{record.json}{/if}</code></pre> 112 + >{#if tokenLines.length > 0}{#each tokenLines as line, lineIndex (`${record.uri}-${lineIndex}`)}<span 113 + class="code-line" 114 + >{#each line as token, tokenIndex (`${record.uri}-${lineIndex}-${tokenIndex}`)}<span 115 + style:color={token.color}>{token.content}</span 116 + >{/each}</span 117 + >{/each}{:else}{#each record.json.split('\n') as line, index (`fallback-${record.uri}-${index}`)}<span 118 + class="code-line">{line}</span 119 + >{/each}{/if}</code></pre> 73 120 </div> 74 121 </div> 75 122 76 123 <footer class="statusbar"> 77 124 <span>{record.uri}</span> 78 125 <span>JSON</span> 79 - <span>Ln 1, Col 1</span> 126 + <span>{wordWrap ? 'Wrap on' : 'Wrap off'}</span> 80 127 </footer> 81 128 </section> 82 129 ··· 140 187 cursor: default; 141 188 } 142 189 190 + button.active, 191 + button.copied { 192 + color: white; 193 + background: linear-gradient(#729fcf, #3465a4); 194 + text-shadow: 0 1px 0 rgb(0 0 0 / 0.5); 195 + } 196 + 197 + button.copied { 198 + background: linear-gradient(#8ae234, #4e9a06); 199 + border-color: #3b7c09; 200 + box-shadow: 201 + 0 1px 0 rgb(255 255 255 / 0.5) inset, 202 + 0 0 0 1px rgb(138 226 52 / 0.35); 203 + } 204 + 143 205 .document-path { 144 206 display: flex; 145 207 align-items: center; ··· 196 258 font-family: var(--font-mono); 197 259 font-size: var(--text-1); 198 260 line-height: 1.45; 261 + } 262 + 263 + .code-line { 264 + display: block; 265 + min-height: 1.45em; 266 + white-space: pre; 267 + } 268 + 269 + .word-wrap .code-line { 270 + white-space: pre-wrap; 271 + overflow-wrap: anywhere; 199 272 } 200 273 201 274 .statusbar {
+23
src/lib/components/NativeWindow.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + import AppWindow from './AppWindow.svelte'; 4 + 5 + type Props = { 6 + title: string; 7 + icon: string; 8 + address?: string; 9 + windowId?: string; 10 + maximized?: boolean; 11 + onfocus?: () => void; 12 + onminimize?: () => void; 13 + onmaximize?: () => void; 14 + onclose?: () => void; 15 + children: Snippet; 16 + }; 17 + 18 + let { children, ...props }: Props = $props(); 19 + </script> 20 + 21 + <AppWindow {...props} variant="native" showMenubar={false} showToolbar={false}> 22 + {@render children()} 23 + </AppWindow>
+3 -2
src/routes/+layout.svelte
··· 9 9 import DesktopIcon from '$lib/components/DesktopIcon.svelte'; 10 10 import Gedit from '$lib/components/Gedit.svelte'; 11 11 import GnomePanel from '$lib/components/GnomePanel.svelte'; 12 + import NativeWindow from '$lib/components/NativeWindow.svelte'; 12 13 import SetupDialog from '$lib/components/SetupDialog.svelte'; 13 14 import '$lib/styles/style.css'; 14 15 ··· 127 128 128 129 {#if repoBrowser.selectedRecord && geditWindow?.isOpen && !geditWindow.isMinimized} 129 130 <div class="gedit-window" class:maximized={geditWindow.isMaximized} style:z-index={geditWindow.zIndex}> 130 - <AppWindow 131 + <NativeWindow 131 132 windowId="gedit" 132 133 title={`${repoBrowser.selectedRecord.rkey}.json - gedit`} 133 134 icon="/icons/humanity/apps/accessories-text-editor.svg" ··· 138 139 onmaximize={() => windowManager.toggleMaximize('gedit')} 139 140 onclose={() => windowManager.close('gedit')}> 140 141 <Gedit record={repoBrowser.selectedRecord} /> 141 - </AppWindow> 142 + </NativeWindow> 142 143 </div> 143 144 {/if} 144 145