diff --git a/backend/src/evaluator.rs b/backend/src/evaluator.rs index f54dc99..f23fc7c 100644 --- a/backend/src/evaluator.rs +++ b/backend/src/evaluator.rs @@ -28,9 +28,9 @@ impl Evaluator { }; } - pub fn set_cell(&mut self, cell_ref: CellRef, raw_val: String) -> Result<(), String> { + pub fn set_cell(&mut self, cell_ref: CellRef, raw_val: String) -> Result { if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val { - return Ok(()); + return self.get_cell(cell_ref); } let eval: Eval; @@ -50,19 +50,23 @@ impl Evaluator { } } - self.cells.insert(cell_ref, Cell::new(eval, raw_val)); - Ok(()) + self.cells + .insert(cell_ref, Cell::new(eval.clone(), raw_val)); + Ok(eval) } - pub fn get_cell(&mut self, cell_ref: CellRef) -> Result<(String, Eval), String> { + // pub fn get_cell(&mut self, cell_ref: CellRef) -> Result<(String, Eval), String> { + pub fn get_cell(&mut self, cell_ref: CellRef) -> Result { if !self.cells.contains_key(&cell_ref) { return Err(format!("Cell at {:?} not found.", cell_ref)); } let cell = &self.cells[&cell_ref]; - Ok((cell.raw(), cell.eval())) + // Ok((cell.raw(), cell.eval())) + Ok(cell.eval()) } + pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> { if !self.cells.contains_key(&cell_ref) { return Err(format!("Cell at {:?} not found.", cell_ref)); @@ -87,7 +91,7 @@ impl Evaluator { fn evaluate_expr(&mut self, expr: &Expr) -> Result { let res = match expr { Expr::Literal(lit) => Eval::Literal(lit.clone()), - Expr::CellRef(re) => self.get_cell(re.to_owned())?.1, + Expr::CellRef(re) => self.get_cell(re.to_owned())?, Expr::Infix { op, lhs, rhs } => { let lval = self.evaluate_expr(lhs)?; let rval = self.evaluate_expr(rhs)?; @@ -151,6 +155,14 @@ fn eval_mul(lval: &Eval, rval: &Eval) -> Result { fn eval_div(lval: &Eval, rval: &Eval) -> Result { match (lval, rval) { (Eval::Literal(a), Eval::Literal(b)) => { + if let (Literal::Integer(_), Literal::Integer(y)) = (a, b) { + if *y == 0 { + return Err( + "Evaluation error: integers attempted to divide by zero.".to_string() + ); + } + } + if let Some(res) = eval_numeric_infix(a, b, |x, y| x / y, |x, y| x / y) { return Ok(Eval::Literal(res)); } diff --git a/backend/src/main.rs b/backend/src/main.rs index 953c6d6..43323f3 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -80,6 +80,36 @@ async fn main() -> Result<(), Error> { // } } +// async fn accept_connection(stream: TcpStream) { +// let addr = stream +// .peer_addr() +// .expect("connected streams should have a peer address"); +// info!("Peer address: {}", addr); +// +// let ws_stream = tokio_tungstenite::accept_async(stream) +// .await +// .expect("Error during the websocket handshake occurred"); +// +// info!("New WebSocket connection: {}", addr); +// +// let (mut write, mut read) = ws_stream.split(); +// +// // We should not forward messages other than text or binary. +// while let Some(msg) = read.try_next().await.unwrap_or(None) { +// if msg.is_text() || msg.is_binary() { +// if let Err(e) = write +// .send(format!("This is a message {}!", msg.to_text().unwrap_or("")).into()) +// .await +// { +// eprintln!("send error: {}", e); +// break; +// } +// } +// } +// +// info!("Disconnected from {}", addr); +// } + async fn accept_connection(stream: TcpStream) { let addr = stream .peer_addr() @@ -94,15 +124,57 @@ async fn accept_connection(stream: TcpStream) { let (mut write, mut read) = ws_stream.split(); - // We should not forward messages other than text or binary. + // Each connection gets its own evaluator + let mut evaluator = Evaluator::new(); + while let Some(msg) = read.try_next().await.unwrap_or(None) { - if msg.is_text() || msg.is_binary() { - if let Err(e) = write - .send(format!("This is a message {}!", msg.to_text().unwrap_or("")).into()) - .await - { - eprintln!("send error: {}", e); - break; + if msg.is_text() { + let input = msg.to_text().unwrap_or("").trim().to_string(); + + let cmds = ["set", "get"]; + let cmd = &input[0..3.min(input.len())]; // avoid panic on short strings + + if !cmds.iter().any(|c| c == &cmd) { + let _ = write + .send(format!("ERR invalid command: {}", input).into()) + .await; + continue; + } + + let rest = input[4..].trim(); + let mut parts = rest.splitn(2, char::is_whitespace); + + let raw_ref = parts.next().unwrap_or("").trim(); // cell reference + let raw_str = parts.next().unwrap_or("").trim(); // rest (value) + + if let Ok(cell_ref) = CellRef::new(raw_ref.to_owned()) { + match cmd { + "set" => match evaluator.set_cell(cell_ref.clone(), raw_str.to_owned()) { + Ok(eval) => { + let _ = write.send(format!("{} {}", raw_ref, eval).into()).await; + } + Err(e) => { + let _ = write.send(format!("ERR {}", e).into()).await; + } + }, + "get" => match evaluator.get_cell(cell_ref.clone()) { + Ok(res) => { + let _ = write + .send(format!("{} {}", raw_ref, res.to_string()).into()) + .await; + } + Err(e) => { + let _ = write.send(format!("ERR {}", e).into()).await; + } + }, + _ => { + let _ = write.send("ERR impossible".into()).await; + } + } + } else { + let _ = write + .send(format!("ERR invalid cell reference: {}", raw_ref).into()) + .await; } } } diff --git a/frontend/package.json b/frontend/package.json index 2889186..604cc1d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "prettier-plugin-tailwindcss": "^0.6.11", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.1", "tailwind-variants": "^1.0.0", "tailwindcss": "^4.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index af48336..a1016cb 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: svelte-check: specifier: ^4.0.0 version: 4.3.1(picomatch@4.0.3)(svelte@5.38.6)(typescript@5.9.2) + svelte-sonner: + specifier: ^1.0.5 + version: 1.0.5(svelte@5.38.6) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -859,6 +862,11 @@ packages: peerDependencies: svelte: ^5.7.0 + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + runed@0.29.2: resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} peerDependencies: @@ -890,6 +898,11 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' + svelte-sonner@1.0.5: + resolution: {integrity: sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg==} + peerDependencies: + svelte: ^5.0.0 + svelte-toolbelt@0.7.1: resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -1584,6 +1597,11 @@ snapshots: esm-env: 1.2.2 svelte: 5.38.6 + runed@0.28.0(svelte@5.38.6): + dependencies: + esm-env: 1.2.2 + svelte: 5.38.6 + runed@0.29.2(svelte@5.38.6): dependencies: esm-env: 1.2.2 @@ -1619,6 +1637,11 @@ snapshots: transitivePeerDependencies: - picomatch + svelte-sonner@1.0.5(svelte@5.38.6): + dependencies: + runed: 0.28.0(svelte@5.38.6) + svelte: 5.38.6 + svelte-toolbelt@0.7.1(svelte@5.38.6): dependencies: clsx: 2.1.1 diff --git a/frontend/src/app.css b/frontend/src/app.css index e48f5f0..05decf4 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,121 +1,121 @@ -@import "tailwindcss"; +@import 'tailwindcss'; -@import "tw-animate-css"; +@import 'tw-animate-css'; @custom-variant dark (&:is(.dark *)); :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.129 0.042 264.695); - --card: oklch(1 0 0); - --card-foreground: oklch(0.129 0.042 264.695); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.129 0.042 264.695); - --primary: oklch(0.208 0.042 265.755); - --primary-foreground: oklch(0.984 0.003 247.858); - --secondary: oklch(0.968 0.007 247.896); - --secondary-foreground: oklch(0.208 0.042 265.755); - --muted: oklch(0.968 0.007 247.896); - --muted-foreground: oklch(0.554 0.046 257.417); - --accent: oklch(0.968 0.007 247.896); - --accent-foreground: oklch(0.208 0.042 265.755); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.929 0.013 255.508); - --input: oklch(0.929 0.013 255.508); - --ring: oklch(0.704 0.04 256.788); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.984 0.003 247.858); - --sidebar-foreground: oklch(0.129 0.042 264.695); - --sidebar-primary: oklch(0.208 0.042 265.755); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.968 0.007 247.896); - --sidebar-accent-foreground: oklch(0.208 0.042 265.755); - --sidebar-border: oklch(0.929 0.013 255.508); - --sidebar-ring: oklch(0.704 0.04 256.788); + --radius: 0.65rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.623 0.214 259.815); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.623 0.214 259.815); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.623 0.214 259.815); + --sidebar-primary-foreground: oklch(0.97 0.014 254.604); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.623 0.214 259.815); } .dark { - --background: oklch(0.129 0.042 264.695); - --foreground: oklch(0.984 0.003 247.858); - --card: oklch(0.208 0.042 265.755); - --card-foreground: oklch(0.984 0.003 247.858); - --popover: oklch(0.208 0.042 265.755); - --popover-foreground: oklch(0.984 0.003 247.858); - --primary: oklch(0.929 0.013 255.508); - --primary-foreground: oklch(0.208 0.042 265.755); - --secondary: oklch(0.279 0.041 260.031); - --secondary-foreground: oklch(0.984 0.003 247.858); - --muted: oklch(0.279 0.041 260.031); - --muted-foreground: oklch(0.704 0.04 256.788); - --accent: oklch(0.279 0.041 260.031); - --accent-foreground: oklch(0.984 0.003 247.858); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.551 0.027 264.364); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.208 0.042 265.755); - --sidebar-foreground: oklch(0.984 0.003 247.858); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.279 0.041 260.031); - --sidebar-accent-foreground: oklch(0.984 0.003 247.858); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.551 0.027 264.364); + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.546 0.245 262.881); + --primary-foreground: oklch(0.379 0.146 265.522); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.488 0.243 264.376); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.546 0.245 262.881); + --sidebar-primary-foreground: oklch(0.379 0.146 265.522); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.488 0.243 264.376); } @theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } @layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/src/lib/components/grid/cell-header.svelte b/frontend/src/lib/components/grid/cell-header.svelte new file mode 100644 index 0000000..de23b45 --- /dev/null +++ b/frontend/src/lib/components/grid/cell-header.svelte @@ -0,0 +1,125 @@ + + +
+ + {val} + + + + + diff --git a/frontend/src/lib/components/grid/cell.svelte b/frontend/src/lib/components/grid/cell.svelte new file mode 100644 index 0000000..314fe67 --- /dev/null +++ b/frontend/src/lib/components/grid/cell.svelte @@ -0,0 +1,90 @@ + + +{#if editing} +
+ { + raw_val = (e.target as HTMLInputElement).value; + stopediting(); + }} + /> +
+{:else} +
+ {#if raw_val !== '' || val !== ''} + + {#if val !== undefined} + {val} + {:else} + {raw_val} + {/if} + + {/if} +
+{/if} + + diff --git a/frontend/src/lib/components/grid/grid.svelte b/frontend/src/lib/components/grid/grid.svelte index e69de29..98b27df 100644 --- a/frontend/src/lib/components/grid/grid.svelte +++ b/frontend/src/lib/components/grid/grid.svelte @@ -0,0 +1,260 @@ + + +
+
+ + {#each Array(cols) as _, j} + setColWidth(j, width)} + direction="col" + val={toColLetter(j + 1)} + active={active_cell !== null && active_cell[1] === j} + /> + {/each} +
+ {#each Array(rows) as _, i} +
+ setRowHeight(i, height)} + val={(i + 1).toString()} + active={active_cell !== null && active_cell[0] === i} + /> + {#each Array(cols) as _, j} + startEditing(i, j)} + stopediting={stopEditing} + onmousedown={(e) => { + handleCellInteraction(i, j, e); + }} + bind:raw_val={() => getCellRaw(i, j), (v) => setCell(i, j, v)} + val={getCellVal(i, j)} + active={active_cell !== null && active_cell[0] === i && active_cell[1] === j} + /> + {/each} +
+ {/each} +
diff --git a/frontend/src/lib/components/grid/utils.ts b/frontend/src/lib/components/grid/utils.ts new file mode 100644 index 0000000..143bd67 --- /dev/null +++ b/frontend/src/lib/components/grid/utils.ts @@ -0,0 +1,39 @@ +export function fromGridRef(ref: string): [number, number] { + const match = ref.match(/^([A-Z]+)([0-9]+)$/i); + if (!match) throw new Error('Invalid reference'); + + const [, letters, rowStr] = match; + + let col = 0; + for (let i = 0; i < letters.length; i++) { + col = col * 26 + (letters.charCodeAt(i) - 64); // 'A' = 65 → 1 + } + + const row = parseInt(rowStr, 10); + return [row - 1, col - 1]; +} + +export function toColLetter(col: number): string { + let result = ''; + let n = col; + while (n > 0) { + const rem = (n - 1) % 26; + result = String.fromCharCode(65 + rem) + result; // 65 = 'A' + n = Math.floor((n - 1) / 26); + } + + return result; +} + +export function toGridRef(row: number, col: number): string { + row++; + col++; + return toColLetter(col) + row.toString(); +} + +export type CellValue = number | string | undefined; + +export interface CellData { + raw_val: string; + val: CellValue; +} diff --git a/frontend/src/lib/components/ui/sonner/index.ts b/frontend/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000..1ad9f4a --- /dev/null +++ b/frontend/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/frontend/src/lib/components/ui/sonner/sonner.svelte b/frontend/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000..1f50e1e --- /dev/null +++ b/frontend/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,13 @@ + + + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 4277a7d..921aa44 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,12 +1,14 @@ + diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 29d916c..f4ab7e6 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -5,18 +5,20 @@ import { Button } from '$lib/components/ui/button/index.js'; import { toggleMode } from 'mode-watcher'; import Grid from '$lib/components/grid/grid.svelte'; + + let socket = new WebSocket('ws://localhost:7050'); - + + + + + + + + + -
- +
+