Skip to content

Commit

Permalink
Merge pull request #81 from Abdenasser/transaparent-title-bar
Browse files Browse the repository at this point in the history
custom title bar + removing resizable columns functionality for now
Abdenasser authored Nov 9, 2024

Verified

This commit was signed with the committer’s verified signature.
bonjourmauko Mauko Quiroga-Alvarado
2 parents 478d746 + b56dae8 commit bafb8f5
Showing 6 changed files with 428 additions and 289 deletions.
6 changes: 5 additions & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
@@ -5,7 +5,11 @@
"windows": ["main"],
"permissions": [
"core:default",
"core:app:allow-version"
"core:app:allow-version",
"core:window:allow-start-dragging",
"core:window:allow-maximize",
"core:window:allow-minimize",
"core:window:allow-close"
]
}

4 changes: 3 additions & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -40,7 +40,9 @@
"title": "NeoHtop",
"width": 1280,
"minWidth": 1120,
"minHeight": 700
"minHeight": 700,
"titleBarStyle": "Overlay",
"hiddenTitle": true
}
],
"security": {
232 changes: 17 additions & 215 deletions src/lib/components/ProcessTable.svelte
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
faThumbtack,
faInfoCircle,
faXmark,
faGripLinesVertical,
} from "@fortawesome/free-solid-svg-icons";
import Fa from "svelte-fa";
import type { Process, Column } from "$lib/types";
@@ -20,88 +19,6 @@
export let onShowDetails: (process: Process) => void;
export let onKillProcess: (process: Process) => void;
interface ResizeState {
leftColumnId: string | null;
rightColumnId: string | null;
startX: number;
leftStartWidth: number;
rightStartWidth: number;
}
let resizing: ResizeState = {
leftColumnId: null,
rightColumnId: null,
startX: 0,
leftStartWidth: 0,
rightStartWidth: 0,
};
// Store column widths
let columnWidths: Record<string, number> = {};
// Initialize default widths
$: {
columns.forEach((col) => {
if (!columnWidths[col.id]) {
columnWidths[col.id] = 100; // Default width
}
});
}
function handleResizeStart(
event: MouseEvent,
leftColId: string,
rightColId: string,
) {
event.stopPropagation();
resizing = {
leftColumnId: leftColId,
rightColumnId: rightColId,
startX: event.pageX,
leftStartWidth: columnWidths[leftColId],
rightStartWidth: columnWidths[rightColId],
};
document.addEventListener("mousemove", handleResizeMove);
document.addEventListener("mouseup", handleResizeEnd);
}
function handleResizeMove(event: MouseEvent) {
if (!resizing.leftColumnId || !resizing.rightColumnId) return;
const delta = event.pageX - resizing.startX;
// Ensure minimum width (50px) for both columns
const newLeftWidth = Math.max(50, resizing.leftStartWidth + delta);
const totalWidth = resizing.leftStartWidth + resizing.rightStartWidth;
const newRightWidth = Math.max(50, totalWidth - newLeftWidth);
// Only update if both columns maintain minimum width
if (newLeftWidth >= 50 && newRightWidth >= 50) {
columnWidths = {
...columnWidths,
[resizing.leftColumnId]: newLeftWidth,
[resizing.rightColumnId]: newRightWidth,
};
}
}
function handleResizeEnd() {
resizing = {
leftColumnId: null,
rightColumnId: null,
startX: 0,
leftStartWidth: 0,
rightStartWidth: 0,
};
document.removeEventListener("mousemove", handleResizeMove);
document.removeEventListener("mouseup", handleResizeEnd);
// Optional: Save to localStorage
localStorage.setItem("columnWidths", JSON.stringify(columnWidths));
}
function getSortIndicator(field: keyof Process) {
if (sortConfig.field !== field) return "";
return sortConfig.direction === "asc" ? "" : "";
@@ -173,38 +90,20 @@
<table>
<thead>
<tr>
{#each columns.filter((col) => col.visible) as column, i}
<th
data-column={column.id}
style="width: {columnWidths[column.id]}px"
>
<div class="th-content" on:click={() => onToggleSort(column.id)}>
<div class="th-label">
{column.label}
<span
class="sort-indicator"
class:active={sortConfig.field === column.id}
>
{getSortIndicator(column.id)}
</span>
</div>
{#if i < columns.filter((col) => col.visible).length - 1}
<div
class="resize-handle"
on:mousedown|stopPropagation={(e) =>
handleResizeStart(
e,
column.id,
columns.filter((col) => col.visible)[i + 1].id,
)}
>
<Fa icon={faGripLinesVertical} size="xs" />
</div>
{/if}
{#each columns.filter((col) => col.visible) as column}
<th class="sortable" on:click={() => onToggleSort(column.id)}>
<div class="th-content">
{column.label}
<span
class="sort-indicator"
class:active={sortConfig.field === column.id}
>
{getSortIndicator(column.id)}
</span>
</div>
</th>
{/each}
<th class="col-actions">Actions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@@ -353,63 +252,27 @@
}
.th-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
cursor: pointer;
user-select: none;
}
.th-label {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.resize-handle {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: col-resize;
.sort-indicator {
color: var(--overlay0);
opacity: 0;
font-size: 12px;
opacity: 0.5;
transition: all 0.2s ease;
margin-left: 8px;
}
/* Show handle on header hover */
th:hover .resize-handle {
opacity: 1;
}
.resize-handle:hover {
color: var(--blue);
}
/* Active state during resize */
.resize-handle:active {
.sort-indicator.active {
color: var(--blue);
opacity: 1;
}
.sort-indicator {
display: inline-flex;
opacity: 0;
transition: opacity 0.2s ease;
}
.sort-indicator.active {
.sortable:hover .sort-indicator {
opacity: 1;
}
th:hover .sort-indicator {
opacity: 0.5;
}
.high-usage {
background-color: color-mix(in srgb, var(--red) 10%, transparent);
}
@@ -443,6 +306,7 @@
position: sticky;
right: 0;
z-index: 2;
background: var(--base);
border-left: 1px solid var(--surface0);
width: 120px;
}
@@ -563,66 +427,4 @@
align-items: center;
gap: 8px;
}
th {
position: relative;
min-width: 50px;
box-sizing: border-box;
}
.resize-handle {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: col-resize;
color: var(--overlay0);
opacity: 0;
transition: all 0.2s ease;
}
/* Show handle on header hover */
th:hover .resize-handle {
opacity: 1;
}
.resize-handle:hover {
color: var(--blue);
}
/* Active state during resize */
.resize-handle:active {
color: var(--blue);
opacity: 1;
}
/* Optional: Add a subtle background on hover */
.resize-handle:hover::before {
content: "";
position: absolute;
inset: -4px;
background: var(--surface0);
border-radius: 4px;
z-index: -1;
}
/* Make sure the table doesn't shrink columns */
table {
table-layout: fixed;
}
td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
th {
transition: width 0.05s ease;
}
</style>
112 changes: 112 additions & 0 deletions src/lib/components/TitleBar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script lang="ts">
import { Window } from "@tauri-apps/api/window";
const appWindow = new Window("main");
let isMaximized = false;
async function toggleMaximize() {
isMaximized = await appWindow.isMaximized();
if (isMaximized) {
await appWindow.unmaximize();
} else {
await appWindow.maximize();
}
isMaximized = await appWindow.isMaximized();
}
</script>

<div class="title-bar" data-tauri-drag-region>
<div class="title">
<div class="neon">NeoHtop</div>
<div class="scanlines"></div>
</div>
</div>

<style>
.title-bar {
height: 32px;
/* background: var(--mantle); */
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
-webkit-user-select: none;
user-select: none;
position: relative;
overflow: hidden;
}
.title {
display: flex;
align-items: center;
position: relative;
height: 100%;
}
.neon {
font-family: "Courier New", monospace;
font-size: 16px;
font-weight: bold;
color: var(--text);
text-shadow:
0 0 5px var(--text),
0 0 10px var(--text),
0 0 20px var(--blue),
0 0 40px var(--blue),
0 0 80px var(--blue);
animation: flicker 3s infinite alternate;
}
@keyframes flicker {
0%,
19.999%,
22%,
62.999%,
64%,
64.999%,
70%,
100% {
opacity: 1;
text-shadow:
0 0 5px var(--text),
0 0 10px var(--text),
0 0 20px var(--blue),
0 0 40px var(--blue),
0 0 80px var(--blue);
}
20%,
21.999%,
63%,
63.999%,
65%,
69.999% {
opacity: 0.4;
text-shadow: none;
}
}
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
transparent 50%,
rgba(0, 0, 0, 0.2) 51%
);
background-size: 100% 4px;
animation: scanlines 0.6s steps(40) infinite;
pointer-events: none;
}
@keyframes scanlines {
from {
transform: translateY(0);
}
to {
transform: translateY(2px);
}
}
</style>
49 changes: 46 additions & 3 deletions src/lib/components/ToolBar.svelte
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@
faPause,
faChevronDown,
faChevronRight,
faArrowRotateRight,
} from "@fortawesome/free-solid-svg-icons";
export let searchTerm: string;
export let statusFilter: string = "all";
@@ -54,11 +53,16 @@
<div class="search-box">
<div class="search-input-wrapper">
<input
type="search"
type="text"
placeholder="Search processes"
bind:value={searchTerm}
class="search-input"
/>
{#if searchTerm}
<button class="btn-clear" on:click={() => (searchTerm = "")}>
Clear
</button>
{/if}
</div>
</div>
<div class="toolbar-group">
@@ -214,7 +218,9 @@
.search-input {
width: 240px;
padding: 6px 12px;
height: 28px;
padding: 0 12px;
padding-right: 70px;
border: 1px solid var(--surface1);
border-radius: 6px;
font-size: 12px;
@@ -223,6 +229,43 @@
transition: all 0.2s ease;
}
.btn-clear {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
padding: 4px 8px;
font-size: 11px;
color: var(--subtext0);
background: var(--surface1);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-clear:hover {
background: var(--surface2);
color: var(--text);
}
.search-input::-webkit-search-cancel-button {
-webkit-appearance: none;
height: 14px;
width: 14px;
margin-right: 4px;
background: var(--overlay0);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E")
no-repeat 50% 50%;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E")
no-repeat 50% 50%;
cursor: pointer;
}
.search-input::-webkit-search-cancel-button:hover {
background: var(--text);
}
.search-input:hover {
background-color: var(--surface1);
}
314 changes: 245 additions & 69 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
import { formatMemorySize, formatStatus } from "$lib/utils";
import { themeStore } from "$lib/stores";
import type { Process, SystemStats, Column } from "$lib/types";
import TitleBar from "$lib/components/TitleBar.svelte";
let processes: Process[] = [];
let systemStats: SystemStats | null = null;
@@ -47,20 +48,28 @@
},
{
id: "memory_usage",
label: "Memory",
label: "RAM",
visible: true,
format: (v) => (v / (1024 * 1024)).toFixed(1) + " MB",
},
{ id: "command", label: "Command", visible: false },
{ id: "ppid", label: "Parent PID", visible: false },
{ id: "environ", label: "Environment Variables", visible: false },
{ id: "root", label: "Root", visible: false },
{
id: "virtual_memory",
label: "Virtual Memory",
visible: false,
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",
@@ -70,7 +79,7 @@
{
id: "run_time",
label: "Run Time",
visible: false,
visible: true,
format: (v) => {
const seconds = v; // v is the time the process has been running in seconds
const hours = Math.floor(seconds / 3600);
@@ -79,14 +88,6 @@
return `${hours}h ${minutes}m ${remainingSeconds}s`; // Format as HH:MM:SS
},
},
{
id: "disk_usage",
label: "Disk Usage read/write",
visible: false,
format: (v) =>
`${(v[0] / (1024 * 1024)).toFixed(1)} / ${(v[1] / (1024 * 1024)).toFixed(1)} MB`,
},
{ id: "session_id", label: "Session ID", visible: false },
];
let sortConfig = {
@@ -245,9 +246,18 @@
selectedProcessPid = null;
}
let minLoadingTimer: ReturnType<typeof setTimeout>;
const MIN_LOADING_TIME = 2000; // Show loading screen for at least 2 seconds
onMount(async () => {
const loadingPromise = Promise.all([getProcesses()]);
const timerPromise = new Promise((resolve) => {
minLoadingTimer = setTimeout(resolve, MIN_LOADING_TIME);
});
try {
await Promise.all([getProcesses()]);
// Wait for both the data to load AND the minimum time to pass
await Promise.all([loadingPromise, timerPromise]);
} finally {
isLoading = false;
}
@@ -257,50 +267,58 @@
onDestroy(() => {
if (intervalId) clearInterval(intervalId);
if (minLoadingTimer) clearTimeout(minLoadingTimer);
});
</script>

{#if isLoading}
<div class="loading-container">
<div class="loading-content">
<div class="spinner"></div>
<span class="loading-text">Loading processes...</span>
<div class="title-wrapper">
<div class="neon-title">NeoHtop</div>
<div class="neon-flare"></div>
</div>
<div class="cyber-spinner"></div>
<span class="loading-text">System Initialization...</span>
</div>
</div>
{:else}
<main>
{#if systemStats}
<StatsBar {systemStats} />
{/if}

<ToolBar
bind:searchTerm
bind:statusFilter
bind:itemsPerPage
bind:currentPage
bind:refreshRate
bind:isFrozen
{totalPages}
totalResults={filteredProcesses.length}
bind:columns
/>

{#if error}
<div class="alert">{error}</div>
{/if}

<ProcessTable
processes={paginatedProcesses}
{columns}
{systemStats}
{sortConfig}
{pinnedProcesses}
onToggleSort={toggleSort}
onTogglePin={togglePin}
onShowDetails={showProcessDetails}
onKillProcess={confirmKillProcess}
/>
</main>
<div class="app-container">
<TitleBar />
<main>
{#if systemStats}
<StatsBar {systemStats} />
{/if}

<ToolBar
bind:searchTerm
bind:statusFilter
bind:itemsPerPage
bind:currentPage
bind:refreshRate
bind:isFrozen
{totalPages}
totalResults={filteredProcesses.length}
bind:columns
/>

{#if error}
<div class="alert">{error}</div>
{/if}

<ProcessTable
processes={paginatedProcesses}
{columns}
{systemStats}
{sortConfig}
{pinnedProcesses}
onToggleSort={toggleSort}
onTogglePin={togglePin}
onShowDetails={showProcessDetails}
onKillProcess={confirmKillProcess}
/>
</main>
</div>
{/if}

<ProcessDetailsModal
@@ -320,14 +338,6 @@
onConfirm={handleConfirmKill}
/>

<svelte:head>
<title>NeoHtop - Modern System Monitor</title>
<meta
name="description"
content="A modern, web-based system monitoring interface inspired by htop"
/>
</svelte:head>

<style>
:global(:root) {
--base: #1e1e2e;
@@ -365,10 +375,11 @@
}
main {
height: 100vh;
flex: 1;
display: flex;
flex-direction: column;
min-width: min-content;
overflow: hidden;
}
.alert {
@@ -388,23 +399,70 @@
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--base) 0%, var(--mantle) 100%);
position: relative;
overflow: hidden;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
z-index: 2;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--surface0);
border-top-color: var(--blue);
border-radius: 50%;
animation: spin 1s ease-in-out infinite;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
@keyframes glitch {
0%,
100% {
transform: translate(0);
}
20% {
transform: translate(-2px, 2px);
}
40% {
transform: translate(-2px, -2px);
}
60% {
transform: translate(2px, 2px);
}
80% {
transform: translate(2px, -2px);
}
}
@keyframes glitch-2 {
0%,
100% {
transform: translate(0);
}
25% {
transform: translate(1px, 1px);
}
75% {
transform: translate(-1px, -1px);
}
}
@keyframes glitch-3 {
0%,
100% {
transform: translate(0);
}
25% {
transform: translate(-1px, -1px);
}
75% {
transform: translate(1px, 1px);
}
}
@keyframes scanlines {
from {
transform: translateY(0);
}
to {
transform: translateY(4px);
}
}
.loading-text {
@@ -431,4 +489,122 @@
opacity: 1;
}
}
.app-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.title-wrapper {
position: relative;
margin-bottom: 2rem;
}
.neon-title {
font-family: "Courier New", monospace;
font-size: 64px;
font-weight: bold;
color: var(--text);
text-shadow:
0 0 5px var(--text),
0 0 10px var(--text),
0 0 20px var(--blue),
0 0 40px var(--blue),
0 0 80px var(--blue);
animation: pulse 4s ease-in-out infinite;
}
.neon-flare {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
45deg,
transparent 45%,
var(--blue) 48%,
var(--text) 50%,
var(--blue) 52%,
transparent 55%
);
background-size: 200% 200%;
animation: flare 4s ease-in-out infinite;
opacity: 0.1;
filter: blur(3px);
}
.cyber-spinner {
width: 50px;
height: 50px;
margin: 20px;
background: transparent;
border: 3px solid var(--surface0);
border-top: 3px solid var(--blue);
border-right: 3px solid var(--blue);
border-radius: 50%;
position: relative;
animation: spin 1s linear infinite;
}
.cyber-spinner::before {
content: "";
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: var(--blue);
animation: spin 2s linear infinite;
}
.loading-text {
color: var(--text);
font-size: 16px;
font-family: "Courier New", monospace;
letter-spacing: 2px;
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes pulse {
0%,
100% {
text-shadow:
0 0 5px var(--text),
0 0 10px var(--text),
0 0 20px var(--blue),
0 0 40px var(--blue),
0 0 80px var(--blue);
}
50% {
text-shadow:
0 0 10px var(--text),
0 0 20px var(--text),
0 0 40px var(--blue),
0 0 80px var(--blue),
0 0 120px var(--blue);
}
}
@keyframes flare {
0%,
100% {
background-position: 200% 200%;
}
50% {
background-position: 0% 0%;
}
}
@keyframes glow {
from {
text-shadow:
0 0 2px var(--text),
0 0 4px var(--text),
0 0 6px var(--blue);
}
}
</style>

0 comments on commit bafb8f5

Please sign in to comment.