From 95cef714902bbcbdc3ef016457e7a77d38293ea8 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 22 Jul 2024 16:31:12 +0100 Subject: feat(gui): background terminals and more (#2303) * fixes & allow for background terminals to stay running * status indicators etc --- ui/src/state/store.ts | 124 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 3 deletions(-) (limited to 'ui/src/state/store.ts') diff --git a/ui/src/state/store.ts b/ui/src/state/store.ts index cde2da17..c6d3c152 100644 --- a/ui/src/state/store.ts +++ b/ui/src/state/store.ts @@ -19,11 +19,24 @@ import { invoke } from "@tauri-apps/api/core"; import { sessionToken, settings } from "./client"; import { getWeekInfo } from "@/lib/utils"; import Runbook from "./runbooks/runbook"; +import { Terminal } from "@xterm/xterm"; +import { FitAddon } from "@xterm/addon-fit"; +import { WebglAddon } from "@xterm/addon-webgl"; + +export class TerminalData { + terminal: Terminal; + fitAddon: FitAddon; + + constructor(terminal: Terminal, fit: FitAddon) { + this.terminal = terminal; + this.fitAddon = fit; + } +} // I'll probs want to slice this up at some point, but for now a // big blobby lump of state is fine. // Totally just hoping that structure will be emergent in the future. -interface AtuinState { +export interface AtuinState { user: User; homeInfo: HomeInfo; aliases: Alias[]; @@ -32,7 +45,7 @@ interface AtuinState { calendar: any[]; weekStart: number; runbooks: Runbook[]; - currentRunbook: String | null; + currentRunbook: string | null; refreshHomeInfo: () => void; refreshCalendar: () => void; @@ -44,6 +57,16 @@ interface AtuinState { historyNextPage: (query?: string) => void; setCurrentRunbook: (id: String) => void; + setPtyTerm: (pty: string, terminal: any) => void; + newPtyTerm: (pty: string, runbook: string) => TerminalData; + cleanupPtyTerm: (pty: string) => void; + + terminals: { [pty: string]: TerminalData }; + + // Store ephemeral state for runbooks, that is not persisted to the database + runbookInfo: { [runbook: string]: { ptys: number } }; + incRunbookPty: (runbook: string) => void; + decRunbookPty: (runbook: string) => void; } let state = (set: any, get: any): AtuinState => ({ @@ -55,6 +78,8 @@ let state = (set: any, get: any): AtuinState => ({ calendar: [], runbooks: [], currentRunbook: "", + terminals: {}, + runbookInfo: {}, weekStart: getWeekInfo().firstDay, @@ -158,8 +183,101 @@ let state = (set: any, get: any): AtuinState => ({ setCurrentRunbook: (id: String) => { set({ currentRunbook: id }); }, + + setPtyTerm: (pty: string, terminal: TerminalData) => { + set({ + terminals: { ...get().terminals, [pty]: terminal }, + }); + }, + + cleanupPtyTerm: (pty: string) => { + set((state: AtuinState) => { + const terminals = Object.keys(state.terminals).reduce( + (terms: { [pty: string]: TerminalData }, key) => { + if (key !== pty) { + terms[key] = state.terminals[key]; + } + return terms; + }, + {}, + ); + + return { terminals }; + }); + }, + + newPtyTerm: (pty: string) => { + let terminal = new Terminal(); + + // TODO: fallback to canvas, also some sort of setting to allow disabling webgl usage + // probs fine for now though, it's widely supported. maybe issues on linux. + terminal.loadAddon(new WebglAddon()); + + let fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + + const onResize = (size: { cols: number; rows: number }) => { + invoke("pty_resize", { + pid: pty, + cols: size.cols, + rows: size.rows, + }); + }; + + terminal.onResize(onResize); + + let td = new TerminalData(terminal, fitAddon); + + set({ + terminals: { ...get().terminals, [pty]: td }, + }); + + return td; + }, + + incRunbookPty: (runbook: string) => { + set((state: AtuinState) => { + let oldVal = state.runbookInfo[runbook] || { ptys: 0 }; + let newVal = { ptys: oldVal.ptys + 1 }; + console.log(newVal); + + return { + runbookInfo: { + ...state.runbookInfo, + [runbook]: newVal, + }, + }; + }); + }, + + decRunbookPty: (runbook: string) => { + set((state: AtuinState) => { + let newVal = state.runbookInfo[runbook]; + if (!newVal) { + return; + } + + newVal.ptys--; + + return { + runbookInfo: { + ...state.runbookInfo, + [runbook]: newVal, + }, + }; + }); + }, }); export const useStore = create()( - persist(state, { name: "atuin-storage" }), + persist(state, { + name: "atuin-storage", + + // don't serialize the terminals map + // it won't work as JSON. too cyclical + partialize: (state) => + Object.fromEntries( + Object.entries(state).filter(([key]) => !["terminals"].includes(key)), + ), + }), ); -- cgit v1.3.1