From 18c94fd7ddbbceab48ca4287b859c5b8572c5ca2 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 23 Sep 2025 21:13:41 -0700 Subject: [PATCH] unify notepad config with the app config --- package.json | 2 +- src/components/Notepad/Container.tsx | 29 ++--- src/components/Notepad/Editor.tsx | 6 +- src/components/Notepad/Tabs.tsx | 36 +++--- src/main/ipc.ts | 15 ++- src/main/modules/config.ts | 2 + src/main/modules/notepad.ts | 181 ++++++++++++++------------- src/preload/index.ts | 12 +- src/stores/notepad.ts | 77 +++++++----- src/types/electron.d.ts | 16 +-- 10 files changed, 197 insertions(+), 179 deletions(-) diff --git a/package.json b/package.json index 0de597c..6ad549a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "1.5.0", + "version": "1.5.1", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", diff --git a/src/components/Notepad/Container.tsx b/src/components/Notepad/Container.tsx index 03a8405..c070881 100644 --- a/src/components/Notepad/Container.tsx +++ b/src/components/Notepad/Container.tsx @@ -26,45 +26,40 @@ export const NotepadContainer = () => { const [resizeDirection, setResizeDirection] = useState(null); const [confirmCloseModal, setConfirmCloseModal] = useState<{ isOpen: boolean; - tabId: string | null; - tabTitle: string; + title: string; }>({ isOpen: false, - tabId: null, - tabTitle: '', + title: '', }); - const activeTab = tabs.find((tab) => tab.id === activeTabId); + const activeTab = tabs.find((tab) => tab.title === activeTabId); const handleCreateNewTab = async () => { const newTab = await window.electronAPI.notepad.createNewTab(); addTab(newTab); }; - const handleTabCloseRequest = (tabId: string) => { - const tab = tabs.find((t) => t.id === tabId); + const handleTabCloseRequest = (title: string) => { + const tab = tabs.find((t) => t.title === title); if (!tab) return; if (tab.content.trim().length > 0) { setConfirmCloseModal({ isOpen: true, - tabId, - tabTitle: tab.title, + title, }); } else { - removeTab(tabId); + removeTab(title); } }; const handleConfirmClose = () => { - if (confirmCloseModal.tabId) { - removeTab(confirmCloseModal.tabId); - } - setConfirmCloseModal({ isOpen: false, tabId: null, tabTitle: '' }); + removeTab(confirmCloseModal.title); + setConfirmCloseModal({ isOpen: false, title: '' }); }; const handleCancelClose = () => { - setConfirmCloseModal({ isOpen: false, tabId: null, tabTitle: '' }); + setConfirmCloseModal({ isOpen: false, title: '' }); }; const handleResizeStart = @@ -207,7 +202,7 @@ export const NotepadContainer = () => { onClick={() => setVisible(false)} color="red" > - + @@ -219,7 +214,7 @@ export const NotepadContainer = () => { diff --git a/src/components/Notepad/Editor.tsx b/src/components/Notepad/Editor.tsx index 3976db4..c4e465d 100644 --- a/src/components/Notepad/Editor.tsx +++ b/src/components/Notepad/Editor.tsx @@ -38,12 +38,12 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => { } const timeout = setTimeout(() => { - saveTabContent(tab.id, newContent); + saveTabContent(tab.title, newContent); }, 500); setSaveTimeout(timeout); }, - [tab.id, saveTabContent, saveTimeout] + [tab.title, saveTabContent, saveTimeout] ); const handleEditorContextMenu = (e: MouseEvent) => { @@ -62,7 +62,7 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => { useEffect(() => { setContent(tab.content); - }, [tab.content, tab.id]); + }, [tab.content, tab.title]); useEffect( () => () => { diff --git a/src/components/Notepad/Tabs.tsx b/src/components/Notepad/Tabs.tsx index f5bb7bb..3d27963 100644 --- a/src/components/Notepad/Tabs.tsx +++ b/src/components/Notepad/Tabs.tsx @@ -13,14 +13,13 @@ import { usePreferencesStore } from '@/stores/preferences'; interface NotepadTabsProps { onCreateNewTab: () => Promise; - onCloseTab: (tabId: string) => void; + onCloseTab: (title: string) => void; } interface TabProps { - id: string; + title: string; index: number; isActive: boolean; - title: string; onSelect: () => void; onClose: (e: MouseEvent) => void; onDragStart: (e: DragEvent, index: number) => void; @@ -166,6 +165,7 @@ const Tab = ({ ) : ( { @@ -208,17 +208,18 @@ export const NotepadTabs = ({ const [draggedTabIndex, setDraggedTabIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); - const handleTabSelect = (tabId: string) => { - setActiveTab(tabId); + const handleTabSelect = (title: string) => { + setActiveTab(title); }; - const handleTabClose = (e: MouseEvent, tabId: string) => { + const handleTabClose = (e: MouseEvent, title: string) => { e.stopPropagation(); - onCloseTab(tabId); + onCloseTab(title); }; - const handleTabRename = (tabId: string, newTitle: string) => { - updateTab(tabId, { title: newTitle }); + const handleTabRename = (title: string, newTitle: string) => { + updateTab(title, { title: newTitle }); + window.electronAPI.notepad.renameTab(title, newTitle); }; const handleTabBarContextMenu = (e: MouseEvent) => { @@ -258,8 +259,6 @@ export const NotepadTabs = ({ setDragOverIndex(null); }; - if (tabs.length === 0) return null; - return ( {tabs.map((tab, index) => ( handleDragEnter(index)} onDragLeave={handleDragLeave} > handleTabSelect(tab.id)} - onClose={(e) => handleTabClose(e, tab.id)} - onRename={(newTitle) => handleTabRename(tab.id, newTitle)} + index={index} + isActive={tab.title === activeTabId} + onSelect={() => handleTabSelect(tab.title)} + onClose={(e) => handleTabClose(e, tab.title)} + onRename={(newTitle) => handleTabRename(tab.title, newTitle)} onDragStart={handleDragStart} onDragOver={handleDragOver} onDrop={handleDrop} @@ -303,7 +301,7 @@ export const NotepadTabs = ({ size="xs" onClick={onCreateNewTab} style={{ - margin: '4px', + margin: '0.25rem', alignSelf: 'center', }} > diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 35f4b4d..6064b41 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -50,6 +50,7 @@ import { loadNotepadState, deleteTabFile, createNewTab, + renameTab, } from '@/main/modules/notepad'; import { detectGPU, @@ -279,11 +280,15 @@ export function setupIPCHandlers() { ipcMain.handle( 'notepad:saveTabContent', - (_, tabId: string, content: string) => saveTabContent(tabId, content) + (_, title: string, content: string) => saveTabContent(title, content) ); - ipcMain.handle('notepad:loadTabContent', (_, tabId: string) => - loadTabContent(tabId) + ipcMain.handle('notepad:loadTabContent', (_, title: string) => + loadTabContent(title) + ); + + ipcMain.handle('notepad:renameTab', (_, oldTitle: string, newTitle: string) => + renameTab(oldTitle, newTitle) ); ipcMain.handle('notepad:saveState', (_, state: NotepadState) => @@ -292,8 +297,8 @@ export function setupIPCHandlers() { ipcMain.handle('notepad:loadState', () => loadNotepadState()); - ipcMain.handle('notepad:deleteTab', (_, tabId: string) => - deleteTabFile(tabId) + ipcMain.handle('notepad:deleteTab', (_, title: string) => + deleteTabFile(title) ); ipcMain.handle('notepad:createNewTab', (_, title?: string) => diff --git a/src/main/modules/config.ts b/src/main/modules/config.ts index 7b6db30..4751253 100644 --- a/src/main/modules/config.ts +++ b/src/main/modules/config.ts @@ -8,6 +8,7 @@ import { nativeTheme } from 'electron'; import { PRODUCT_NAME } from '@/constants'; import type { FrontendPreference } from '@/types'; import type { MantineColorScheme } from '@mantine/core'; +import type { SavedNotepadState } from '@/types/electron'; interface WindowBounds { x: number; @@ -28,6 +29,7 @@ interface AppConfig { skipEjectConfirmation?: boolean; dismissedUpdates?: string[]; zoomLevel?: number; + notepad?: SavedNotepadState; } let config: AppConfig = {}; diff --git a/src/main/modules/notepad.ts b/src/main/modules/notepad.ts index 848e339..316726f 100644 --- a/src/main/modules/notepad.ts +++ b/src/main/modules/notepad.ts @@ -1,116 +1,119 @@ -import { promises as fs } from 'fs'; -import { join } from 'path'; -import { safeExecute, tryExecute } from '@/utils/node/logging'; -import { getInstallDir } from './config'; -import type { SavedNotepadState, SavedNotepadTab } from '@/types/electron'; +import { get, set, getInstallDir } from './config'; +import type { SavedNotepadState } from '@/types/electron'; import { DEFAULT_NOTEPAD_POSITION, DEFAULT_TAB_CONTENT, } from '@/constants/notepad'; - -const NOTEPAD_DIR = join(getInstallDir(), 'notepad'); -const NOTEPAD_STATE_FILE = join(NOTEPAD_DIR, 'state.json'); +import { pathExists } from '@/utils/node/fs'; +import { join } from 'path'; +import { readFile, readdir, writeFile, unlink, rename } from 'fs/promises'; const DEFAULT_NOTEPAD_STATE: SavedNotepadState = { - tabs: [], activeTabId: null, position: DEFAULT_NOTEPAD_POSITION, isVisible: false, }; -async function ensureNotepadDir() { - return tryExecute(async () => { - await fs.mkdir(NOTEPAD_DIR, { recursive: true }); - }, 'Failed to create notepad directory'); -} +const getNotepadDir = () => join(getInstallDir(), 'notepad'); -export async function saveTabContent(tabId: string, content: string) { - return tryExecute(async () => { - await ensureNotepadDir(); - const filePath = join(NOTEPAD_DIR, `${tabId}.txt`); - await fs.writeFile(filePath, content, 'utf8'); - }, 'Failed to save tab content'); -} +const getTabsFromStorage = async () => { + const tabs = []; -export async function loadTabContent(tabId: string) { - const filePath = join(NOTEPAD_DIR, `${tabId}.txt`); try { - return fs.readFile(filePath, 'utf8'); - } catch (error) { - if ((error as { code?: string }).code === 'ENOENT') { - return ''; - } - throw error; - } -} + const notepadDir = getNotepadDir(); + if (await pathExists(notepadDir)) { + const files = await readdir(notepadDir); + const tabFiles = files.filter((f) => f.endsWith('.txt')); -export async function saveNotepadState(state: SavedNotepadState) { - return tryExecute(async () => { - await ensureNotepadDir(); - await fs.writeFile( - NOTEPAD_STATE_FILE, - JSON.stringify(state, null, 2), - 'utf8' - ); - }, 'Failed to save notepad state'); -} - -export async function loadNotepadState() { - const result = await safeExecute(async () => { - try { - const data = await fs.readFile(NOTEPAD_STATE_FILE, 'utf8'); - return JSON.parse(data) as SavedNotepadState; - } catch { - return DEFAULT_NOTEPAD_STATE; - } - }, 'Failed to load notepad state'); - - return result || DEFAULT_NOTEPAD_STATE; -} - -export async function deleteTabFile(tabId: string) { - return tryExecute(async () => { - const filePath = join(NOTEPAD_DIR, `${tabId}.txt`); - try { - await fs.unlink(filePath); - } catch (error) { - if ((error as { code?: string }).code !== 'ENOENT') { - throw error; + for (const file of tabFiles) { + const title = file.replace('.txt', ''); + tabs.push({ title }); } } - }, 'Failed to delete tab file'); -} + } catch { + return []; + } -let tabCounter = 1; + return tabs; +}; -export async function createNewTab(title?: string) { +export const renameTab = async (oldTitle: string, newTitle: string) => { + try { + const notepadDir = getNotepadDir(); + + await rename( + join(notepadDir, `${oldTitle}.txt`), + join(notepadDir, `${newTitle}.txt`) + ); + return true; + } catch { + return false; + } +}; + +export const saveTabContent = async (title: string, content: string) => { + try { + const notepadDir = getNotepadDir(); + const filePath = join(notepadDir, `${title}.txt`); + + await writeFile(filePath, content, 'utf-8'); + return true; + } catch { + return false; + } +}; + +export const loadTabContent = async (title: string) => { + try { + const notepadDir = getNotepadDir(); + return readFile(join(notepadDir, `${title}.txt`), 'utf-8'); + } catch { + return ''; + } +}; + +export const saveNotepadState = async (state: SavedNotepadState) => { + await set('notepad', state); + return true; +}; + +export const loadNotepadState = async () => { + const stored = get('notepad') || DEFAULT_NOTEPAD_STATE; + const tabs = await getTabsFromStorage(); + const activeTabId = + stored.activeTabId && tabs.some((tab) => tab.title === stored.activeTabId) + ? stored.activeTabId + : tabs[0]?.title || null; + + return { ...stored, tabs, activeTabId }; +}; + +export const deleteTabFile = async (title: string) => { + try { + await unlink(join(getNotepadDir(), `${title}.txt`)); + return true; + } catch { + return false; + } +}; + +export const createNewTab = async (title?: string) => { if (!title) { const state = await loadNotepadState(); const noteNumbers = state.tabs - .map((tab: SavedNotepadTab) => { - const match = tab.title.match(/^Note (\d+)$/); - return match ? parseInt(match[1], 10) : 0; - }) - .filter((num: number) => num > 0) - .sort((a: number, b: number) => a - b); + .map((tab) => tab.title.match(/^Note (\d+)$/)?.[1]) + .filter(Boolean) + .map(Number) + .sort((a, b) => a - b); - tabCounter = 1; + let counter = 1; for (const num of noteNumbers) { - if (num === tabCounter) { - tabCounter++; - } else { - break; - } + if (num === counter) counter++; + else break; } + title = `Note ${counter}`; } - const newTab = { - id: `tab-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, - title: title || `Note ${tabCounter++}`, - content: DEFAULT_TAB_CONTENT, - }; - - await saveTabContent(newTab.id, newTab.content); - - return newTab; -} + await saveTabContent(title, DEFAULT_TAB_CONTENT); + return { title, content: DEFAULT_TAB_CONTENT }; +}; diff --git a/src/preload/index.ts b/src/preload/index.ts index 77be374..f0de50d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -191,13 +191,15 @@ const updaterAPI: UpdaterAPI = { }; const notepadAPI: NotepadAPI = { - saveTabContent: (tabId, content) => - ipcRenderer.invoke('notepad:saveTabContent', tabId, content), - loadTabContent: (tabId) => - ipcRenderer.invoke('notepad:loadTabContent', tabId), + saveTabContent: (title, content) => + ipcRenderer.invoke('notepad:saveTabContent', title, content), + loadTabContent: (title) => + ipcRenderer.invoke('notepad:loadTabContent', title), + renameTab: (oldTitle, newTitle) => + ipcRenderer.invoke('notepad:renameTab', oldTitle, newTitle), saveState: (state) => ipcRenderer.invoke('notepad:saveState', state), loadState: () => ipcRenderer.invoke('notepad:loadState'), - deleteTab: (tabId) => ipcRenderer.invoke('notepad:deleteTab', tabId), + deleteTab: (title) => ipcRenderer.invoke('notepad:deleteTab', title), createNewTab: (title) => ipcRenderer.invoke('notepad:createNewTab', title), }; diff --git a/src/stores/notepad.ts b/src/stores/notepad.ts index edb014d..a535ea7 100644 --- a/src/stores/notepad.ts +++ b/src/stores/notepad.ts @@ -9,17 +9,17 @@ import { interface NotepadStore extends NotepadState { isLoaded: boolean; setTabs: (tabs: NotepadTab[]) => void; - setActiveTab: (tabId: string | null) => void; + setActiveTab: (title: string | null) => void; addTab: (tab: NotepadTab) => void; - updateTab: (tabId: string, updates: Partial) => void; - removeTab: (tabId: string) => void; + updateTab: (title: string, updates: Partial) => void; + removeTab: (title: string) => void; reorderTabs: (fromIndex: number, toIndex: number) => void; setPosition: (position: NotepadState['position']) => void; setVisible: (visible: boolean) => void; setShowLineNumbers: (showLineNumbers: boolean) => void; loadState: () => Promise; saveState: () => Promise; - saveTabContent: (tabId: string, content: string) => Promise; + saveTabContent: (title: string, content: string) => Promise; } export const useNotepadStore = create()( @@ -33,45 +33,55 @@ export const useNotepadStore = create()( setTabs: (tabs) => set({ tabs }), - setActiveTab: (tabId) => set({ activeTabId: tabId }), + setActiveTab: (title) => set({ activeTabId: title }), addTab: (tab) => { set((state) => ({ tabs: [...state.tabs, tab], - activeTabId: tab.id, + activeTabId: tab.title, })); }, - updateTab: (tabId, updates) => { - set((state) => ({ - tabs: state.tabs.map((tab) => - tab.id === tabId ? { ...tab, ...updates } : tab - ), - })); + updateTab: (title, updates) => { + set((state) => { + const updatedTabs = state.tabs.map((tab) => + tab.title === title ? { ...tab, ...updates } : tab + ); + + let newActiveTabId = state.activeTabId; + if (updates.title && state.activeTabId === title) { + newActiveTabId = updates.title; + } + + return { + tabs: updatedTabs, + activeTabId: newActiveTabId, + }; + }); }, - removeTab: (tabId) => { + removeTab: (title) => { const state = get(); if (state.tabs.length <= 1) { - const tab = state.tabs.find((t) => t.id === tabId); + const tab = state.tabs.find((t) => t.title === title); if (tab) { set((state) => ({ tabs: state.tabs.map((t) => - t.id === tabId ? { ...t, content: DEFAULT_TAB_CONTENT } : t + t.title === title ? { ...t, content: DEFAULT_TAB_CONTENT } : t ), })); - window.electronAPI.notepad.saveTabContent(tabId, DEFAULT_TAB_CONTENT); + window.electronAPI.notepad.saveTabContent(title, DEFAULT_TAB_CONTENT); } return; } - const newTabs = state.tabs.filter((tab) => tab.id !== tabId); + const newTabs = state.tabs.filter((tab) => tab.title !== title); const newActiveTabId = - state.activeTabId === tabId + state.activeTabId === title ? newTabs.length > 0 - ? newTabs[Math.max(0, newTabs.length - 1)].id + ? newTabs[Math.max(0, newTabs.length - 1)].title : null : state.activeTabId; @@ -80,7 +90,7 @@ export const useNotepadStore = create()( activeTabId: newActiveTabId, }); - window.electronAPI.notepad.deleteTab(tabId); + window.electronAPI.notepad.deleteTab(title); }, reorderTabs: (fromIndex, toIndex) => { @@ -109,10 +119,14 @@ export const useNotepadStore = create()( const savedState = await window.electronAPI.notepad.loadState(); const tabsWithContent = await Promise.all( - savedState.tabs.map(async (tab) => ({ - ...tab, - content: await window.electronAPI.notepad.loadTabContent(tab.id), - })) + savedState.tabs.map((tab) => + window.electronAPI.notepad + .loadTabContent(tab.title) + .then((content) => ({ + ...tab, + content, + })) + ) ); if (tabsWithContent.length === 0) { @@ -122,7 +136,8 @@ export const useNotepadStore = create()( set({ tabs: tabsWithContent, - activeTabId: savedState.activeTabId || tabsWithContent[0]?.id || null, + activeTabId: + savedState.activeTabId || tabsWithContent[0]?.title || null, position: savedState.position, isVisible: savedState.isVisible, showLineNumbers: savedState.showLineNumbers ?? true, @@ -132,7 +147,7 @@ export const useNotepadStore = create()( const defaultTab = await window.electronAPI.notepad.createNewTab(); set({ tabs: [defaultTab], - activeTabId: defaultTab.id, + activeTabId: defaultTab.title, position: DEFAULT_NOTEPAD_POSITION, isVisible: false, isLoaded: true, @@ -145,10 +160,6 @@ export const useNotepadStore = create()( if (!state.isLoaded) return; await window.electronAPI.notepad.saveState({ - tabs: state.tabs.map((tab) => ({ - id: tab.id, - title: tab.title, - })), activeTabId: state.activeTabId, position: state.position, isVisible: state.isVisible, @@ -156,9 +167,9 @@ export const useNotepadStore = create()( }); }, - saveTabContent: async (tabId, content) => { - await window.electronAPI.notepad.saveTabContent(tabId, content); - get().updateTab(tabId, { content }); + saveTabContent: async (title, content) => { + await window.electronAPI.notepad.saveTabContent(title, content); + get().updateTab(title, { content }); }, })) ); diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 9c34990..26dcd26 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -210,13 +210,11 @@ export interface UpdaterAPI { } export interface NotepadTab { - id: string; title: string; content: string; } export interface SavedNotepadTab { - id: string; title: string; } @@ -232,7 +230,6 @@ export interface NotepadState { } export interface SavedNotepadState { - tabs: SavedNotepadTab[]; activeTabId: string | null; position: { width: number; @@ -242,12 +239,17 @@ export interface SavedNotepadState { showLineNumbers?: boolean; } +export interface NotepadStateWithTabs extends SavedNotepadState { + tabs: SavedNotepadTab[]; +} + export interface NotepadAPI { - saveTabContent: (tabId: string, content: string) => Promise; - loadTabContent: (tabId: string) => Promise; + saveTabContent: (title: string, content: string) => Promise; + loadTabContent: (title: string) => Promise; + renameTab: (oldTitle: string, newTitle: string) => Promise; saveState: (state: SavedNotepadState) => Promise; - loadState: () => Promise; - deleteTab: (tabId: string) => Promise; + loadState: () => Promise; + deleteTab: (title: string) => Promise; createNewTab: (title?: string) => Promise; }