aboutsummaryrefslogtreecommitdiff
path: root/src/stores
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-06-05 20:52:54 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-06-05 20:52:54 +0000
commit09f964451d7d92e9891430ec4595c1276d486aab (patch)
tree28da4483f5c28924a8c47fceb648b1baebe88224 /src/stores
downloadyace-master.tar.gz
yace-master.zip
feat: uploadHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'src/stores')
-rw-r--r--src/stores/useCommandStore.ts137
-rw-r--r--src/stores/useContextMenuStore.ts31
-rw-r--r--src/stores/useFilesStore.ts84
-rw-r--r--src/stores/useLayoutStore.ts51
-rw-r--r--src/stores/useSettingsStore.ts34
-rw-r--r--src/stores/useTabsStore.ts152
-rw-r--r--src/stores/useThemeStore.ts41
-rw-r--r--src/stores/useToastStore.ts31
8 files changed, 561 insertions, 0 deletions
diff --git a/src/stores/useCommandStore.ts b/src/stores/useCommandStore.ts
new file mode 100644
index 0000000..2975436
--- /dev/null
+++ b/src/stores/useCommandStore.ts
@@ -0,0 +1,137 @@
+import { create } from 'zustand'
+import { useTabsStore } from './useTabsStore'
+import { useFilesStore } from './useFilesStore'
+import { useLayoutStore } from './useLayoutStore'
+
+export interface Command {
+ id: string
+ label: string
+ shortcut?: string
+ category: string
+ action: () => void
+}
+
+export interface CommandState {
+ isOpen: boolean
+ query: string
+ selectedIndex: number
+ commands: Command[]
+ open: () => void
+ close: () => void
+ toggle: () => void
+ setQuery: (query: string) => void
+ setSelectedIndex: (index: number) => void
+ executeSelected: () => void
+}
+
+function buildCommands(): Command[] {
+ const filesStore = useFilesStore.getState()
+ const layoutStore = useLayoutStore.getState()
+
+ return [
+ {
+ id: 'open-folder',
+ label: 'Open Folder',
+ shortcut: 'Ctrl+Shift+K',
+ category: 'File',
+ action: () => filesStore.openFolder(),
+ },
+ {
+ id: 'toggle-sidebar',
+ label: 'Toggle Sidebar',
+ shortcut: 'Ctrl+B',
+ category: 'View',
+ action: () => layoutStore.toggleSidePanel(),
+ },
+ {
+ id: 'save-file',
+ label: 'Save File',
+ shortcut: 'Ctrl+S',
+ category: 'File',
+ action: () => {
+ const state = useTabsStore.getState()
+ if (state.activeTabId) state.saveFile(state.activeTabId)
+ },
+ },
+ {
+ id: 'close-tab',
+ label: 'Close Current Tab',
+ shortcut: 'Ctrl+W',
+ category: 'File',
+ action: () => {
+ const state = useTabsStore.getState()
+ if (state.activeTabId) state.closeTab(state.activeTabId)
+ },
+ },
+ {
+ id: 'close-all-tabs',
+ label: 'Close All Tabs',
+ shortcut: '',
+ category: 'File',
+ action: () => {
+ const state = useTabsStore.getState()
+ state.tabs.forEach((t) => state.closeTab(t.id))
+ },
+ },
+ {
+ id: 'show-explorer',
+ label: 'Focus Explorer',
+ shortcut: '',
+ category: 'View',
+ action: () => layoutStore.setActiveView('explorer'),
+ },
+ {
+ id: 'show-search',
+ label: 'Show Search',
+ shortcut: '',
+ category: 'View',
+ action: () => layoutStore.setActiveView('search'),
+ },
+ ]
+}
+
+export const useCommandStore = create<CommandState>((set, get) => ({
+ isOpen: false,
+ query: '',
+ selectedIndex: 0,
+ commands: [],
+
+ open: () => {
+ set({ isOpen: true, query: '', selectedIndex: 0, commands: buildCommands() })
+ },
+
+ close: () => {
+ set({ isOpen: false, query: '', selectedIndex: 0 })
+ },
+
+ toggle: () => {
+ const { isOpen } = get()
+ if (isOpen) {
+ get().close()
+ } else {
+ get().open()
+ }
+ },
+
+ setQuery: (query: string) => {
+ set({ query, selectedIndex: 0 })
+ },
+
+ setSelectedIndex: (index: number) => {
+ set({ selectedIndex: index })
+ },
+
+ executeSelected: () => {
+ const { commands, query, selectedIndex } = get()
+ const filtered = commands.filter(
+ (c) =>
+ c.label.toLowerCase().includes(query.toLowerCase()) ||
+ c.category.toLowerCase().includes(query.toLowerCase())
+ )
+ const cmd = filtered[selectedIndex]
+ if (cmd) {
+ cmd.action()
+ get().close()
+ }
+ },
+}))
diff --git a/src/stores/useContextMenuStore.ts b/src/stores/useContextMenuStore.ts
new file mode 100644
index 0000000..a0fd2cf
--- /dev/null
+++ b/src/stores/useContextMenuStore.ts
@@ -0,0 +1,31 @@
+import { create } from 'zustand'
+
+export type ContextMenuIcon = 'copy' | 'cut' | 'paste' | 'undo' | 'redo' | 'selectAll' | 'close' | 'folder' | 'file' | 'link' | 'rename' | 'trash'
+
+export interface ContextMenuItem {
+ id: string
+ label: string
+ shortcut?: string
+ icon?: ContextMenuIcon
+ action: () => void
+ disabled?: boolean
+ separator?: boolean
+}
+
+interface ContextMenuState {
+ isOpen: boolean
+ x: number
+ y: number
+ items: ContextMenuItem[]
+ open: (x: number, y: number, items: ContextMenuItem[]) => void
+ close: () => void
+}
+
+export const useContextMenuStore = create<ContextMenuState>((set) => ({
+ isOpen: false,
+ x: 0,
+ y: 0,
+ items: [],
+ open: (x, y, items) => set({ isOpen: true, x, y, items }),
+ close: () => set({ isOpen: false }),
+}))
diff --git a/src/stores/useFilesStore.ts b/src/stores/useFilesStore.ts
new file mode 100644
index 0000000..200fb68
--- /dev/null
+++ b/src/stores/useFilesStore.ts
@@ -0,0 +1,84 @@
+import { create } from 'zustand'
+import type { FileEntry } from '@/types'
+import { openDir, pickDirectory, writeFile, createDir } from '@/lib/tauri'
+
+interface FilesState {
+ fileTree: FileEntry[]
+ rootPath: string | null
+ loading: boolean
+ openFolder: () => Promise<void>
+ loadFolder: (path: string) => Promise<void>
+ createFile: (parentPath?: string) => Promise<string | null>
+ createFolder: (parentPath?: string) => Promise<string | null>
+}
+
+function generateName(base: string, existing: string[]): string {
+ const used = new Set(existing)
+ let name = base
+ let counter = 1
+ while (used.has(name)) {
+ name = `${base} ${counter++}`
+ }
+ return name
+}
+
+function collectNames(entries: FileEntry[]): string[] {
+ const names: string[] = []
+ for (const e of entries) {
+ names.push(e.name)
+ }
+ return names
+}
+
+export const useFilesStore = create<FilesState>((set, get) => ({
+ fileTree: [],
+ rootPath: null,
+ loading: false,
+
+ openFolder: async () => {
+ const path = await pickDirectory()
+ if (!path) return
+ set({ rootPath: path, loading: true })
+ localStorage.setItem('yace_last_folder', path)
+ try {
+ const entries = await openDir(path)
+ set({ fileTree: entries, loading: false })
+ } catch {
+ set({ loading: false })
+ }
+ },
+
+ loadFolder: async (path: string) => {
+ set({ loading: true })
+ try {
+ const entries = await openDir(path)
+ set({ fileTree: entries, rootPath: path, loading: false })
+ } catch {
+ set({ loading: false })
+ }
+ },
+
+ createFile: async (parentPath?: string): Promise<string | null> => {
+ const { rootPath, fileTree } = get()
+ if (!rootPath) return null
+ const targetDir = parentPath ?? rootPath
+ const name = generateName('Untitled-1', collectNames(fileTree))
+ const filePath = `${targetDir}\\${name}`
+ await writeFile(filePath, '')
+ const entries = await openDir(rootPath)
+ set({ fileTree: entries })
+ return filePath
+ },
+
+ createFolder: async (parentPath?: string): Promise<string | null> => {
+ const { rootPath, fileTree } = get()
+ if (!rootPath) return null
+ const targetDir = parentPath ?? rootPath
+ const name = generateName('New Folder', collectNames(fileTree))
+ const dirPath = `${targetDir}\\${name}`
+ await createDir(dirPath)
+ const entries = await openDir(rootPath)
+ set({ fileTree: entries })
+ return dirPath
+ },
+}))
diff --git a/src/stores/useLayoutStore.ts b/src/stores/useLayoutStore.ts
new file mode 100644
index 0000000..adcdae7
--- /dev/null
+++ b/src/stores/useLayoutStore.ts
@@ -0,0 +1,51 @@
+import { create } from 'zustand'
+
+export type ActivityView = 'explorer' | 'search' | 'extensions' | 'settings'
+
+interface LayoutState {
+ activeView: ActivityView | null
+ sidePanelVisible: boolean
+ sidePanelWidth: number
+ setActiveView: (view: ActivityView | null) => void
+ toggleSidePanel: () => void
+ showSidePanel: () => void
+ hideSidePanel: () => void
+ setSidePanelWidth: (width: number) => void
+}
+
+const SIDEPANEL_MIN = 180
+const SIDEPANEL_MAX = 500
+const SIDEPANEL_DEFAULT = 240
+
+export const useLayoutStore = create<LayoutState>((set, get) => ({
+ activeView: 'explorer',
+ sidePanelVisible: true,
+ sidePanelWidth: SIDEPANEL_DEFAULT,
+
+ setActiveView: (view: ActivityView | null) => {
+ const { activeView, hideSidePanel, showSidePanel } = get()
+ if (activeView === view) {
+ hideSidePanel()
+ set({ activeView: null })
+ } else {
+ set({ activeView: view })
+ showSidePanel()
+ }
+ },
+
+ toggleSidePanel: () => {
+ set((s) => ({ sidePanelVisible: !s.sidePanelVisible }))
+ },
+
+ showSidePanel: () => {
+ set({ sidePanelVisible: true })
+ },
+
+ hideSidePanel: () => {
+ set({ sidePanelVisible: false })
+ },
+
+ setSidePanelWidth: (width: number) => {
+ set({ sidePanelWidth: Math.max(SIDEPANEL_MIN, Math.min(SIDEPANEL_MAX, width)) })
+ },
+}))
diff --git a/src/stores/useSettingsStore.ts b/src/stores/useSettingsStore.ts
new file mode 100644
index 0000000..7f2be4f
--- /dev/null
+++ b/src/stores/useSettingsStore.ts
@@ -0,0 +1,34 @@
+import { create } from 'zustand'
+import { persist } from 'zustand/middleware'
+
+interface SettingsState {
+ fontSize: number
+ tabSize: number
+ fontFamily: string
+ wordWrap: boolean
+ lineNumbers: boolean
+ ruler: boolean
+ rulerColumn: number
+ autoSave: boolean
+ autoSaveDelay: number
+
+ set: <K extends keyof SettingsState>(key: K, value: SettingsState[K]) => void
+}
+
+export const useSettingsStore = create<SettingsState>()(
+ persist(
+ (set) => ({
+ fontSize: 14,
+ tabSize: 2,
+ fontFamily: 'JetBrains Mono',
+ wordWrap: true,
+ lineNumbers: true,
+ ruler: true,
+ rulerColumn: 80,
+ autoSave: false,
+ autoSaveDelay: 2000,
+ set: (key, value) => set({ [key]: value }),
+ }),
+ { name: 'yace-settings' }
+ )
+)
diff --git a/src/stores/useTabsStore.ts b/src/stores/useTabsStore.ts
new file mode 100644
index 0000000..eb9a03a
--- /dev/null
+++ b/src/stores/useTabsStore.ts
@@ -0,0 +1,152 @@
+import { create } from 'zustand'
+import type { Tab } from '@/types'
+import { readFile, writeFile, saveDialog, pickFile } from '@/lib/tauri'
+import { detectLanguage } from '@/lib/languageMap'
+
+interface TabsState {
+ tabs: Tab[]
+ activeTabId: string | null
+ cursorLine: number
+ cursorCol: number
+ savingId: string | null
+ statusMessage: string | null
+ untitledCounter: number
+
+ openFile: (path: string) => Promise<void>
+ pickAndOpenFile: () => Promise<void>
+ createUntitledFile: () => void
+ closeTab: (id: string) => void
+ setActiveTab: (id: string) => void
+ updateContent: (id: string, content: string) => void
+ saveFile: (id: string) => Promise<void>
+ setCursorPosition: (line: number, col: number) => void
+ clearStatusMessage: () => void
+}
+
+function isUntitled(id: string) {
+ return id.startsWith('untitled://')
+}
+
+export const useTabsStore = create<TabsState>((set, get) => ({
+ tabs: [],
+ activeTabId: null,
+ cursorLine: 1,
+ cursorCol: 1,
+ savingId: null,
+ statusMessage: null,
+ untitledCounter: 0,
+
+ openFile: async (path: string) => {
+ const { tabs } = get()
+ const existing = tabs.find((t) => t.path === path)
+ if (existing) {
+ set({ activeTabId: existing.id })
+ return
+ }
+
+ try {
+ const content = await readFile(path)
+ const name = path.split(/[\\/]/).pop() ?? 'untitled'
+ const id = path
+ const tab: Tab = {
+ id,
+ path,
+ name,
+ language: detectLanguage(name),
+ content,
+ modified: false,
+ }
+ set({ tabs: [...tabs, tab], activeTabId: id })
+ } catch (err) {
+ console.error('Failed to open file:', err)
+ }
+ },
+
+ pickAndOpenFile: async () => {
+ const path = await pickFile()
+ if (path) get().openFile(path)
+ },
+
+ createUntitledFile: () => {
+ const { tabs, untitledCounter } = get()
+ const n = untitledCounter + 1
+ const name = `Untitled-${n}`
+ const id = `untitled://${n}`
+ const tab: Tab = {
+ id,
+ path: id,
+ name,
+ language: 'plaintext',
+ content: '',
+ modified: false,
+ }
+ set({ tabs: [...tabs, tab], activeTabId: id, untitledCounter: n })
+ },
+
+ closeTab: (id: string) => {
+ const { tabs, activeTabId } = get()
+ const newTabs = tabs.filter((t) => t.id !== id)
+ let newActive = activeTabId
+ if (activeTabId === id) {
+ const idx = tabs.findIndex((t) => t.id === id)
+ newActive = newTabs[Math.min(idx, newTabs.length - 1)]?.id ?? null
+ }
+ set({ tabs: newTabs, activeTabId: newActive })
+ },
+
+ setActiveTab: (id: string) => {
+ set({ activeTabId: id })
+ },
+
+ updateContent: (id: string, content: string) => {
+ set((state) => ({
+ tabs: state.tabs.map((t) =>
+ t.id === id ? { ...t, content, modified: true } : t
+ ),
+ }))
+ },
+
+ saveFile: async (id: string) => {
+ const { tabs } = get()
+ const tab = tabs.find((t) => t.id === id)
+ if (!tab) return
+
+ set({ savingId: id, statusMessage: null })
+
+ try {
+ let targetPath = tab.path
+
+ if (isUntitled(targetPath)) {
+ const picked = await saveDialog(tab.name)
+ if (!picked) {
+ set({ savingId: null })
+ return
+ }
+ targetPath = picked
+ }
+
+ await writeFile(targetPath, tab.content)
+ const name = targetPath.split(/[\\/]/).pop() ?? tab.name
+ set({
+ tabs: tabs.map((t) =>
+ t.id === id
+ ? { ...t, path: targetPath, name, language: detectLanguage(name), modified: false }
+ : t
+ ),
+ savingId: null,
+ statusMessage: 'Saved',
+ })
+ } catch (err) {
+ set({ savingId: null, statusMessage: 'Save failed' })
+ console.error('Failed to save file:', err)
+ }
+ },
+
+ setCursorPosition: (line: number, col: number) => {
+ set({ cursorLine: line, cursorCol: col })
+ },
+
+ clearStatusMessage: () => {
+ set({ statusMessage: null })
+ },
+}))
diff --git a/src/stores/useThemeStore.ts b/src/stores/useThemeStore.ts
new file mode 100644
index 0000000..d9c3d06
--- /dev/null
+++ b/src/stores/useThemeStore.ts
@@ -0,0 +1,41 @@
+import { create } from 'zustand'
+
+export type Theme = 'dark' | 'light'
+
+interface ThemeState {
+ theme: Theme
+ setTheme: (theme: Theme) => void
+ toggleTheme: () => void
+}
+
+function getInitialTheme(): Theme {
+ const stored = localStorage.getItem('yace_theme')
+ if (stored === 'light' || stored === 'dark') return stored
+ return 'dark'
+}
+
+function applyTheme(theme: Theme) {
+ document.documentElement.setAttribute('data-theme', theme)
+}
+
+const initial = getInitialTheme()
+applyTheme(initial)
+
+export const useThemeStore = create<ThemeState>((set) => ({
+ theme: initial,
+
+ setTheme: (theme: Theme) => {
+ applyTheme(theme)
+ localStorage.setItem('yace_theme', theme)
+ set({ theme })
+ },
+
+ toggleTheme: () => {
+ set((state) => {
+ const next = state.theme === 'dark' ? 'light' : 'dark'
+ applyTheme(next)
+ localStorage.setItem('yace_theme', next)
+ return { theme: next }
+ })
+ },
+}))
diff --git a/src/stores/useToastStore.ts b/src/stores/useToastStore.ts
new file mode 100644
index 0000000..ab45325
--- /dev/null
+++ b/src/stores/useToastStore.ts
@@ -0,0 +1,31 @@
+import { create } from 'zustand'
+
+export type ToastType = 'success' | 'error' | 'info' | 'warning'
+
+export interface Toast {
+ id: string
+ message: string
+ type: ToastType
+}
+
+interface ToastState {
+ toasts: Toast[]
+ addToast: (message: string, type?: ToastType, duration?: number) => void
+ removeToast: (id: string) => void
+}
+
+let counter = 0
+
+export const useToastStore = create<ToastState>((set, get) => ({
+ toasts: [],
+ addToast: (message, type = 'info', duration = 3500) => {
+ const id = `toast-${++counter}`
+ set((s) => ({ toasts: [...s.toasts, { id, message, type }] }))
+ setTimeout(() => {
+ get().removeToast(id)
+ }, duration)
+ },
+ removeToast: (id) => {
+ set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }))
+ },
+}))