Skip to content

Add user-configurable distribution path prefixes (implements #822)#1961

Open
JonasPfi wants to merge 2 commits into
htop-dev:mainfrom
JonasPfi:main
Open

Add user-configurable distribution path prefixes (implements #822)#1961
JonasPfi wants to merge 2 commits into
htop-dev:mainfrom
JonasPfi:main

Conversation

@JonasPfi

Copy link
Copy Markdown

Fixes #822, depends on #637

Problem

The 'Shadow distribution path prefixes' feature (#637) uses a hardcoded list of paths (/usr/bin/, /bin/, /nix/store/, /run/current-system/, etc.) that cannot be changed by the user. This makes the feature unusable for non-standard setups such as Flatpak (/var/lib/flatpak/), custom install prefixes, or other distributions with different filesystem layouts.

Solution

This PR implements two things as requested in #822:

1. A new StringItem settings control

Extends the existing settings UI infrastructure (OptionItem) with a new text input type backed by the existing LineEditor. This gives users a readline-like editing experience with support for:

  • Cursor movement (/, Ctrl+A, Ctrl+E)
  • Backspace / Delete

The StringItem follows the same patterns as the existing CheckItem and NumberItem controls.

2. User-configurable path prefix list

Replaces the hardcoded CHECK_AND_MARK_DIST_PATH_PREFIXES switch macro in Process.c with a matchesDistPrefix() function that checks against a user-supplied colon-separated list of paths. When the list is empty, the original built-in defaults are used as fallback.

Changes

  • OptionItem.h/.c — new StringItem type with LineEditor integration
  • Settings.h — new char* distPathPrefixes field
  • Settings.c — read/write dist_path_prefixes from/to htoprc
  • DisplayOptionsPanel.c — new text input field below the existing checkbox
  • Process.c — replace hardcoded switch macro with matchesDistPrefix()

Testing

  • Custom paths are highlighted correctly after input
  • Settings persist across restarts
  • Empty input correctly falls back to built-in defaults
  • valgrind --leak-check=full reports 0 definitely lost bytes
  • Escape correctly cancels editing and restores previous value
  • Tested on WSL2 (Ubuntu) with Linux build

@fasterit

Copy link
Copy Markdown
Member

Undeclared use of AI, please read and understand the AI policy. Review your code and only then submit PRs.

@JonasPfi

Copy link
Copy Markdown
Author

I added what I used to the commit. Since its my first contribution I used Claude mostly to get used to the repo and also for some logic.

@fasterit

Copy link
Copy Markdown
Member

Did you see the "review your code" part?

@JonasPfi

Copy link
Copy Markdown
Author

I've reviewed all changes. I removed a comment I missed earlier but did not find anything else.

@Explorer09

Copy link
Copy Markdown
Contributor

My question is: Why do we need this? Even though I believe there are demands for a custom list of path prefixes, I don't think the need for a UI for it is guaranteed.
Also, the format for storing those and loading those in the config can be discussed further. (I personally prefer storing the list as a separate file, e.g. ~/.config/htop/pathprefixes.txt, not in htoprc)

Comment thread Process.c Outdated
Comment thread Process.c Outdated
Comment thread OptionItem.h
Comment on lines +67 to +74
typedef struct StringItem_ {
OptionItem super;
char** ref;
bool editing;
bool valid;
LineEditor editor;
bool (*validate)(const char* text);
} StringItem;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skeptical of this item. It looks like a duplicate functionality when we have "Screens" whose names are editable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify. Do you mean that StringItem is unnecessary as a new abstraction, and we should instead embed LineEditor directly in DisplayOptionsPanel like ScreensPanel does?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. I mean there's a significant overlap between the two string editing functionality. It's better to consolidate the two functionality into one code. How to do that is left for you to figure out.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh. Forgot to mention this: LINEEDITOR_MAX is currently 128 (bytes). This is like not enough for editing a list of strings.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I'll hold off on refactoring until there's consensus on whether the feature is wanted.

Comment thread OptionItem.h
bool NumberItem_addChar(NumberItem* this, char c);
void NumberItem_deleteChar(NumberItem* this);

StringItem* StringItem_newByRef(const char* text, char** ref, bool (*validate)(const char* text));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split the bool (*validate)(const char* text) function definition to its own typedef, please?

Comment thread Process.c Outdated
Comment thread Process.c Outdated
Comment thread Process.c Outdated
Comment thread OptionItem.h
bool editing;
bool valid;
LineEditor editor;
bool (*validate)(const char* text);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto. (Split the bool (*validate)(const char* text) function definition to its own typedef.)

Comment thread OptionItem.c Outdated
@fasterit

Copy link
Copy Markdown
Member

My question is: Why do we need this? Even though I believe there are demands for a custom list of path prefixes, I don't think the need for a UI for it is guaranteed.

We do not encourage editing htoprc by hand.

Also, the format for storing those and loading those in the config can be discussed further. (I personally prefer storing the list as a separate file, e.g. ~/.config/htop/pathprefixes.txt, not in htoprc)

Inside htoprc is fine, we should not inflate the config files for such minor functionality.

@fasterit

Copy link
Copy Markdown
Member

Thank you for your review @Explorer09, @JonasPfi you see you have lots to do.

@fasterit fasterit added the needs-discussion 🤔 Changes need to be discussed and require consent label Apr 17, 2026
@fasterit

Copy link
Copy Markdown
Member

@BenBE, @natoscott, @cgzones:
Do we want this path list to be editable, or do we want to have one canonical list (and add whatever flatpak needs there)?

@Explorer09

Explorer09 commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

My question is: Why do we need this? Even though I believe there are demands for a custom list of path prefixes, I don't think the need for a UI for it is guaranteed.

We do not encourage editing htoprc by hand.

Also, the format for storing those and loading those in the config can be discussed further. (I personally prefer storing the list as a separate file, e.g. ~/.config/htop/pathprefixes.txt, not in htoprc)

Inside htoprc is fine, we should not inflate the config files for such minor functionality.

My concern is that, if someone needs to ever edit this list, they would likely comment what each path does. The UX requirement for that would be insufficient to fulfill with just a line editor.

Look at how long a PATH variable it is in my system, for example:
macos-26-path-env-var-example

@fasterit

Copy link
Copy Markdown
Member

Yes, this might need more than the line editor and a full editor panel would probably not be worth it.
At least I would like to keep the scope creep a bit under control.
Let's see what the others say.

@Explorer09

Copy link
Copy Markdown
Contributor

Another advantage for using an external list file is that distributions can ship with a preconfigured list of paths, freeing the user for needing to configure the list themselves.

If distributions want to ship with a preconfigured list, this list will likely reside in /etc/htop, separate from the per-user configurations.

@fasterit

Copy link
Copy Markdown
Member

That would still be in a distro-default /etc/htoprc, we don't read /etc/htop/.

@JonasPfi

Copy link
Copy Markdown
Author

My question is: Why do we need this? Even though I believe there are demands for a custom list of path prefixes, I don't think the need for a UI for it is guaranteed.

We do not encourage editing htoprc by hand.

Also, the format for storing those and loading those in the config can be discussed further. (I personally prefer storing the list as a separate file, e.g. ~/.config/htop/pathprefixes.txt, not in htoprc)

Inside htoprc is fine, we should not inflate the config files for such minor functionality.

My concern is that, if someone needs to ever edit this list, they would likely comment what each path does. The UX requirement for that would be insufficient to fulfill with just a line editor.

Look at how long a PATH variable it is in my system, for example: macos-26-path-env-var-example

I do agree with this. Currently you won't be able to recreate the default paths with the custom ones, and I think it looks off from a UX perspective to have such a long string in the display options.

@Explorer09

Copy link
Copy Markdown
Contributor

That would still be in a distro-default /etc/htoprc, we don't read /etc/htop/.

Um... Putting a settings file inside /etc without a subdirectory is a bad idea... Just FYI.

(If we ever need a system-wide htoprc, it would be /etc/htop/htoprc)

@fasterit

Copy link
Copy Markdown
Member

Use the source, Luke. It is what we do. And have been doing for decades.
Ofc, these days people use subdirs in /etc but that was not always the case.

Comment thread Settings.h Outdated
Comment thread DisplayOptionsPanel.c
}

NumberItem* numItem = (OptionItem_kind(selected) == OPTION_ITEM_NUMBER) ? (NumberItem*)selected : NULL;
StringItem* strItem = (OptionItem_kind(selected) == OPTION_ITEM_STRING) ? (StringItem*)selected : NULL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RTTI (run-time type information). This is a smell of bad object/class modeling. Unfortunately I don't have any idea to fix it for now.

Comment thread Process.c Outdated
Comment thread Process.c
return len;
token = *end ? end + 1 : end;
}
return 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still a performance concern with this new function. The old code had a switch-case that could make the lookup slightly faster, but now we're forced to perform a linear search...

I wish a slightly faster data structure such as a trie.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this specific case implementing a trie might be overkill given the small number of prefixes. A full implementation would add significant complexity for what I'd expect to be a minimal performance gain.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont know. If it's me I'd implement it. Otherwise I don't like to trade a performance loss for a feature that few people use.

Comment thread OptionItem.c
} else {
const char* val = (this->ref && *this->ref) ? *this->ref : NULL;
if (val) {
RichString_appendAscii(out, valAttr, val);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use RichString_appendAscii for strings read from external sources. Use RichString_appendWide instead.

Assisted-by: Claude (Anthropic)
Comment thread OptionItem.c
const StringItem* this = (const StringItem*)cast;
int labelAttr = CRT_colors[CHECK_TEXT];
int boxAttr = CRT_colors[CHECK_BOX];
int valAttr = this->valid ? CRT_colors[CHECK_MARK] : CRT_colors[FAILED_READ];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the validate function inside a StringItem do? And what would happen (or should happen) if the validation fails?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently its a placeholder. There was the idea of validating if the input path is a valid path in #822 but I did not implement any validation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what should be done if the "validation" fails?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input should be displayed in red as visual feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "red" visual feedback would be not good enough if you have a list of path prefixes to edit. Also, I saw no code the "discard" directories that don't exist or are exact duplicates of previously specified paths.

Comment thread OptionItem.c
StringItem* StringItem_newByRef(const char* text, char** ref, bool (*validate)(const char* text)) {
StringItem* this = AllocThis(StringItem);
this->super.text = xStrdup(text);
this->ref = ref;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does ref do here and how is this argument different from text?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text is the label displayed next to the input field ( "- Custom path prefixes"). ref is a pointer to the settings field being edited (&settings->distPathPrefixes), so changes are written directly to the settings struct.

Comment thread OptionItem.c
this->editBuffer[this->editLen] = '\0';
}
}
StringItem* StringItem_newByRef(const char* text, char** ref, bool (*validate)(const char* text)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typedef bool (*ValidateStringFn)(const char* str, size_t len, void* context);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will consolidate into a typedef. Will use your suggested signature with len and context if the feature moves forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-discussion 🤔 Changes need to be discussed and require consent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement text input control for settings

3 participants