Change scaling/resolution of primary monitor from bash/terminal

I’d like to add a button to my Gnome panel that toggles my primary monitor’s display scaling between 100% and 200%. It seems there currently is no compatible extension that can do this. I did however find the executor extension which can add buttons with arbitrary commands to the panel.

Now my question is: How can I change the scaling of the monitor that (at the time of execution) is the primary monitor from Bash or using a terminal command in general?

Caveat: My notebook is usually connected with 2 external displays in which case the bigger one of those is primary. When I’m traveling, however, the primary monitor is the built-in display.

Edit: It would also be interesting to know why the Scale Switcher and Display Profile Manager extensions are not compatible with the latest version of Gnome. Maybe there’d be an easy way to fix one of those?

Is this the scaling factor in Gnome tweak tools shown in the image?

If yes, the corresponding gsettings setting is:

org.gnome.desktop.interface text-scaling-factor

So you can get/set the value using gsettings on the terminal:

gsettings set org.gnome.desktop.interface text-scaling-factor 1.2

and so on.

Most gnome settings will have a corresponding gsettings value that can be modified from the terminal. You can install dconf-editor to search (and see/set) these, but as dconf-editor will note, please tread lightly.

2 Likes

Thanks for the quick response. I wasn’t aware that there is font scaling as well. I was talking about display scaling only actually:

1 Like

Best to report these upstream at their GitHub projects and help the devs fix them. As with all extensions, the extension maintainers need to update their code for Gnome stack updates.

1 Like

They seem to be abandoned. So I’d have to figure it out on my own.

Hrm, I don’t have an external monitor at the moment, so not sure how/where these are set. Could be dconf, but couldn’t find anything there, unfortunately.

A primary display should exist in both cases. With or without external monitors. When I’m traveling I’d want to change the scaling of the built-in monitor of that notebook which would be the only one connected at that time (and thus would have to be the primary display).

1 Like

I thought this was org.gnome.desktop.interface scaling-factor

I think
0 auto
1 = 100
2= 200

2 Likes

gsettings set org.gnome.desktop.interface scaling-factor 2
gsettings set org.gnome.desktop.interface scaling-factor 1

etc. don’t do anything for me. I don’t even get any output on stdout/stderr. Checking the settings in the GUI, they are unaffected as well.

1 Like

It maybe set via dbus. I found this:

and this is the display config file:

Unfortunately, this isn’t very easy to decipher:

$ gdbus call --session \
  --dest=org.gnome.Mutter.DisplayConfig \
  --object-path /org/gnome/Mutter/DisplayConfig \
  --method org.gnome.Mutter.DisplayConfig.GetCurrentState

(uint32 1, [(('HDMI-2', 'HSD', 'HL229DPB', '1234567890123'), [('1920x1080@60', 1920, 1080, 60.0, 1.0, [1.0, 2.0], {'is-current': <true>, 'is-preferred': <true>}), ('1680x1050@59.883251190185547', 1680, 1050, 59.883251190185547, 1.0, [1.0, 2.0], {}), ('1600x1200@60', 1600, 1200, 60.0, 1.0, [1.0, 2.0], {}), ('1440x900@59.901458740234375', 1440, 900, 59.901458740234375, 1.0, [1.0], {}), ('1400x1050@59.947769165039062', 1400, 1050, 59.947769165039062, 1.0, [1.0], {}), ('1280x1024@75.024673461914062', 1280, 1024, 75.024673461914062, 1.0, [1.0], {}), ('1280x1024@60.019741058349609', 1280, 1024, 60.019741058349609, 1.0, [1.0], {}), ('1280x960@60', 1280, 960, 60.0, 1.0, [1.0], {}), ('1152x864@75', 1152, 864, 75.0, 1.0, [1.0], {}), ('1024x768@75.028579711914062', 1024, 768, 75.028579711914062, 1.0, [1.0], {}), ('1024x768@70.069358825683594', 1024, 768, 70.069358825683594, 1.0, [1.0], {}), ('1024x768@60.003841400146484', 1024, 768, 60.003841400146484, 1.0, [1.0], {}), ('832x624@74.55126953125', 832, 624, 74.55126953125, 1.0, [1.0], {}), ('800x600@75', 800, 600, 75.0, 1.0, [1.0], {}), ('800x600@72.187568664550781', 800, 600, 72.187568664550781, 1.0, [1.0], {}), ('800x600@60.316539764404297', 800, 600, 60.316539764404297, 1.0, [1.0], {}), ('800x600@56.25', 800, 600, 56.25, 1.0, [1.0], {})], {'is-builtin': <false>, 'display-name': <'HannStar Display Corp 22"'>})], [(0, 0, 1.0, uint32 0, true, [('HDMI-2', 'HSD', 'HL229DPB', '1234567890123')], @a{sv} {})], {'layout-mode': <uint32 2>, 'legacy-ui-scaling-factor': <1>})

