- {#if currentProcess.environ.length > 0}
+ {#if process.environ.length > 0}
Environment:
- {#each currentProcess.environ as env}
+ {#each process.environ as env}
{env}
{/each}
diff --git a/src/lib/components/modals/index.ts b/src/lib/components/modals/index.ts
new file mode 100644
index 0000000..1de7682
--- /dev/null
+++ b/src/lib/components/modals/index.ts
@@ -0,0 +1,3 @@
+export { default as Modal } from "./Modal.svelte";
+export { default as ProcessDetailsModal } from "./ProcessDetailsModal.svelte";
+export { default as KillProcessModal } from "./KillProcessModal.svelte";
diff --git a/src/lib/components/process/ActionButtons.svelte b/src/lib/components/process/ActionButtons.svelte
new file mode 100644
index 0000000..2c28fd5
--- /dev/null
+++ b/src/lib/components/process/ActionButtons.svelte
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+ |
+
+
diff --git a/src/lib/components/process/ProcessIcon.svelte b/src/lib/components/process/ProcessIcon.svelte
new file mode 100644
index 0000000..a4c71a6
--- /dev/null
+++ b/src/lib/components/process/ProcessIcon.svelte
@@ -0,0 +1,84 @@
+
+
+
})
+
+
diff --git a/src/lib/components/process/ProcessRow.svelte b/src/lib/components/process/ProcessRow.svelte
new file mode 100644
index 0000000..1f60e0e
--- /dev/null
+++ b/src/lib/components/process/ProcessRow.svelte
@@ -0,0 +1,79 @@
+
+
+
+ {#each columns.filter((col) => col.visible) as column}
+
+ {#if column.id === "name"}
+
+ {:else if column.format}
+ {@html column.format(process[column.id])}
+ {:else}
+ {process[column.id]}
+ {/if}
+ |
+ {/each}
+
+
+
+
diff --git a/src/lib/components/process/ProcessTable.svelte b/src/lib/components/process/ProcessTable.svelte
new file mode 100644
index 0000000..f98932e
--- /dev/null
+++ b/src/lib/components/process/ProcessTable.svelte
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+ {#each processes as process (process.pid)}
+ 50 ||
+ process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
+ {onTogglePin}
+ {onShowDetails}
+ {onKillProcess}
+ />
+ {/each}
+
+
+
+
+
diff --git a/src/lib/components/process/TableHeader.svelte b/src/lib/components/process/TableHeader.svelte
new file mode 100644
index 0000000..58bdea4
--- /dev/null
+++ b/src/lib/components/process/TableHeader.svelte
@@ -0,0 +1,79 @@
+
+
+
+
+ {#each columns.filter((col) => col.visible) as column}
+ onToggleSort(column.id)}>
+
+ {column.label}
+
+ {getSortIndicator(column.id)}
+
+
+ |
+ {/each}
+ Actions |
+
+
+
+
diff --git a/src/lib/components/process/index.ts b/src/lib/components/process/index.ts
new file mode 100644
index 0000000..0dbed06
--- /dev/null
+++ b/src/lib/components/process/index.ts
@@ -0,0 +1,5 @@
+export { default as ProcessTable } from "./ProcessTable.svelte";
+export { default as ProcessRow } from "./ProcessRow.svelte";
+export { default as TableHeader } from "./TableHeader.svelte";
+export { default as ActionButtons } from "./ActionButtons.svelte";
+export { default as ProcessIcon } from "./ProcessIcon.svelte";
diff --git a/src/lib/components/stats/CpuPanel.svelte b/src/lib/components/stats/CpuPanel.svelte
new file mode 100644
index 0000000..4a88f62
--- /dev/null
+++ b/src/lib/components/stats/CpuPanel.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+
+ {#each cpuUsage as usage, i}
+
+ {/each}
+
+
+
+
diff --git a/src/lib/components/stats/MemoryPanel.svelte b/src/lib/components/stats/MemoryPanel.svelte
new file mode 100644
index 0000000..e329670
--- /dev/null
+++ b/src/lib/components/stats/MemoryPanel.svelte
@@ -0,0 +1,56 @@
+
+
+
+
+
diff --git a/src/lib/components/stats/NetworkPanel.svelte b/src/lib/components/stats/NetworkPanel.svelte
new file mode 100644
index 0000000..2e2d0a7
--- /dev/null
+++ b/src/lib/components/stats/NetworkPanel.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
diff --git a/src/lib/components/stats/PanelHeader.svelte b/src/lib/components/stats/PanelHeader.svelte
new file mode 100644
index 0000000..11798df
--- /dev/null
+++ b/src/lib/components/stats/PanelHeader.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/src/lib/components/stats/ProgressBar.svelte b/src/lib/components/stats/ProgressBar.svelte
new file mode 100644
index 0000000..830d21d
--- /dev/null
+++ b/src/lib/components/stats/ProgressBar.svelte
@@ -0,0 +1,80 @@
+
+
+
+
{label}
+
+
{Math.round(value)}%
+
+
+
diff --git a/src/lib/components/stats/StatItem.svelte b/src/lib/components/stats/StatItem.svelte
new file mode 100644
index 0000000..d575317
--- /dev/null
+++ b/src/lib/components/stats/StatItem.svelte
@@ -0,0 +1,30 @@
+
+
+
+ {label}
+ {value}
+
+
+
diff --git a/src/lib/components/stats/StatPanel.svelte b/src/lib/components/stats/StatPanel.svelte
new file mode 100644
index 0000000..90cdc20
--- /dev/null
+++ b/src/lib/components/stats/StatPanel.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
diff --git a/src/lib/components/stats/StatsBar.svelte b/src/lib/components/stats/StatsBar.svelte
new file mode 100644
index 0000000..b4fa191
--- /dev/null
+++ b/src/lib/components/stats/StatsBar.svelte
@@ -0,0 +1,52 @@
+
+
+
+ {#if systemStats}
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
+
+
+
diff --git a/src/lib/components/stats/StoragePanel.svelte b/src/lib/components/stats/StoragePanel.svelte
new file mode 100644
index 0000000..9ed0431
--- /dev/null
+++ b/src/lib/components/stats/StoragePanel.svelte
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/src/lib/components/stats/SystemPanel.svelte b/src/lib/components/stats/SystemPanel.svelte
new file mode 100644
index 0000000..b5fd204
--- /dev/null
+++ b/src/lib/components/stats/SystemPanel.svelte
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/src/lib/components/stats/index.ts b/src/lib/components/stats/index.ts
new file mode 100644
index 0000000..e79623e
--- /dev/null
+++ b/src/lib/components/stats/index.ts
@@ -0,0 +1,9 @@
+export { default as StatsBar } from "./StatsBar.svelte";
+export { default as CpuPanel } from "./CpuPanel.svelte";
+export { default as MemoryPanel } from "./MemoryPanel.svelte";
+export { default as StoragePanel } from "./StoragePanel.svelte";
+export { default as SystemPanel } from "./SystemPanel.svelte";
+export { default as NetworkPanel } from "./NetworkPanel.svelte";
+export { default as PanelHeader } from "./PanelHeader.svelte";
+export { default as ProgressBar } from "./ProgressBar.svelte";
+export { default as StatItem } from "./StatItem.svelte";
diff --git a/src/lib/components/toolbar/ColumnToggle.svelte b/src/lib/components/toolbar/ColumnToggle.svelte
new file mode 100644
index 0000000..e055a74
--- /dev/null
+++ b/src/lib/components/toolbar/ColumnToggle.svelte
@@ -0,0 +1,172 @@
+
+
+
+
+
+ {#if showColumnMenu}
+
+
(showColumnMenu = false)}>
+ {#each columns as column}
+
+ {/each}
+
+ {/if}
+
+
+
diff --git a/src/lib/components/toolbar/PaginationControls.svelte b/src/lib/components/toolbar/PaginationControls.svelte
new file mode 100644
index 0000000..a9b05ae
--- /dev/null
+++ b/src/lib/components/toolbar/PaginationControls.svelte
@@ -0,0 +1,148 @@
+
+
+
+
+
diff --git a/src/lib/components/toolbar/RefreshControls.svelte b/src/lib/components/toolbar/RefreshControls.svelte
new file mode 100644
index 0000000..b9d66b2
--- /dev/null
+++ b/src/lib/components/toolbar/RefreshControls.svelte
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/toolbar/SearchBox.svelte b/src/lib/components/toolbar/SearchBox.svelte
new file mode 100644
index 0000000..cb5acdf
--- /dev/null
+++ b/src/lib/components/toolbar/SearchBox.svelte
@@ -0,0 +1,90 @@
+
+
+
+
+
+ {#if searchTerm}
+
+ {/if}
+
+
+
+
diff --git a/src/lib/components/toolbar/StatusFilter.svelte b/src/lib/components/toolbar/StatusFilter.svelte
new file mode 100644
index 0000000..0925f5e
--- /dev/null
+++ b/src/lib/components/toolbar/StatusFilter.svelte
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
diff --git a/src/lib/components/toolbar/ToolBar.svelte b/src/lib/components/toolbar/ToolBar.svelte
new file mode 100644
index 0000000..c740517
--- /dev/null
+++ b/src/lib/components/toolbar/ToolBar.svelte
@@ -0,0 +1,68 @@
+
+
+
+
+
diff --git a/src/lib/components/toolbar/index.ts b/src/lib/components/toolbar/index.ts
new file mode 100644
index 0000000..880daaa
--- /dev/null
+++ b/src/lib/components/toolbar/index.ts
@@ -0,0 +1,6 @@
+export { default as ToolBar } from "./ToolBar.svelte";
+export { default as SearchBox } from "./SearchBox.svelte";
+export { default as StatusFilter } from "./StatusFilter.svelte";
+export { default as PaginationControls } from "./PaginationControls.svelte";
+export { default as ColumnToggle } from "./ColumnToggle.svelte";
+export { default as RefreshControls } from "./RefreshControls.svelte";
diff --git a/src/lib/constants/index.ts b/src/lib/constants/index.ts
new file mode 100644
index 0000000..f8b24ba
--- /dev/null
+++ b/src/lib/constants/index.ts
@@ -0,0 +1,71 @@
+export const ASCII_ART = `
+███╗ ██╗███████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗
+████╗ ██║██╔════╝██╔═══██╗██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗
+██╔██╗ ██║█████╗ ██║ ██║███████║ ██║ ██║ ██║██████╔╝
+██║╚██╗██║██╔══╝ ██║ ██║██╔══██║ ██║ ██║ ██║██╔═══╝
+██║ ╚████║███████╗╚██████╔╝██║ ██║ ██║ ╚██████╔╝██║
+╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝
+`;
+
+export const APP_INFO = {
+ name: "NeoHtop",
+ developer: "Abdenasser",
+ github: "https://github.com/Abdenasser/neohtop",
+ stack: ["Tauri", "Rust", "Svelte", "TypeScript"],
+};
+
+export const ITEMS_PER_PAGE_OPTIONS = [15, 25, 50, 100, 250, 500];
+
+export const REFRESH_RATE_OPTIONS = [
+ { value: 1000, label: "1s" },
+ { value: 2000, label: "2s" },
+ { value: 5000, label: "5s" },
+ { value: 10000, label: "10s" },
+ { value: 30000, label: "30s" },
+];
+
+export const STATUS_OPTIONS = [
+ { value: "all", label: "All Statuses" },
+ { value: "running", label: "Running" },
+ { value: "sleeping", label: "Sleeping" },
+ { value: "idle", label: "Idle" },
+ { value: "unknown", label: "Unknown" },
+];
+
+export const THEME_GROUPS = [
+ {
+ label: "Dark",
+ themes: [
+ "catppuccin",
+ "dracula",
+ "monokaiPro",
+ "tokyoNight",
+ "ayuDark",
+ "ayuMirage",
+ ],
+ },
+ {
+ label: "Light",
+ themes: ["githubLight", "solarizedLight", "oneLight", "ayuLight"],
+ },
+ {
+ label: "Warm",
+ themes: ["gruvbox"],
+ },
+ {
+ label: "Cool",
+ themes: ["nord", "oneDark"],
+ },
+ {
+ label: "Fun",
+ themes: ["bubblegum", "rosePine", "cottonCandy", "synthwave", "candyfloss"],
+ },
+ {
+ label: "Retro",
+ themes: ["terminal", "amber", "ibmPC"],
+ },
+ {
+ label: "Accessibility",
+ themes: ["highContrast"],
+ },
+];
diff --git a/src/lib/definitions/columns.ts b/src/lib/definitions/columns.ts
new file mode 100644
index 0000000..61b86d6
--- /dev/null
+++ b/src/lib/definitions/columns.ts
@@ -0,0 +1,61 @@
+import type { Column } from "$lib/types";
+import { formatMemorySize } from "$lib/utils";
+
+export let column_definitions: Column[] = [
+ { id: "name", label: "Process Name", visible: true, required: true },
+ { id: "pid", label: "PID", visible: true, required: false },
+ {
+ id: "status",
+ label: "Status",
+ visible: true,
+ },
+ { id: "user", label: "User", visible: true },
+ {
+ id: "cpu_usage",
+ label: "CPU %",
+ visible: true,
+ format: (v) => v.toFixed(1) + "%",
+ },
+ {
+ id: "memory_usage",
+ label: "RAM",
+ visible: true,
+ format: (v) => (v / (1024 * 1024)).toFixed(1) + " MB",
+ },
+ {
+ id: "virtual_memory",
+ label: "VIRT",
+ visible: true,
+ format: (v) => formatMemorySize(v),
+ },
+ {
+ id: "disk_usage",
+ label: "Disk R/W",
+ visible: true,
+ format: (v) =>
+ `${(v[0] / (1024 * 1024)).toFixed(1)} / ${(v[1] / (1024 * 1024)).toFixed(1)} MB`,
+ },
+ { id: "ppid", label: "Parent PID", visible: false },
+ { id: "root", label: "Root", visible: false },
+ { id: "command", label: "Command", visible: false },
+ { id: "environ", label: "Environment Variables", visible: false },
+ { id: "session_id", label: "Session ID", visible: false },
+ {
+ id: "start_time",
+ label: "Start Time",
+ visible: false,
+ format: (v) => new Date(v * 1000).toLocaleString(), // v is the time where the process was started (in seconds) from epoch
+ },
+ {
+ id: "run_time",
+ label: "Run Time",
+ visible: true,
+ format: (v) => {
+ const seconds = v; // v is the time the process has been running in seconds
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const remainingSeconds = seconds % 60;
+ return `${hours}h ${minutes}m ${remainingSeconds}s`; // Format as HH:MM:SS
+ },
+ },
+];
diff --git a/src/lib/definitions/index.ts b/src/lib/definitions/index.ts
new file mode 100644
index 0000000..bdc4c1b
--- /dev/null
+++ b/src/lib/definitions/index.ts
@@ -0,0 +1,3 @@
+export * from "./columns";
+export * from "./settings";
+export * from "./themes";
diff --git a/src/lib/types/config.ts b/src/lib/definitions/settings.ts
similarity index 72%
rename from src/lib/types/config.ts
rename to src/lib/definitions/settings.ts
index 9affb5f..fcd489b 100644
--- a/src/lib/types/config.ts
+++ b/src/lib/definitions/settings.ts
@@ -1,13 +1,4 @@
-export interface AppConfig {
- appearance: {
- columnVisibility: Record
;
- };
- behavior: {
- itemsPerPage: number;
- refreshRate: number;
- defaultStatusFilter: string;
- };
-}
+import type { AppConfig } from "$lib/types";
export const DEFAULT_CONFIG: AppConfig = {
appearance: {
diff --git a/src/lib/styles/index.ts b/src/lib/definitions/themes.ts
similarity index 97%
rename from src/lib/styles/index.ts
rename to src/lib/definitions/themes.ts
index 8143a4f..4fc7188 100644
--- a/src/lib/styles/index.ts
+++ b/src/lib/definitions/themes.ts
@@ -1,30 +1,4 @@
-export interface Theme {
- name: string;
- label: string;
- colors: {
- base: string;
- mantle: string;
- crust: string;
- text: string;
- subtext0: string;
- subtext1: string;
- surface0: string;
- surface1: string;
- surface2: string;
- overlay0: string;
- overlay1: string;
- blue: string;
- lavender: string;
- sapphire: string;
- sky: string;
- red: string;
- maroon: string;
- peach: string;
- yellow: string;
- green: string;
- teal: string;
- };
-}
+import type { Theme } from "$lib/types";
export const themes: Record = {
catppuccin: {
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts
index 6087dbb..d8836d7 100644
--- a/src/lib/stores/index.ts
+++ b/src/lib/stores/index.ts
@@ -1,58 +1,3 @@
-import { writable } from "svelte/store";
-import { themes, type Theme } from "$lib/styles";
-
-function createThemeStore() {
- // Default theme
- const defaultTheme = themes.catppuccin;
-
- // Initialize with default theme
- const { subscribe, set } = writable(defaultTheme);
-
- // Initialize theme on client-side only
- if (typeof window !== "undefined") {
- const storedTheme = localStorage.getItem("theme");
- if (storedTheme && themes[storedTheme]) {
- set(themes[storedTheme]);
- }
- }
-
- return {
- subscribe,
- setTheme: (themeName: string) => {
- const theme = themes[themeName];
- if (theme) {
- if (typeof window !== "undefined") {
- localStorage.setItem("theme", themeName);
- // Add this line to set the data-theme attribute
- document.documentElement.setAttribute("data-theme", themeName);
- }
- set(theme);
- applyTheme(theme);
- }
- },
- init: () => {
- const storedTheme =
- typeof window !== "undefined" ? localStorage.getItem("theme") : null;
- const theme = (storedTheme && themes[storedTheme]) || defaultTheme;
- if (typeof window !== "undefined") {
- // Add this line to set the data-theme attribute on init
- document.documentElement.setAttribute(
- "data-theme",
- storedTheme || "catppuccin",
- );
- }
- applyTheme(theme);
- },
- };
-}
-
-function applyTheme(theme: Theme) {
- if (typeof window !== "undefined") {
- const root = document.documentElement;
- Object.entries(theme.colors).forEach(([key, value]) => {
- root.style.setProperty(`--${key}`, value);
- });
- }
-}
-
-export const themeStore = createThemeStore();
+export * from "./processes";
+export * from "./theme";
+export * from "./settings";
diff --git a/src/lib/stores/processes.ts b/src/lib/stores/processes.ts
new file mode 100644
index 0000000..ec263f0
--- /dev/null
+++ b/src/lib/stores/processes.ts
@@ -0,0 +1,218 @@
+import { writable, derived } from "svelte/store";
+import type { Process, SystemStats } from "$lib/types";
+import { invoke } from "@tauri-apps/api/core";
+
+interface ProcessStore {
+ processes: Process[];
+ systemStats: SystemStats | null;
+ error: string | null;
+ isLoading: boolean;
+ searchTerm: string;
+ currentPage: number;
+ pinnedProcesses: Set;
+ selectedProcess: Process | null;
+ showInfoModal: boolean;
+ showConfirmModal: boolean;
+ processToKill: Process | null;
+ isKilling: boolean;
+ isFrozen: boolean;
+ selectedProcessPid: number | null;
+ sortConfig: {
+ field: keyof Process;
+ direction: "asc" | "desc";
+ };
+}
+
+const initialState: ProcessStore = {
+ processes: [],
+ systemStats: null,
+ error: null,
+ isLoading: true,
+ searchTerm: "",
+ currentPage: 1,
+ pinnedProcesses: new Set(),
+ selectedProcess: null,
+ showInfoModal: false,
+ showConfirmModal: false,
+ processToKill: null,
+ isKilling: false,
+ isFrozen: false,
+ selectedProcessPid: null,
+ sortConfig: {
+ field: "cpu_usage",
+ direction: "desc",
+ },
+};
+
+function createProcessStore() {
+ const { subscribe, set, update } = writable(initialState);
+
+ // Define all methods first
+ const setIsLoading = (isLoading: boolean) =>
+ update((state) => ({ ...state, isLoading }));
+
+ const getProcesses = async () => {
+ try {
+ const result = await invoke<[Process[], SystemStats]>("get_processes");
+ update((state) => {
+ let updatedSelectedProcess = state.selectedProcess;
+ if (state.selectedProcessPid) {
+ updatedSelectedProcess =
+ result[0].find((p) => p.pid === state.selectedProcessPid) || null;
+ }
+
+ return {
+ ...state,
+ processes: result[0],
+ systemStats: result[1],
+ error: null,
+ selectedProcess: updatedSelectedProcess,
+ };
+ });
+ } catch (e: unknown) {
+ update((state) => ({
+ ...state,
+ error: e instanceof Error ? e.message : String(e),
+ }));
+ }
+ };
+
+ const killProcess = async (pid: number) => {
+ try {
+ update((state) => ({ ...state, isKilling: true }));
+ const success = await invoke("kill_process", { pid });
+ if (success) {
+ await getProcesses();
+ } else {
+ throw new Error("Failed to kill process");
+ }
+ } catch (e: unknown) {
+ update((state) => ({
+ ...state,
+ error: e instanceof Error ? e.message : String(e),
+ }));
+ } finally {
+ update((state) => ({ ...state, isKilling: false }));
+ }
+ };
+
+ const toggleSort = (field: keyof Process) => {
+ update((state) => ({
+ ...state,
+ sortConfig: {
+ field,
+ direction:
+ state.sortConfig.field === field
+ ? state.sortConfig.direction === "asc"
+ ? "desc"
+ : "asc"
+ : "desc",
+ },
+ }));
+ };
+
+ const togglePin = (command: string) => {
+ update((state) => {
+ const newPinnedProcesses = new Set(state.pinnedProcesses);
+ if (newPinnedProcesses.has(command)) {
+ newPinnedProcesses.delete(command);
+ } else {
+ newPinnedProcesses.add(command);
+ }
+ return { ...state, pinnedProcesses: newPinnedProcesses };
+ });
+ };
+
+ const setSearchTerm = (searchTerm: string) =>
+ update((state) => ({ ...state, searchTerm, currentPage: 1 }));
+
+ const setIsFrozen = (isFrozen: boolean) =>
+ update((state) => ({ ...state, isFrozen }));
+
+ const setCurrentPage = (currentPage: number) =>
+ update((state) => ({ ...state, currentPage }));
+
+ const showProcessDetails = (process: Process) => {
+ update((state) => ({
+ ...state,
+ selectedProcessPid: process.pid,
+ selectedProcess: process,
+ showInfoModal: true,
+ }));
+ };
+
+ const closeProcessDetails = () => {
+ update((state) => ({
+ ...state,
+ showInfoModal: false,
+ selectedProcess: null,
+ selectedProcessPid: null,
+ }));
+ };
+
+ const confirmKillProcess = (process: Process) => {
+ update((state) => ({
+ ...state,
+ processToKill: process,
+ showConfirmModal: true,
+ }));
+ };
+
+ const closeConfirmKill = () => {
+ update((state) => ({
+ ...state,
+ showConfirmModal: false,
+ processToKill: null,
+ }));
+ };
+
+ const handleConfirmKill = async () => {
+ let processToKill: Process | null = null;
+
+ let currentState: ProcessStore | undefined;
+ const unsubscribe = subscribe((state) => {
+ currentState = state;
+ });
+ unsubscribe();
+
+ if (currentState?.processToKill && "pid" in currentState.processToKill) {
+ processToKill = currentState.processToKill;
+ }
+
+ if (!processToKill?.pid) {
+ return;
+ }
+
+ try {
+ await killProcess(processToKill.pid);
+ } finally {
+ update((state) => ({
+ ...state,
+ showConfirmModal: false,
+ processToKill: null,
+ }));
+ }
+ };
+
+ // Return all methods
+ return {
+ subscribe,
+ set,
+ update,
+ setIsLoading,
+ getProcesses,
+ killProcess,
+ toggleSort,
+ togglePin,
+ setSearchTerm,
+ setIsFrozen,
+ setCurrentPage,
+ showProcessDetails,
+ closeProcessDetails,
+ confirmKillProcess,
+ closeConfirmKill,
+ handleConfirmKill,
+ };
+}
+
+export const processStore = createProcessStore();
diff --git a/src/lib/stores/config.ts b/src/lib/stores/settings.ts
similarity index 82%
rename from src/lib/stores/config.ts
rename to src/lib/stores/settings.ts
index 55037b9..f7e500f 100644
--- a/src/lib/stores/config.ts
+++ b/src/lib/stores/settings.ts
@@ -1,8 +1,8 @@
import { writable } from "svelte/store";
-import type { AppConfig } from "$lib/types/config";
-import { DEFAULT_CONFIG } from "$lib/types/config";
+import type { AppConfig } from "$lib/types";
+import { DEFAULT_CONFIG } from "$lib/definitions/settings";
-function createConfigStore() {
+function createSettingsStore() {
const { subscribe, set, update } = writable(DEFAULT_CONFIG);
return {
@@ -33,4 +33,4 @@ function createConfigStore() {
};
}
-export const configStore = createConfigStore();
+export const settingsStore = createSettingsStore();
diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts
new file mode 100644
index 0000000..735f874
--- /dev/null
+++ b/src/lib/stores/theme.ts
@@ -0,0 +1,58 @@
+import { writable } from "svelte/store";
+import { themes } from "$lib/definitions/themes";
+import type { Theme } from "$lib/types";
+function createThemeStore() {
+ // Default theme
+ const defaultTheme = themes.catppuccin;
+
+ // Initialize with default theme
+ const { subscribe, set } = writable(defaultTheme);
+
+ // Initialize theme on client-side only
+ if (typeof window !== "undefined") {
+ const storedTheme = localStorage.getItem("theme");
+ if (storedTheme && themes[storedTheme]) {
+ set(themes[storedTheme]);
+ }
+ }
+
+ return {
+ subscribe,
+ setTheme: (themeName: string) => {
+ const theme = themes[themeName];
+ if (theme) {
+ if (typeof window !== "undefined") {
+ localStorage.setItem("theme", themeName);
+ // Add this line to set the data-theme attribute
+ document.documentElement.setAttribute("data-theme", themeName);
+ }
+ set(theme);
+ applyTheme(theme);
+ }
+ },
+ init: () => {
+ const storedTheme =
+ typeof window !== "undefined" ? localStorage.getItem("theme") : null;
+ const theme = (storedTheme && themes[storedTheme]) || defaultTheme;
+ if (typeof window !== "undefined") {
+ // Add this line to set the data-theme attribute on init
+ document.documentElement.setAttribute(
+ "data-theme",
+ storedTheme || "catppuccin",
+ );
+ }
+ applyTheme(theme);
+ },
+ };
+}
+
+function applyTheme(theme: Theme) {
+ if (typeof window !== "undefined") {
+ const root = document.documentElement;
+ Object.entries(theme.colors).forEach(([key, value]) => {
+ root.style.setProperty(`--${key}`, value);
+ });
+ }
+}
+
+export const themeStore = createThemeStore();
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
index d9f3f2d..9305937 100644
--- a/src/lib/types/index.ts
+++ b/src/lib/types/index.ts
@@ -40,3 +40,76 @@ export interface Column {
required?: boolean;
format?: (value: any) => string;
}
+
+export interface Theme {
+ name: string;
+ label: string;
+ colors: {
+ base: string;
+ mantle: string;
+ crust: string;
+ text: string;
+ subtext0: string;
+ subtext1: string;
+ surface0: string;
+ surface1: string;
+ surface2: string;
+ overlay0: string;
+ overlay1: string;
+ blue: string;
+ lavender: string;
+ sapphire: string;
+ sky: string;
+ red: string;
+ maroon: string;
+ peach: string;
+ yellow: string;
+ green: string;
+ teal: string;
+ };
+}
+
+export interface AppConfig {
+ appearance: {
+ columnVisibility: Record;
+ };
+ behavior: {
+ itemsPerPage: number;
+ refreshRate: number;
+ defaultStatusFilter: string;
+ };
+}
+
+export interface ColumnDefinition {
+ id: string;
+ label: string;
+ visible: boolean;
+ required?: boolean;
+}
+
+export interface StatusOption {
+ value: string;
+ label: string;
+}
+
+export interface RefreshRateOption {
+ value: number;
+ label: string;
+}
+
+export interface ToolBarProps {
+ searchTerm: string;
+ statusFilter: string;
+ itemsPerPage: number;
+ currentPage: number;
+ totalPages: number;
+ totalResults: number;
+ columns: ColumnDefinition[];
+ refreshRate: number;
+ isFrozen: boolean;
+}
+
+export interface SortConfig {
+ field: keyof Process;
+ direction: "asc" | "desc";
+}
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts
index 15dedd1..80c1e4f 100644
--- a/src/lib/utils/index.ts
+++ b/src/lib/utils/index.ts
@@ -1,39 +1,12 @@
+import type { Process } from "$lib/types";
+import type { SortConfig } from "$lib/types";
+
export interface ProcessStatus {
label: string;
emoji: string;
color: string;
}
-export const statusMap: Record = {
- Running: {
- label: "Running",
- emoji: "🏃",
- color: "var(--green)",
- },
- Sleeping: {
- label: "Sleeping",
- emoji: "😴",
- color: "var(--blue)",
- },
- Idle: {
- label: "Idle",
- emoji: "⌛",
- color: "var(--overlay0)",
- },
- Unknown: {
- label: "Unknown",
- emoji: "❓",
- color: "var(--overlay0)",
- },
-};
-
-export function formatStatus(status: string): string {
- const processStatus = statusMap[status] || statusMap.Unknown;
- return `
- ${processStatus.label}
- `;
-}
-
export function formatMemorySize(bytes: number): string {
const gb = bytes / (1024 * 1024 * 1024);
return `${gb.toFixed(1)} GB`;
@@ -56,3 +29,123 @@ export function getUsageClass(percentage: number): string {
if (percentage >= 30) return "medium";
return "low";
}
+
+export function formatBytes(bytes: number): string {
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ let value = bytes;
+ let unitIndex = 0;
+
+ while (value >= 1024 && unitIndex < units.length - 1) {
+ value /= 1024;
+ unitIndex++;
+ }
+
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
+}
+
+export function formatDate(timestamp: number) {
+ return new Date(timestamp * 1000).toLocaleString();
+}
+
+// Cache for compiled regex patterns
+const regexCache = new Map();
+
+export function filterProcesses(
+ processes: Process[],
+ searchTerm: string,
+ statusFilter: string,
+): Process[] {
+ // Early return for empty search and "all" status
+ if (searchTerm.length === 0 && statusFilter === "all") {
+ return processes;
+ }
+
+ // Pre-process search terms once
+ const terms =
+ searchTerm.length > 0
+ ? searchTerm.split(",").map((term) => term.trim())
+ : [];
+
+ return processes.filter((process) => {
+ // Early status check
+ if (
+ statusFilter !== "all" &&
+ process.status.toLowerCase() !== statusFilter
+ ) {
+ return false;
+ }
+
+ // Skip search if no terms
+ if (terms.length === 0) {
+ return true;
+ }
+
+ // Cache lowercase values
+ const processNameLower = process.name.toLowerCase();
+ const processCommandLower = process.command.toLowerCase();
+ const processPidString = process.pid.toString();
+
+ // Check each term
+ return terms.some((term) => {
+ const termLower = term.toLowerCase();
+
+ // Try exact matches first (faster)
+ if (
+ processNameLower.includes(termLower) ||
+ processCommandLower.includes(termLower) ||
+ processPidString.includes(term)
+ ) {
+ return true;
+ }
+
+ // Try regex match last (slower)
+ try {
+ let regex = regexCache.get(term);
+ if (!regex) {
+ regex = new RegExp(term, "i");
+ regexCache.set(term, regex);
+ }
+ return regex.test(process.name);
+ } catch {
+ return false;
+ }
+ });
+ });
+}
+
+// Create a Map for quick pinned status lookup
+const isPinned = new Map();
+
+export function sortProcesses(
+ processes: Process[],
+ sortConfig: SortConfig,
+ pinnedProcesses: Set,
+): Process[] {
+ // Clear the cache before sorting
+ isPinned.clear();
+
+ return [...processes].sort((a, b) => {
+ // Cache pinned status
+ let aPin = pinnedProcesses.has(a.command);
+ isPinned.set(a.command, aPin);
+
+ let bPin = pinnedProcesses.has(b.command);
+ isPinned.set(b.command, bPin);
+
+ // Quick pin comparison
+ if (aPin !== bPin) {
+ return aPin ? -1 : 1;
+ }
+
+ // Only compute direction once
+ const direction = sortConfig.direction === "asc" ? 1 : -1;
+ const aValue = a[sortConfig.field];
+ const bValue = b[sortConfig.field];
+
+ // Type-specific comparisons
+ if (typeof aValue === "string") {
+ return direction * aValue.localeCompare(bValue as string);
+ }
+ return direction * (Number(aValue) - Number(bValue));
+ });
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 1e35955..a1ca4fe 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,154 +1,53 @@