2
1
Fork 0
quickshell-docs/content/docs/configuration/intro.md
2024-03-12 05:31:39 -07:00

14 KiB

+++ title = "Introduction" +++

This page will walk you through the process of creating a simple bar/panel, and introduce you to all the basic concepts involved.

There are many links to the QML Overview and Type Reference which you should follow if you don't fully understand the concepts involved.

Shell Files

Every quickshell instance starts from a shell root file, conventionally named shell.qml. The default path is ~/.config/quickshell/shell.qml. (where ~/.config can be substituted with $XDG_CONFIG_HOME if present.)

Each shell file starts with the shell root object. Only one may exist per configuration.

import Quickshell

ShellRoot {
  // ...
}

The shell root is not a visual element but instead contains all of the visual and non visual objects in your shell. You can have multiple different shells with shared components and different shell roots.

{{% details title="Shell search paths and manifests" closed="true" %}}

Quickshell can be launched with configurations in locations other than the default one.

The -p or --path option will launch the shell root at the given path. It will also accept folders with a shell.qml file in them. It can also be specified via the QS_CONFIG_PATH environment variable.

The -c or --config option will launch a configuration from the current manifest, or if no manifest is specified, a subfolder of quickshell's base path. It can also be specified via the QS_CONFIG_NAME environment variable.

The base path defaults to ~/.config/quickshell, but can be changed using the QS_BASE_PATH environment variable.

The -m or --manifest option specifies the quickshell manifest to read configs from. When used with -c, the config will be chosen by name from the manifest. It can also be specified via the QS_MANIFEST environment variable.

The manifest path defaults to ~/.config/quickshell/manifest.conf and is a list of name = path pairs where path can be relative or absolute. Lines starting with # are comments.

# ~/.config/quickshell/manifest.conf
myconf1 = myconf
myconf2 = ./myconf
myconf3 = myconf/shell.nix
myconf4 = ~/.config/quickshell/myconf

You can use quickshell --current to print the current values of any of these options and what set them.

{{% /details %}}

Creating Windows

Quickshell has two main window types available, PanelWindow for bars and widgets, and FloatingWindow for standard desktop windows.

We'll start with an example:

import Quickshell // for ShellRoot and PanelWindow
import QtQuick // for Text

ShellRoot {
  PanelWindow {
    anchors {
      top: true
      left: true
      right: true
    }

    height: 30

    Text {
      // center the bar in its parent component (the window)
      anchors.centerIn: parent

      text: "hello world"
    }
  }
}

The above example creates a bar/panel on your currently focused monitor with a centered piece of text. It will also reserve space for itself on your monitor.

More information about available properties is available in the type reference.

Running a process

Now that we have a piece of text, what if it did something useful? To start with lets make a clock. To get the time we'll use the date command.

We can use a Process object to run commands and return their results.

We'll listen to the DataStreamParser.read signal emitted by SplitParser using a signal handler to update the text on the clock.

{{< callout type="info" >}} Note: Quickshell live-reloads your code. You can leave it open and edit the original file. The panel will reload when you save it. {{< /callout >}}

import Quickshell
import Quickshell.Io // for Process
import QtQuick

ShellRoot {
  PanelWindow {
    anchors {
      top: true
      left: true
      right: true
    }

    height: 30

    Text {
      // give the text an ID we can refer to elsewhere in the file
      id: clock

      anchors.centerIn: parent

      // create a process management object
      Process {
        // the command it will run, every argument is its own string
        command: ["date"]

        // run the command immediately
        running: true

        // process the stdout stream using a SplitParser
        // which returns chunks of output after a delimiter
        stdout: SplitParser {
          // listen for the read signal, which returns the data that was read
          // from stdout, then write that data to the clock's text property
          onRead: data => clock.text = data
        }
      }
    }
  }
}

Running code at an interval

With the above example, your bar should now display the time, but it isn't updating! Let's use a Timer fix that.

import Quickshell
import Quickshell.Io
import QtQuick

ShellRoot {
  PanelWindow {
    anchors {
      top: true
      left: true
      right: true
    }

    height: 30

    Text {
      id: clock
      anchors.centerIn: parent

      Process {
        // give the process object an id so we can talk
        // about it from the timer
        id: dateProc

        command: ["date"]
        running: true

        stdout: SplitParser {
          onRead: data => clock.text = data
        }
      }

      // use a timer to rerun the process at an interval
      Timer {
        // 1000 milliseconds is 1 second
        interval: 1000

        // start the timer immediately
        running: true

        // run the timer again when it ends
        repeat: true

        // when the timer is triggered, set the running property of the
        // process to true, which reruns it if stopped.
        onTriggered: dateProc.running = true
      }
    }
  }
}

Reuseable components

If you have multiple monitors you might have noticed that your bar is only on one of them. If not, you'll still want to follow this section to make sure your bar dosen't disappear if your monitor disconnects.

We can use a Variants object to create instances of non widget items. (See Repeater for doing something similar with visual items.)

The Variants type creates instances of a Component based on a data model you supply. (A component is a re-usable tree of objects.)

The most common use of Variants in a shell is to create instances of a window (your bar) based on your monitor list (the data model).