You could try d-feet to tinker with things, and there should be some documentation somewhere:

Yeh, I tried that as well, and when it didn’t work, I started looking at dbus.

1 Like

This extension:

points to:

https://wiki.gnome.org/HowDoI/HiDpi

which has a gsettings command:

gsettings set org.gnome.settings-daemon.plugins.xsettings overrides "[{'Gdk/WindowScalingFactor', <2>}]"

but that didn’t work for me either. So maybe it’s no longer how things work :laughing:

1 Like

Maybe it’s related to the switch to wayland. I really don’t wanna go back though.
Deciphering that dbus stuff is gonna take me a while.

Can you tell me what that output format is called? I’m looking for a way to parse it in Bash.

Okay I think it’s called serialized GVariant values, but I can’t find a parser for that. So I guess I’ll have to use regex or something…

Trying to break the method signatures down to a reasonable level:

GetCurrentState () ↦ (
    UInt32 serial, 
    Array of monitors, 
    Array of logical_monitors, 
    Dict of  properties
)

ApplyMonitorsConfig (
    UInt32 serial, 
    UInt32 method, 
    Array of logical_monitors, 
    Dict of properties
) ↦ ()

I’d have to call GetCurrentState and save serial, logical_monitors and properties. Then modify the logical_monitors array (specifically the entry for the primary monitor) and then pass those to the ApplyMonitorsConfig method. Not sure what to pass for the second argument “methodApplyMonitorsConfig yet.

1 Like

Yeh, it doesn’t seem to be designed for command line use. Maybe the Gnome Settings source code will give some ideas:

1 Like

I’m trying to convert the output format into JSON so I can parse it properly. It basically works, but there is this one weird value “@a{sv} {}”. I feel like that doesn’t translate to JSON well. Maybe I’ll just make it a string for now…

Edit:

Anyway, this is what I got so far (it’s a very naive approach):

const exampleDbusOutput = ``

