@@ -101,7 +101,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101101 ctx , cancel := context .WithTimeout (context .Background (), m .collectionTimeout ())
102102 m .collectCancel = cancel
103103 m .collecting = true
104- cmds = append (cmds , collectSnapshot (ctx , m .sortBy , m .snap , m .processSampleEvery (), metrics.CollectOptions {
104+ cmds = append (cmds , collectSnapshot (ctx , m .sortBy , m .snap , m .processSampleEvery (), m . cfg . ProcLimit , metrics.CollectOptions {
105105 SkipGPU : m .cfg .NoGPU ,
106106 SkipTemp : m .cfg .NoTemp ,
107107 }))
@@ -217,22 +217,7 @@ func (m Model) View() string {
217217 }
218218
219219 // Render panels at appropriate widths
220- cpuPanel := ui .RenderCPU (m .snap .CPU , colL , m .cpuHistory )
221- gpuPanel := ui .RenderGPU (m .snap .GPU , colR , m .gpuHistory )
222- tempPanel := ui .RenderTemperature (m .snap .Temperature , colR )
223- netPanel := ui .RenderNetwork (m .netDelta , colL )
224- diskPanel := ui .RenderDisk (m .diskDelta , m .snap .Disk , colR )
225-
226- // Memory panel width depends on whether GPU is present (left vs right column)
227- var memPanel string
228- if twoCol && gpuPanel != "" {
229- memPanel = ui .RenderMemory (m .snap .Memory , m .snap .Load , colL , m .memHistory )
230- } else if twoCol {
231- // No GPU: memory goes to the right of CPU
232- memPanel = ui .RenderMemory (m .snap .Memory , m .snap .Load , colR , m .memHistory )
233- } else {
234- memPanel = ui .RenderMemory (m .snap .Memory , m .snap .Load , colL , m .memHistory )
235- }
220+ metricsSection := m .buildMetricsSection (colL , colR , twoCol )
236221
237222 // Filter processes and resolve PID-based selection.
238223 procs := m .filteredProcesses ()
@@ -248,57 +233,6 @@ func (m Model) View() string {
248233 TotalProcs : len (m .snap .Processes ),
249234 }
250235
251- // Build the metric panels section
252- var metricRows []string
253-
254- if twoCol {
255- // Two-column layout: pair panels side by side with matched heights
256- // Row 1: CPU | GPU (or Memory if no GPU)
257- if gpuPanel != "" {
258- metricRows = append (metricRows , joinPanelRow (cpuPanel , gpuPanel , colL , colR ))
259- } else {
260- metricRows = append (metricRows , joinPanelRow (cpuPanel , memPanel , colL , colR ))
261- }
262-
263- // Row 2: Memory | Temperature (only if GPU existed in row 1)
264- if gpuPanel != "" {
265- if tempPanel != "" {
266- metricRows = append (metricRows , joinPanelRow (memPanel , tempPanel , colL , colR ))
267- } else {
268- metricRows = append (metricRows , memPanel )
269- }
270- } else if tempPanel != "" {
271- metricRows = append (metricRows , tempPanel )
272- }
273-
274- // Row 3: Network | Disk
275- if netPanel != "" && diskPanel != "" {
276- metricRows = append (metricRows , joinPanelRow (netPanel , diskPanel , colL , colR ))
277- } else if netPanel != "" {
278- metricRows = append (metricRows , netPanel )
279- } else if diskPanel != "" {
280- metricRows = append (metricRows , diskPanel )
281- }
282- } else {
283- // Single-column stacked layout
284- metricRows = append (metricRows , cpuPanel )
285- if gpuPanel != "" {
286- metricRows = append (metricRows , gpuPanel )
287- }
288- metricRows = append (metricRows , memPanel )
289- if tempPanel != "" {
290- metricRows = append (metricRows , tempPanel )
291- }
292- if netPanel != "" {
293- metricRows = append (metricRows , netPanel )
294- }
295- if diskPanel != "" {
296- metricRows = append (metricRows , diskPanel )
297- }
298- }
299-
300- metricsSection := lipgloss .JoinVertical (lipgloss .Left , metricRows ... )
301-
302236 // Count lines used by fixed panels to size the process panel.
303237 usedLines := strings .Count (header , "\n " ) + 1
304238 usedLines += strings .Count (metricsSection , "\n " ) + 1
@@ -507,9 +441,6 @@ func (m Model) computeUsedLines() int {
507441 w = 80
508442 }
509443
510- // Header is always 1 line
511- usedLines := 1
512-
513444 twoCol := w >= 110
514445 var colL , colR int
515446 if twoCol {
@@ -520,6 +451,20 @@ func (m Model) computeUsedLines() int {
520451 colR = w
521452 }
522453
454+ metricsSection := m .buildMetricsSection (colL , colR , twoCol )
455+
456+ // Header is always 1 line
457+ usedLines := 1
458+ usedLines += strings .Count (metricsSection , "\n " ) + 1
459+ usedLines += 1 // help bar
460+
461+ return usedLines
462+ }
463+
464+ // buildMetricsSection renders all metric panels and joins them into a single string.
465+ // This is the single source of truth for panel layout, used by both View() and
466+ // computeUsedLines() to avoid duplication.
467+ func (m Model ) buildMetricsSection (colL , colR int , twoCol bool ) string {
523468 cpuPanel := ui .RenderCPU (m .snap .CPU , colL , m .cpuHistory )
524469 gpuPanel := ui .RenderGPU (m .snap .GPU , colR , m .gpuHistory )
525470 tempPanel := ui .RenderTemperature (m .snap .Temperature , colR )
@@ -536,6 +481,7 @@ func (m Model) computeUsedLines() int {
536481 }
537482
538483 var metricRows []string
484+
539485 if twoCol {
540486 if gpuPanel != "" {
541487 metricRows = append (metricRows , joinPanelRow (cpuPanel , gpuPanel , colL , colR ))
@@ -575,11 +521,7 @@ func (m Model) computeUsedLines() int {
575521 }
576522 }
577523
578- metricsSection := lipgloss .JoinVertical (lipgloss .Left , metricRows ... )
579- usedLines += strings .Count (metricsSection , "\n " ) + 1
580- usedLines += 1 // help bar
581-
582- return usedLines
524+ return lipgloss .JoinVertical (lipgloss .Left , metricRows ... )
583525}
584526
585527// computeProcDataY returns the Y line where process data rows begin on screen.
@@ -591,6 +533,13 @@ func (m Model) computeProcDataY() int {
591533
592534func (m Model ) handleSearchKey (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
593535 switch msg .Type {
536+ case tea .KeyCtrlC :
537+ m .quitting = true
538+ if m .collectCancel != nil {
539+ m .collectCancel ()
540+ m .collectCancel = nil
541+ }
542+ return m , tea .Quit
594543 case tea .KeyEscape :
595544 m .searching = false
596545 m .searchQuery = ""
@@ -724,9 +673,9 @@ func tick(d time.Duration) tea.Cmd {
724673 })
725674}
726675
727- func collectSnapshot (ctx context.Context , sortBy metrics.SortField , previous metrics.Snapshot , processSampleEvery time.Duration , opts metrics.CollectOptions ) tea.Cmd {
676+ func collectSnapshot (ctx context.Context , sortBy metrics.SortField , previous metrics.Snapshot , processSampleEvery time.Duration , procLimit int , opts metrics.CollectOptions ) tea.Cmd {
728677 return func () tea.Msg {
729- snap := metrics .Collect (ctx , 200 * time .Millisecond , sortBy , 50 , processSampleEvery , previous , opts )
678+ snap := metrics .Collect (ctx , 200 * time .Millisecond , sortBy , procLimit , processSampleEvery , previous , opts )
730679 return snapshotMsg (snap )
731680 }
732681}
@@ -766,12 +715,18 @@ func (m Model) killSelectedProcess(sig killSignal) string {
766715}
767716
768717func (m Model ) exportSnapshot () string {
769- filename := fmt .Sprintf ("hideTop_%s.json" , m .snap .CollectedAt .Format ("20060102_150405" ))
718+ basename := fmt .Sprintf ("hideTop_%s.json" , m .snap .CollectedAt .Format ("20060102_150405" ))
719+ // Write to user's home directory for a predictable location.
720+ home , err := os .UserHomeDir ()
721+ if err != nil {
722+ home = "."
723+ }
724+ filename := home + string (os .PathSeparator ) + basename
770725 data , err := json .MarshalIndent (m .snap , "" , " " )
771726 if err != nil {
772727 return fmt .Sprintf ("export error: %v" , err )
773728 }
774- if err := os .WriteFile (filename , data , 0o644 ); err != nil {
729+ if err := os .WriteFile (filename , data , 0o600 ); err != nil {
775730 return fmt .Sprintf ("export error: %v" , err )
776731 }
777732 return fmt .Sprintf ("exported to %s" , filename )
0 commit comments