What persists: every card (kind, position, size, title, note text, ptyId, z-order) + viewport pan/zoom + maxZ counter,
in a single board.json under app_data_dir() (~/.local/share/{appName}/board.json on Linux).
Write pattern: tmp file + atomic rename, debounced 400ms. A crash mid-write can't corrupt the existing file; worst
case you lose ~400ms of recent edits.
Read pattern: load on app start, gate the Canvas render on the result. Missing file → default welcome board. Parse
error → log and fall back to default (won't overwrite the bad file until the user makes changes; actually it will save
over it after the next change — if you want to preserve a corrupt file for recovery that's a one-line tweak).
Terminal caveat: live PTY processes die with the app session, so sanitize() in App.tsx clears every terminal card's
ptyId on load. Each terminal card respawns a fresh shell on mount via the existing pty.spawn path. The card's
position, size, and title are preserved.
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Infinite</title>
|
<title>Infinite</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D6iqmLrm.js"></script>
|
<script type="module" crossorigin src="/assets/index-DxdHmf_Q.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BRF-Y_tu.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BZPHLDSC.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
mod pty;
|
mod pty;
|
||||||
|
mod storage;
|
||||||
|
|
||||||
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
|
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
|
||||||
|
use storage::{board_load, board_save};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -33,6 +35,8 @@ pub fn run() {
|
|||||||
pty_write,
|
pty_write,
|
||||||
pty_resize,
|
pty_resize,
|
||||||
pty_kill,
|
pty_kill,
|
||||||
|
board_save,
|
||||||
|
board_load,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
36
src-tauri/src/storage.rs
Normal file
36
src-tauri/src/storage.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
fn board_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
|
let dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|e| format!("app_data_dir: {e}"))?;
|
||||||
|
fs::create_dir_all(&dir).map_err(|e| format!("create_dir_all: {e}"))?;
|
||||||
|
Ok(dir.join("board.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn board_save(app: AppHandle, state: Value) -> Result<(), String> {
|
||||||
|
let path = board_path(&app)?;
|
||||||
|
let tmp = path.with_extension("json.tmp");
|
||||||
|
let body = serde_json::to_string_pretty(&state).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(&tmp, body).map_err(|e| format!("write tmp: {e}"))?;
|
||||||
|
// Atomic replace so a crash mid-write can't corrupt the existing file.
|
||||||
|
fs::rename(&tmp, &path).map_err(|e| format!("rename: {e}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn board_load(app: AppHandle) -> Result<Option<Value>, String> {
|
||||||
|
let path = board_path(&app)?;
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let body = fs::read_to_string(&path).map_err(|e| format!("read: {e}"))?;
|
||||||
|
let v = serde_json::from_str(&body).map_err(|e| format!("parse: {e}"))?;
|
||||||
|
Ok(Some(v))
|
||||||
|
}
|
||||||
46
src/App.tsx
46
src/App.tsx
@@ -1,7 +1,11 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Canvas } from "./canvas/Canvas";
|
import { Canvas } from "./canvas/Canvas";
|
||||||
import type { Card } from "./canvas/types";
|
import type { BoardState, Card } from "./canvas/types";
|
||||||
|
import { board } from "./ipc";
|
||||||
|
|
||||||
const initialCards: Card[] = [
|
const DEFAULT_BOARD: BoardState = {
|
||||||
|
version: 1,
|
||||||
|
cards: [
|
||||||
{
|
{
|
||||||
id: "welcome",
|
id: "welcome",
|
||||||
kind: "note",
|
kind: "note",
|
||||||
@@ -16,8 +20,42 @@ const initialCards: Card[] = [
|
|||||||
"Zoom: Ctrl + scroll\n\n" +
|
"Zoom: Ctrl + scroll\n\n" +
|
||||||
"Use the toolbar to add notes and terminals.",
|
"Use the toolbar to add notes and terminals.",
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
viewport: { x: 0, y: 0, scale: 1 },
|
||||||
|
maxZ: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Live PTY processes die with the previous app session — wipe ptyIds so each
|
||||||
|
// terminal card respawns a fresh shell on mount.
|
||||||
|
function sanitize(state: BoardState): BoardState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cards: state.cards.map((c): Card =>
|
||||||
|
c.kind === "terminal" ? { ...c, ptyId: "" } : c,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <Canvas initialCards={initialCards} />;
|
const [initial, setInitial] = useState<BoardState | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
board
|
||||||
|
.load()
|
||||||
|
.then((loaded) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setInitial(loaded ? sanitize(loaded) : DEFAULT_BOARD);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("board_load failed", err);
|
||||||
|
if (!cancelled) setInitial(DEFAULT_BOARD);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!initial) return null;
|
||||||
|
return <Canvas initial={initial} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,47 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from "react";
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import type { Card, Viewport } from "./types";
|
import type { BoardState, Card, Viewport } from "./types";
|
||||||
import { ViewportContext } from "./viewport";
|
import { ViewportContext } from "./viewport";
|
||||||
import { NoteCardView } from "./cards/NoteCardView";
|
import { NoteCardView } from "./cards/NoteCardView";
|
||||||
import { TerminalCardView } from "./cards/TerminalCardView";
|
import { TerminalCardView } from "./cards/TerminalCardView";
|
||||||
|
import { board, pty } from "../ipc";
|
||||||
import "./canvas.css";
|
import "./canvas.css";
|
||||||
|
|
||||||
const MIN_SCALE = 0.1;
|
const MIN_SCALE = 0.1;
|
||||||
const MAX_SCALE = 4;
|
const MAX_SCALE = 4;
|
||||||
const ZOOM_SENSITIVITY = 0.0015;
|
const ZOOM_SENSITIVITY = 0.0015;
|
||||||
|
const SAVE_DEBOUNCE_MS = 400;
|
||||||
|
|
||||||
interface CanvasProps {
|
interface CanvasProps {
|
||||||
initialCards: Card[];
|
initial: BoardState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Canvas({ initialCards }: CanvasProps) {
|
export function Canvas({ initial }: CanvasProps) {
|
||||||
const [cards, setCards] = useState<Card[]>(initialCards);
|
const [cards, setCards] = useState<Card[]>(initial.cards);
|
||||||
const [vp, setVp] = useState<Viewport>({ x: 0, y: 0, scale: 1 });
|
const [vp, setVp] = useState<Viewport>(initial.viewport);
|
||||||
const [spaceHeld, setSpaceHeld] = useState(false);
|
const [spaceHeld, setSpaceHeld] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const panState = useRef<{ startX: number; startY: number; vpX: number; vpY: number } | null>(
|
const panState = useRef<{ startX: number; startY: number; vpX: number; vpY: number } | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const maxZRef = useRef(
|
const maxZRef = useRef(
|
||||||
initialCards.reduce((m, c) => Math.max(m, c.z), 0),
|
Math.max(initial.maxZ, initial.cards.reduce((m, c) => Math.max(m, c.z), 0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-save the board whenever cards or viewport change. Debounced so a
|
||||||
|
// pan/drag/typing burst writes once after motion stops.
|
||||||
|
useEffect(() => {
|
||||||
|
const handle = window.setTimeout(() => {
|
||||||
|
const state: BoardState = {
|
||||||
|
version: 1,
|
||||||
|
cards,
|
||||||
|
viewport: vp,
|
||||||
|
maxZ: maxZRef.current,
|
||||||
|
};
|
||||||
|
board.save(state).catch((err) => console.error("board_save failed", err));
|
||||||
|
}, SAVE_DEBOUNCE_MS);
|
||||||
|
return () => window.clearTimeout(handle);
|
||||||
|
}, [cards, vp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.code === "Space" && !e.repeat) setSpaceHeld(true);
|
if (e.code === "Space" && !e.repeat) setSpaceHeld(true);
|
||||||
@@ -99,6 +116,26 @@ export function Canvas({ initialCards }: CanvasProps) {
|
|||||||
setCards((cs) => cs.map((c) => (c.id === id ? ({ ...c, ...patch } as Card) : c)));
|
setCards((cs) => cs.map((c) => (c.id === id ? ({ ...c, ...patch } as Card) : c)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteCard = (id: string) => {
|
||||||
|
setCards((cs) => {
|
||||||
|
const target = cs.find((c) => c.id === id);
|
||||||
|
if (target?.kind === "terminal" && target.ptyId) {
|
||||||
|
pty.kill(target.ptyId).catch(() => {});
|
||||||
|
}
|
||||||
|
return cs.filter((c) => c.id !== id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const bringToFront = (id: string) => {
|
||||||
|
setCards((cs) => {
|
||||||
|
const target = cs.find((c) => c.id === id);
|
||||||
|
if (!target || target.z === maxZRef.current) return cs;
|
||||||
|
maxZRef.current += 1;
|
||||||
|
const newZ = maxZRef.current;
|
||||||
|
return cs.map((c) => (c.id === id ? ({ ...c, z: newZ } as Card) : c));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const addCardAtCenter = (build: (id: string, x: number, y: number, z: number) => Card) => {
|
const addCardAtCenter = (build: (id: string, x: number, y: number, z: number) => Card) => {
|
||||||
const w = window.innerWidth / 2;
|
const w = window.innerWidth / 2;
|
||||||
const h = window.innerHeight / 2;
|
const h = window.innerHeight / 2;
|
||||||
@@ -154,10 +191,26 @@ export function Canvas({ initialCards }: CanvasProps) {
|
|||||||
<ViewportContext.Provider value={vp}>
|
<ViewportContext.Provider value={vp}>
|
||||||
{cards.map((c) => {
|
{cards.map((c) => {
|
||||||
if (c.kind === "note") {
|
if (c.kind === "note") {
|
||||||
return <NoteCardView key={c.id} card={c} onUpdate={(p) => updateCard(c.id, p)} />;
|
return (
|
||||||
|
<NoteCardView
|
||||||
|
key={c.id}
|
||||||
|
card={c}
|
||||||
|
onUpdate={(p) => updateCard(c.id, p)}
|
||||||
|
onClose={() => deleteCard(c.id)}
|
||||||
|
onFocus={() => bringToFront(c.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (c.kind === "terminal") {
|
if (c.kind === "terminal") {
|
||||||
return <TerminalCardView key={c.id} card={c} onUpdate={(p) => updateCard(c.id, p)} />;
|
return (
|
||||||
|
<TerminalCardView
|
||||||
|
key={c.id}
|
||||||
|
card={c}
|
||||||
|
onUpdate={(p) => updateCard(c.id, p)}
|
||||||
|
onClose={() => deleteCard(c.id)}
|
||||||
|
onFocus={() => bringToFront(c.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
padding: 0.46em 0.77em;
|
padding: 0.31em 0.46em 0.31em 0.77em;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-bottom: calc(1px * var(--scale, 1)) solid var(--card-border);
|
border-bottom: calc(1px * var(--scale, 1)) solid var(--card-border);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
@@ -61,6 +61,51 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-top-left-radius: inherit;
|
border-top-left-radius: inherit;
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.46em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
padding: 0.15em 0;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title::placeholder {
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.6em;
|
||||||
|
height: 1.6em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-close:hover {
|
||||||
|
background: rgba(255, 80, 80, 0.25);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
|
|||||||
33
src/canvas/cards/CardHeader.tsx
Normal file
33
src/canvas/cards/CardHeader.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
placeholder: string;
|
||||||
|
onTitleChange: (next: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
dragProps: { onPointerDown: (e: React.PointerEvent) => void };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardHeader({ title, placeholder, onTitleChange, onClose, dragProps }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="card-header" {...dragProps}>
|
||||||
|
<input
|
||||||
|
className="card-title"
|
||||||
|
value={title}
|
||||||
|
placeholder={placeholder}
|
||||||
|
spellCheck={false}
|
||||||
|
onChange={(e) => onTitleChange(e.target.value)}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="card-close"
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={onClose}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,16 +3,19 @@ import type { NoteCard } from "../types";
|
|||||||
import { useViewport } from "../viewport";
|
import { useViewport } from "../viewport";
|
||||||
import { useDragHandle } from "../useDragHandle";
|
import { useDragHandle } from "../useDragHandle";
|
||||||
import { ResizeHandles } from "./ResizeHandles";
|
import { ResizeHandles } from "./ResizeHandles";
|
||||||
|
import { CardHeader } from "./CardHeader";
|
||||||
import { useMarkdownEditor } from "./useMarkdownEditor";
|
import { useMarkdownEditor } from "./useMarkdownEditor";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
card: NoteCard;
|
card: NoteCard;
|
||||||
onUpdate: (patch: Partial<NoteCard>) => void;
|
onUpdate: (patch: Partial<NoteCard>) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onFocus: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BODY_FONT_BASE = 13;
|
const BODY_FONT_BASE = 13;
|
||||||
|
|
||||||
export function NoteCardView({ card, onUpdate }: Props) {
|
export function NoteCardView({ card, onUpdate, onClose, onFocus }: Props) {
|
||||||
const vp = useViewport();
|
const vp = useViewport();
|
||||||
const drag = useDragHandle(card, (p) => onUpdate(p));
|
const drag = useDragHandle(card, (p) => onUpdate(p));
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -21,6 +24,7 @@ export function NoteCardView({ card, onUpdate }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card note-card"
|
className="card note-card"
|
||||||
|
onPointerDownCapture={onFocus}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
left: vp.x + card.x * vp.scale,
|
left: vp.x + card.x * vp.scale,
|
||||||
@@ -33,9 +37,13 @@ export function NoteCardView({ card, onUpdate }: Props) {
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="card-header" {...drag}>
|
<CardHeader
|
||||||
note
|
title={card.title ?? ""}
|
||||||
</div>
|
placeholder="Note"
|
||||||
|
onTitleChange={(t) => onUpdate({ title: t })}
|
||||||
|
onClose={onClose}
|
||||||
|
dragProps={drag}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="card-body note-body"
|
className="card-body note-body"
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { TerminalCard } from "../types";
|
|||||||
import { useViewport } from "../viewport";
|
import { useViewport } from "../viewport";
|
||||||
import { useDragHandle } from "../useDragHandle";
|
import { useDragHandle } from "../useDragHandle";
|
||||||
import { ResizeHandles } from "./ResizeHandles";
|
import { ResizeHandles } from "./ResizeHandles";
|
||||||
|
import { CardHeader } from "./CardHeader";
|
||||||
import { pty, base64ToBytes, stringToBase64 } from "../../ipc";
|
import { pty, base64ToBytes, stringToBase64 } from "../../ipc";
|
||||||
|
|
||||||
const BASE_FONT_SIZE = 13;
|
const BASE_FONT_SIZE = 13;
|
||||||
@@ -13,9 +14,11 @@ const BASE_FONT_SIZE = 13;
|
|||||||
interface Props {
|
interface Props {
|
||||||
card: TerminalCard;
|
card: TerminalCard;
|
||||||
onUpdate: (patch: Partial<TerminalCard>) => void;
|
onUpdate: (patch: Partial<TerminalCard>) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onFocus: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TerminalCardView({ card, onUpdate }: Props) {
|
export function TerminalCardView({ card, onUpdate, onClose, onFocus }: Props) {
|
||||||
const vp = useViewport();
|
const vp = useViewport();
|
||||||
const drag = useDragHandle(card, (p) => onUpdate(p));
|
const drag = useDragHandle(card, (p) => onUpdate(p));
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -106,6 +109,7 @@ export function TerminalCardView({ card, onUpdate }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card terminal-card"
|
className="card terminal-card"
|
||||||
|
onPointerDownCapture={onFocus}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
left: vp.x + card.x * vp.scale,
|
left: vp.x + card.x * vp.scale,
|
||||||
@@ -117,9 +121,13 @@ export function TerminalCardView({ card, onUpdate }: Props) {
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="card-header" {...drag}>
|
<CardHeader
|
||||||
terminal
|
title={card.title ?? ""}
|
||||||
</div>
|
placeholder="Terminal"
|
||||||
|
onTitleChange={(t) => onUpdate({ title: t })}
|
||||||
|
onClose={onClose}
|
||||||
|
dragProps={drag}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="card-body terminal-body"
|
className="card-body terminal-body"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ export interface BaseCard {
|
|||||||
export interface NoteCard extends BaseCard {
|
export interface NoteCard extends BaseCard {
|
||||||
kind: "note";
|
kind: "note";
|
||||||
text: string;
|
text: string;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminalCard extends BaseCard {
|
export interface TerminalCard extends BaseCard {
|
||||||
kind: "terminal";
|
kind: "terminal";
|
||||||
ptyId: string;
|
ptyId: string;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThumbnailCard extends BaseCard {
|
export interface ThumbnailCard extends BaseCard {
|
||||||
@@ -35,3 +37,10 @@ export interface Viewport {
|
|||||||
y: number;
|
y: number;
|
||||||
scale: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BoardState {
|
||||||
|
version: number;
|
||||||
|
cards: Card[];
|
||||||
|
viewport: Viewport;
|
||||||
|
maxZ: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||||
|
import type { BoardState } from "./canvas/types";
|
||||||
|
|
||||||
export interface PtySpawnOpts {
|
export interface PtySpawnOpts {
|
||||||
cols: number;
|
cols: number;
|
||||||
@@ -48,3 +49,8 @@ export function base64ToBytes(b64: string): Uint8Array {
|
|||||||
export function stringToBase64(s: string): string {
|
export function stringToBase64(s: string): string {
|
||||||
return bytesToBase64(new TextEncoder().encode(s));
|
return bytesToBase64(new TextEncoder().encode(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const board = {
|
||||||
|
save: (state: BoardState) => invoke<void>("board_save", { state }),
|
||||||
|
load: () => invoke<BoardState | null>("board_load"),
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user