function sgvToJson(serializedGvariantValues) {
    let output = ""
    const openChars = "{["
    const closeChars = "}]"
    const sepChars = ",:"
    const input = serializedGvariantValues.replace(/\(/g, '[').replace(/\)/g, ']').replace(/, /g, ',').replace(/: /g, ':').replace(/'/g, '').replace(/"/g, '\\"')
    for (let [i,char] of Array.from(input).entries()) {
        let prevChar = input[i-1]
        let nextChar = input[i+1]
        if (sepChars.includes(char) && !closeChars.includes(prevChar)) {
            output+=`"`
        } else if (closeChars.includes(char) && !closeChars.includes(prevChar)) {
            output+=`"`
        }
        output+=char
        if (sepChars.includes(char) && !openChars.includes(nextChar)) {
            output+=`"`
        } else if (openChars.includes(char) && !openChars.includes(nextChar)) {
            output+=`"`
        }
    }
    output = output.replace(/\{\"\"\}/g, '{}').replace(/\[\"\"\]/g, '[]').replace(/"<true>"/g, 'true').replace(/"<false>"/g, 'false').replace(/("@a\{"sv"\} \{\})/g, '"@a{sv} {}"')
    return JSON.stringify(JSON.parse(output), null, '    ')
}
const parsedOutput = JSON.parse(sgvToJson(exampleDbusOutput))
const primaryMonitor = parsedOutput[2].filter(logical_monitor => logical_monitor[4] === "true")[0]
primaryMonitor[2] = "2.0" // set scaling to 200%

// next step: convert modified primaryMonitor object back into serialized GVariant values

It’s written in JavaScript. Maybe I can translate it to Bash later or use it in a Gnome extension directly.

Edit2:
The more I think about it, the more I feel like this is a bad idea. I mean every time a new property is added to one of the structs in the source code, this code ould potentially need to be updated.

1 Like

Hi, if you change the scale from Gnome Settings and there no change on the display, you can edit it manually in user home directory ~/.config/monitors.xml. Find your primary monitor there and change the tag <scale>1</scale> to 2. After that reboot.

1 Like

I’m looking for a way that works without a reboot. :confused:

Okay, I wrote a little nodejs script now that should toggle the scaling between 1.0 and 2.0 on the primary display. But I’m getting an error for the ApplyMonitorsConfig call.

$ node ./index.js 
Current scaling is 1.0.
Changing scaling to 2.0!
Error: Command failed: gdbus call --session --dest=org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.ApplyMonitorsConfig "(uint32 1, [(0, 0, 2.0, uint32 0, true, [('DP-5', 'GSM', 'LG Ultra HD', '0x00067f03')], @a{sv} {}), (3840, 593, 1.0, 0, false, [('DP-2', 'RTK', 'X EQUIP', 'L56051794302')], {})], 1, {'layout-mode': <uint32 2>, 'legacy-ui-scaling-factor': <1>})"
Error parsing parameter 1 of type “u”: can not parse as value of type 'u':
  (uint32 1, [(0, 0, 2.0, uint32 0, true, [('DP-5', 'GSM', 'LG Ultra HD', '0x00067f03')], @a{sv} {}), (3840, 593, 1.0, 0, false, [('DP-2', 'RTK', 'X EQUIP', 'L56051794302')], {})], 1, {'layout-mode': <uint32 2>, 'legacy-ui-scaling-factor': <1>})
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

My script:

const { promisify } = require('util');
const exec = promisify(require('child_process').exec)

function sgvToJson(serializedGvariantValues) {
    let output = ""
    const openChars = "{["
    const closeChars = "}]"
    const sepChars = ",:"
    const input = serializedGvariantValues.replace(/\(/g, '[').replace(/\)/g, ']').replace(/, /g, ',').replace(/: /g, ':').replace(/'/g, '').replace(/"/g, '\\"')
    for (let [i,char] of Array.from(input).entries()) {
        let prevChar = input[i-1]
        let nextChar = input[i+1]
        if (sepChars.includes(char) && !closeChars.includes(prevChar)) {
            output+=`"`
        } else if (closeChars.includes(char) && !closeChars.includes(prevChar)) {
            output+=`"`
        }
        output+=char
        if (sepChars.includes(char) && !openChars.includes(nextChar)) {
            output+=`"`
        } else if (openChars.includes(char) && !openChars.includes(nextChar)) {
            output+=`"`
        }
    }
    output = output.replace(/\{\"\"\}/g, '{}').replace(/\[\"\"\]/g, '[]').replace(/"<true>"/g, 'true').replace(/"<false>"/g, 'false').replace(/("@a\{"sv"\} \{\})/g, '"@a{sv} {}"')
    return JSON.parse(output)
}

function createLogicalMonitorArrSgv(logicialMonitorsObj) {
    let output = "["
    for (const lm of logicialMonitorsObj) {
        output += `(${lm[0]}, ${lm[1]}, ${lm[2]}, ${lm[3]}, ${lm[4]}, [(${lm[5][0].map(el=>`'${el}'`).join(', ')})], `
        if (typeof lm[6] === 'string') {
            output += lm[6]
        } else {
            output += JSON.stringify(lm[6])
        }
        output += `), `
    }
    
    output = output.slice(0, -2) + "]"
    return output
}

function propertiesObjToSgvDict(properties) {
    return JSON.stringify(properties).replace(/"</g,'<').replace(/>"/g,'>').replace(/"/g,"'").replace(/([,:])/g,"$1 ")
}

function setPrimaryMonitorScalingOnStateObj(stateObj, scalingValue="1.0") {
    const logicalMonitors = stateObj[2]
    const primaryMonitor = logicalMonitors.filter(logical_monitor => logical_monitor[4] === "true")[0]
    primaryMonitor[2] = "2.0" // set scaling to 200%
    return stateObj
}
function getPrimaryMonitorScalingFromStateObj(stateObj) {
    const logicalMonitors = stateObj[2]
    const primaryMonitor = logicalMonitors.filter(logical_monitor => logical_monitor[4] === "true")[0]
    return primaryMonitor[2]
}

function createApplyMonitorsConfigParams(stateObj, withPrompt=false) {
    const serial = stateObj[0]
    const logical_monitors = createLogicalMonitorArrSgv(stateObj[2])
    const method = withPrompt ? 2 : 1
    const properties = propertiesObjToSgvDict(stateObj[3])
    const applyMonitorsConfigParamsAsSgv = `(${serial}, ${logical_monitors}, ${method}, ${properties})`
    return applyMonitorsConfigParamsAsSgv
}

async function GetCurrentState() {
    const currentStateSgv = (await exec(`gdbus call --session --dest=org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.GetCurrentState`)).stdout
    return currentStateSgv
}

async function ApplyMonitorsConfig(sgvParams) {
    return await exec(`gdbus call --session --dest=org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.ApplyMonitorsConfig "${sgvParams}"`)
}


async function togglePrimaryScaling() {
    const currentStateSgv = await GetCurrentState()
    const currentStateObj = sgvToJson(currentStateSgv)
    
    const currentScaling = getPrimaryMonitorScalingFromStateObj(currentStateObj)
    const newScaling = currentScaling === '2.0' ? '1.0' : '2.0'
    
    console.log(`Current scaling is ${currentScaling}.`)
    console.log(`Changing scaling to ${newScaling}!`)
    
    const changedSateObj = setPrimaryMonitorScalingOnStateObj(currentStateObj, newScaling)
    const sgvParams = createApplyMonitorsConfigParams(changedSateObj)
    return await ApplyMonitorsConfig(sgvParams)
}

async function main() {
    await togglePrimaryScaling()
}

main().catch(console.error)

:thinking: May be your want to try installing sudo dnf install gnome-monitor-config. Please see the help with gnome-monitor-config --help. Look like no longer supported. My bad.

Am I maybe calling the function incorrectly? I’m trying to use the exact format that is being returned by the GetCurrentState method for the call of ApplyMonitorsConfig. I’m for example wondering if I should really be passing things like uint32 and <uint32 2> and also wondering if I’m even right in the assumption that I should pass all my parameters as a single string appended to the gdbus call.

This is the generated command btw:

gdbus call --session --dest=org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.ApplyMonitorsConfig "(uint32 1, [(0, 0, 2.0, uint32 0, true, [('DP-5', 'GSM', 'LG Ultra HD', '0x00067f03')], @a{sv} {}), (3840, 593, 1.0, 0, false, [('DP-2', 'RTK', 'X EQUIP', 'L56051794302')], {})], 1, {'layout-mode': <uint32 2>, 'legacy-ui-scaling-factor': <1>})"
1 Like