Skip to content

Commit

Permalink
Spreadsheet UI is working nicely
Browse files Browse the repository at this point in the history
  • Loading branch information
J-Cake committed Aug 3, 2024
1 parent 7887d25 commit 835c78d
Show file tree
Hide file tree
Showing 11 changed files with 621 additions and 83 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Terminal Plugin
# Spreadsheet

A simplistic terminal based on xterm.js to provide a terminal which you can use for all your system-manipulation needs
Spreadsheet is an Obsidian plugin which follows the Obsidian mindset of open-standards and supports rich spreadsheet
handling through CSV.
6 changes: 3 additions & 3 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"id": "obsidian-os/terminal",
"name": "Terminal",
"id": "obsidian-os/spreadsheet",
"name": "Spreadsheet",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Provides an in-app terminal",
"description": "Provides a spreadsheet system",
"author": "Jake Schneider",
"authorUrl": "https://jschneiderprojects.com.au",
"isDesktopOnly": false
Expand Down
23 changes: 17 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
{
"name": "terminal",
"name": "spreadsheet",
"main": "./build/index.js",
"version": "0.1.0",
"type": "module",
"dependencies": {
"@j-cake/jcake-utils": "latest",
"chalk": "latest"
"chalk": "latest",
"react": "latest",
"react-dom": "latest",
"expressions": "link:../expressions"
},
"scripts": {
"build": "mkjson build/\\* install"
"build:plugin": "esbuild src/main.ts --outdir=build --bundle --sourcemap --platform=node --format=cjs --external:obsidian --external:electron",
"build:package.json": "cat package.json | jq -r '. * .deploy * {deploy:null} | with_entries(select(.value |. != null))' > build/package.json",
"build:manifest.json": "cat manifest.json | jq -r '.' > build/manifest.json",
"build:styles.css": "esbuild styles.css --outdir=build --bundle",
"build:hotreload": "echo hotreload > build/.hotreload",
"phony:rebuild": "cat package.json | jq -r '.scripts | keys_unsorted[] | select(. | startswith(\"build:\"))' | xargs -d \\\\n -I {} $npm_execpath run {}",
"phony:install": "mkdir -p \"$vault_dir/.obsidian/plugins/$(cat package.json | jq -r '.name')\"; cp -ra build/. \"$vault_dir/.obsidian/plugins/$(cat package.json | jq -r '.name')\"",
"phony:clean": "rm -rf build target node_modules *lock* *yarn* *pnpm*"
},
"devDependencies": {
"@types/node": "latest",
"@j-cake/mkjson": "latest",
"@types/node": "latest",
"@types/react": "latest",
"electron": "latest",
"typescript": "latest",
"esbuild": "latest",
"obsidian": "latest"
"obsidian": "latest",
"typescript": "latest"
},
"imports": {
"#app": "./build/index.js"
Expand Down
110 changes: 110 additions & 0 deletions src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {FrontMatter} from "./spreadsheet.js";
import {editorLivePreviewField, parseYaml, stringifyYaml} from "obsidian";
import * as iter from "@j-cake/jcake-utils/iter";
import {Cell} from "./range.js";

export const typeDefList: Record<string, ((value: string) => any)> = {
raw: column => column,
} as const;

export type Row<FM extends FrontMatter> = string[];

export default class DataSource<FM extends FrontMatter> {
private onChange: () => void = () => void 0;

public data: Row<FM>[] = [];

frontMatter: FM = {} as any;

parsers = typeDefList;

serialise(): string {
return `---\n${stringifyYaml(this.frontMatter)}\n---\n${this.data.map(i => i.join(this.frontMatter.columnSeparator ??= ';')).join('\n')}`;
}

clear() {
this.data = [];
this.onChange();
}

fromString(data: string): this {
const frontMatterMarker = data.indexOf("---\n");

if (frontMatterMarker > -1) {
const end = data.indexOf("---\n", frontMatterMarker + 3);
this.parseFrontMatter(data.slice(frontMatterMarker + 3, end).trim());
data = data.slice(end + 3).trim();
}

const separator = this.frontMatter.columnSeparator ?? /[,;]/g;
const rows = data.split(/\r?\n/);

if (!this.frontMatter.columnTitles)
this.frontMatter.columnTitles = rows.shift()?.split(separator) ?? [];

this.data = rows
.map(line => {
const column = [];

const iterator = zip(iter.IterSync(line.split(separator)), this.frontMatter.columnTypes ?? new InfiniteIterator("raw"));

for (const [value, parser] of iterator)
column.push((this.parsers[parser] ?? this.parsers.raw)(value));

return column;
});

this.onChange();
return this;
}

private parseFrontMatter(frontMatter: string): number {
this.frontMatter = parseYaml(frontMatter);
return frontMatter.length;
}

onExternalChange(change: () => void) {
this.onChange = change;
}

public get columnNames(): string[] {
return this.frontMatter.columnTitles ?? [];
}

public valueAt(cell: Cell): string {
return this.data[cell.row][cell.col];
}

public setValueAt(cell: Cell, value: string) {
this.data[cell.row][cell.col] = value;
this.onChange();
}
}

export class InfiniteIterator<T> {
[Symbol.iterator]() {
return this.iter();
}

constructor(private readonly data: T) {}

private *iter(): Generator<T> {
while (true)
yield this.data;
}
}

export function zip<A, B>(iter1: Iterable<A>, iter2: Iterable<B>): iter.iterSync.Iter<[A, B]> {
const zip = function* (iter1: Iterable<A>, iter2: Iterable<B>): Generator<[A, B]> {
for (const i of iter1) {
const next = iter2[Symbol.iterator]().next();

yield [i, next.value];

if (next.done)
break;
}
}

return iter.IterSync(zip(iter1, iter2));
}
28 changes: 25 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { App, Modal, Plugin, PluginSettingTab } from 'obsidian';
import {App, Menu, Modal, Plugin, PluginSettingTab, TAbstractFile, TFile} from 'obsidian';
import Spreadsheet, {SPREADSHEET_VIEW} from "./spreadsheet.js";

export interface Settings {

Expand All @@ -7,11 +8,32 @@ export const default_settings: Settings = {

};

export default class ContentType extends Plugin {
export default class SpreadsheetPlugin extends Plugin {
settings: Settings = default_settings;

async onload() {
this.registerView(SPREADSHEET_VIEW, leaf => new Spreadsheet(leaf));
this.registerExtensions(["csv", "tab"], SPREADSHEET_VIEW);

this.addCommand({
id: "open-new-spreadsheet",
name: "New Spreadsheet",
callback: async () => await this.app.workspace.getLeaf(true).setViewState({ type: SPREADSHEET_VIEW, active: true })
});

this.registerEvent(this.app.workspace.on("file-menu", menu => menu
.addItem(item => item
.setTitle("New Spreadsheet")
.setIcon("sheet")
.onClick(_ => this.runCommand("obsidian-os/spreadsheet:open-new-spreadsheet")))));
}

private runCommand(command: string) {
(this.app as any as {
commands: {
executeCommandById: (command: string) => void
}
}).commands.executeCommandById(command);
}

async loadSettings() {
Expand All @@ -24,7 +46,7 @@ export default class ContentType extends Plugin {
}

export class SettingsTab extends PluginSettingTab {
constructor(app: App, private plugin: ContentType) {
constructor(app: App, private plugin: SpreadsheetPlugin) {
super(app, plugin);
}

Expand Down
72 changes: 72 additions & 0 deletions src/range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export default class Range {
public constructor(public from: Cell, public to: Cell) {
}

public eq(b: Range): boolean {
return this.from.eq(b.from) && this.to.eq(b.to);
}

public get width(): number {
return Math.max(this.from.col, this.to.col) - Math.min(this.from.col, this.to.col) + 1;
}

public get height(): number {
return Math.max(this.from.row, this.to.row) - Math.min(this.from.row, this.to.row) + 1;
}

public get area(): number {
return this.width * this.height;
}

public get topLeft(): Cell {
return new Cell(Math.min(this.from.row, this.to.row), Math.min(this.from.col, this.to.col));
}

public get topRight(): Cell {
return new Cell(Math.max(this.from.row, this.to.row), Math.min(this.from.col, this.to.col));
}

public get bottomLeft(): Cell {
return new Cell(Math.min(this.from.row, this.to.row), Math.max(this.from.col, this.to.col));
}

public get bottomRight(): Cell {
return new Cell(Math.max(this.from.row, this.to.row), Math.max(this.from.col, this.to.col));
}

public union(range: Range): Range {
const topLeft1 = this.topLeft;
const topLeft2 = range.topLeft;

const bottomRight1 = this.bottomRight;
const bottomRight2 = range.bottomRight;

return new Range(
new Cell(Math.min(topLeft1.row, topLeft2.row), Math.min(topLeft1.col, topLeft2.col)),
new Cell(Math.max(bottomRight1.row, bottomRight2.row), Math.max(bottomRight1.col, bottomRight2.col)),
);
}

public toString(): string {
if (this.area == 1)
return this.topLeft.toString();
else
return `${this.topLeft.toString()}:${this.bottomRight.toString()}`;
}
}

export class Cell {
public constructor(public row: number, public col: number) {
}

public eq(b: Cell): boolean {
return this.col == b.col && this.row == b.row;
}

public toString(): string {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const alphabet2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

return `${[...this.col.toString(26).toUpperCase()].map(i => alphabet[alphabet2.indexOf(i)])}${this.row + 1}`;
}
}
66 changes: 66 additions & 0 deletions src/spreadsheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {ItemView, Menu, Notice, TextFileView, WorkspaceLeaf} from "obsidian";
import * as React from "react";
import * as rdom from "react-dom/client";
import DataSource from "./data.js";
import Table from "./table.js";

export const SPREADSHEET_VIEW = "spreadsheet-view";

export interface FrontMatter extends Record<string, any> {
columnTypes?: string[],
rowTypes?: string[],
constrainToDefinedColumns?: boolean,
constrainToDefinedRows?: boolean,
columnTitles?: string[],
rowTitles?: string[],
allowedTypes?: string[],
columnSeparator?: string
}

export default class Spreadsheet extends TextFileView {
front: FrontMatter = {};
separator: string = ";";

dataSource: DataSource<any> = new DataSource();

getViewData(): string {
return this.dataSource.serialise();
}

setViewData(data: string, clear: boolean): void {
if (clear)
this.clear();

this.dataSource.fromString(data);
}

clear(): void {
this.dataSource.clear();
}

getViewType(): string {
return SPREADSHEET_VIEW
}

getDisplayText(): string {
return this.file?.basename ?? "Untitled Spreadsheet";
}

getIcon(): string {
return "sheet";
}

onPaneMenu(menu: Menu, source: string) {
menu.addItem(item => item
.setIcon("settings")
.setTitle("Spreadsheet Preferences"));
}

protected async onOpen(): Promise<void> {
const spreadsheet = rdom.createRoot(this.contentEl);

spreadsheet.render(<section className={"spreadsheet-container"}>
<Table data={this.dataSource}/>
</section>);
}
}
Loading

0 comments on commit 835c78d

Please sign in to comment.