Variants will inject the properties in the data model directly into the component, meaning we can easily set the screen property of our bar (See Window.screen.)

import Quickshell
import Quickshell.Io
import QtQuick

ShellRoot {
  Variants {
    variants: {
      // get the list of screens from the Quickshell singleton
      const screens = Quickshell.screens;

      // transform the screen list into a list of objects with
      // screen variables, which will be set for each created object
      const variants = screens.map(screen => {
        return { screen: screen };
      });

      return variants;
    }

    component: Component {
      PanelWindow {
        // the screen property will be injected into the window,
        // so each bar displays on the right monitor

        anchors {
          top: true
          left: true
          right: true
        }

        height: 30

        Text {
          id: clock
          anchors.centerIn: parent

          Process {
            id: dateProc
            command: ["date"]
            running: true

            stdout: SplitParser {
              onRead: data => clock.text = data
            }
          }

          Timer {
            interval: 1000
            running: true
            repeat: true
            onTriggered: dateProc.running = true
          }
        }
      }
    }
  }
}

See also: Property Bindings, Variants.component, Quickshell.screens, Array.map

With this example, bars will be created and destroyed as you plug and unplug them, due to the reactive nature of the Quickshell.screens property. (See: Reactive Bindings.)

Now there's an important problem you might have noticed: when the window is created multiple times we also make a new Process and Timer. We can fix this by moving the Process and Timer outside of the window.

{{< callout type="error" >}} This code will not work correctly. {{< /callout >}}

import Quickshell
import Quickshell.Io
import QtQuick

ShellRoot {
  Variants {
    variants: Quickshell.screens.map(screen => ({ screen }))

    component: Component {
      PanelWindow {
        anchors {
          top: true
          left: true
          right: true
        }

        height: 30

        Text {
          id: clock
          anchors.centerIn: parent
        }
      }
    }
  }

  Process {
    id: dateProc
    command: ["date"]
    running: true

    stdout: SplitParser {
      onRead: data => clock.text = data
    }
  }

  Timer {
    interval: 1000
    running: true
    repeat: true
    onTriggered: dateProc.running = true
  }
}

However there is a problem with naively moving the Process and Timer out of the component. What about the clock that the process references?

If you run the above example you'll see something like this in the console every second:

file:///home/name/.config/quickshell/shell.qml:33: ReferenceError: clock is not defined
file:///home/name/.config/quickshell/shell.qml:33: ReferenceError: clock is not defined
file:///home/name/.config/quickshell/shell.qml:33: ReferenceError: clock is not defined
file:///home/name/.config/quickshell/shell.qml:33: ReferenceError: clock is not defined
file:///home/name/.config/quickshell/shell.qml:33: ReferenceError: clock is not defined

This is because the clock object, even though it has an ID, cannot be referenced outside of its component. Remember, components can be created any number of times, including zero, so clock may not exist or there may be more than one, meaning there isn't an object to refer to from here.

We can fix it with a Property Definition.

We can define a property inside of the ShellRoot and reference it from the clock text instead. Due to QML's Reactive Bindings, the clock text will be updated when we update the property for every clock that currently exists.

import Quickshell
import Quickshell.Io
import QtQuick

ShellRoot {
  id: root

  // add a property in the root
  property string time;

  Variants {
    variants: Quickshell.screens.map(screen => ({ screen }))

    component: Component {
      PanelWindow {
        anchors {
          top: true
          left: true
          right: true
        }

        height: 30

        Text {
          // remove the id as we don't need it anymore

          anchors.centerIn: parent

          // bind the text to the root's time property
          text: root.time
        }
      }
    }
  }

  Process {
    id: dateProc
    command: ["date"]
    running: true

    stdout: SplitParser {
      // update the property instead of the clock directly
      onRead: data => root.time = data
    }
  }

  Timer {
    interval: 1000
    running: true
    repeat: true
    onTriggered: dateProc.running = true
  }
}

Now we've fixed the problem so there's nothing actually wrong with the above code, but we can make it more concise:

  1. Components can be defined implicitly, meaning we can remove the component wrapping the window and place the window directly into the component property.
  2. The Variants.component property is a Default Property, which means we can skip the component: part of the assignment. We're already using ShellRoot's default property to store our Variants, Process, and Timer components among other things.
  3. The ShellRoot dosen't actually need an id property to talk about the time property, as it is the outermost object in the file which has special scoping rules.

This is what our shell looks like with the above (optional) cleanup:

import Quickshell
import Quickshell.Io
import QtQuick

ShellRoot {
  property string time;

  Variants {
    variants: Quickshell.screens.map(screen => ({ screen }))

    PanelWindow {
      anchors {
        top: true
        left: true
        right: true
      }

      height: 30

      Text {
        anchors.centerIn: parent

        // now just time instead of root.time
        text: time
      }
    }
  }

  Process {
    id: dateProc
    command: ["date"]
    running: true

    stdout: SplitParser {
      // now just time instead of root.time
      onRead: data => time = data
    }
  }

  Timer {
    interval: 1000
    running: true
    repeat: true
    onTriggered: dateProc.running = true
  }
}