I had this same question. The following is one of the simpler solutions I tried today while looking to colourise an image while cleanly preserving black and white values. It has complications and limitations to be sure (see discussion below), but it should be pretty usable.
If you're ok with the additional computations of HSL conversion, skip my hacky RGB approach and just check out this article on Photoshop blending modes; the GIMP equivalent you're looking for is Photoshop's "Color" mode.
If you want something a little more minimal (no colour mode conversions), hopefully this helps...
- Convert input image values to greyscale using perceptual brightness
- Find the average value of the desired colour and use this to centre the RGB values
- Modulate (in the artistic sense) the original input values using mathematical power
This method behaves reasonably well with the full range of colour input values, including black or white (mix/lerp methods do not). It also retains the brightness of the original image pretty successfully (multiplication methods do not). It is not particularly successful with high-saturation-low-value colours (like a saturated navy blue), since the RGB nature of the computations result in less saturated output. I've arbitrarily boosted saturation in the code example on ShaderToy, which of course has its own downsides (burning and hue shifts can result at higher levels). But this is all pretty simple, and hopefully fairly fast due to not relying on HSL/HSV conversions.
If you know your RGB input values will always be within a known brightness range, a mix/lerp solution might be more accurate and reliable. I needed something that was a little more flexible (if not strictly accurate for hue/saturation), so came up with this.
float3 image = // input image values
float imageGrey = image.r * 0.3 + image.g * 0.7 + image.b * 0.1; // roughly perceptual greyscale conversion
float3 color = // input colour values
float colorAverage = (color.r+color.g+color.b)*0.3334; // mid-point of the float3
float3 colorPow = 1.0+colorAverage-color; // centre at 1.0 and invert the colour values
return pow(imageGrey, colorPow);
References: (for the perceptual greyscale conversion)