Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
23f77cf
feat: add screenshot functionality and UI components
Gitkubikon Sep 27, 2025
7c1c3a7
feat: enhance media handling with new components and installation script
Gitkubikon Sep 28, 2025
f60ae78
feat: remove unused media components and optimize media list placehol…
Gitkubikon Sep 28, 2025
1c1286f
feat: add invert scroll direction option for enhanced user control
Gitkubikon Sep 28, 2025
9af0cfc
feat: update installation paths and enhance build configuration for l…
Gitkubikon Sep 28, 2025
2df8716
feat: unified media capture
Gitkubikon Sep 28, 2025
3f5d8ec
fix: placeholder overflow and animation stagger in MediaList
PixelKhaos Sep 28, 2025
eaf31d2
Merge branch 'screenshot-card' into feat/unified-capture
PixelKhaos Sep 28, 2025
06c6ca7
Merge pull request #1 from PixelKhaos/feat/unified-capture
Gitkubikon Sep 29, 2025
d54d43c
fix: improve height animations and overshoot handling in MediaList an…
Gitkubikon Sep 29, 2025
f963112
feat: add recording functionality with region selection and sound opt…
Gitkubikon Sep 29, 2025
2815a45
feat: update MediaList to use SortFilterProxyModel for improved sorti…
Gitkubikon Sep 30, 2025
f12fdf7
feat: enhance recording functionality with external start tracking an…
Gitkubikon Sep 30, 2025
a66e33b
feat: refactor recording and screenshot handling in Picker and Media …
Gitkubikon Sep 30, 2025
98d0010
feat: enhance area selection and recording functionality with improve…
Gitkubikon Sep 30, 2025
13ebebd
feat: implement fast polling for instant recording state detection in…
Gitkubikon Sep 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ watch_file **/CMakeLists.txt
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv
cmake --build build
export CAELESTIA_LIB_DIR="$PWD/build/lib"
export QML2_IMPORT_PATH="$PWD/build/qml:${QML2_IMPORT_PATH:-}"
export QML2_IMPORT_PATH="$PWD/build/qml:${QML2_IMPORT_PATH:-}"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/.qmlls.ini
build/
.cache/
.local/
32 changes: 32 additions & 0 deletions components/containers/WrapperMouseArea.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Layouts
import QtQuick.Handlers

Item {
id: root

// allow using Layout attached props at call sites
Layout.fillWidth: true

// expose a cursor shape like MouseArea
property alias cursorShape: hover.cursorShape

signal clicked()

// Use pointer handlers so child controls remain interactive
HoverHandler { id: hover; cursorShape: Qt.ArrowCursor }
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}

// forward arbitrary children into this content item
default property alias data: contentItem.data

Item {
id: contentItem
anchors.fill: parent
}
}
1 change: 1 addition & 0 deletions config/BarConfig.qml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ JsonObject {
property bool workspaces: true
property bool volume: true
property bool brightness: true
property bool invertScrollDirection: false
}

component Workspaces: JsonObject {
Expand Down
1 change: 1 addition & 0 deletions config/GeneralConfig.qml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ JsonObject {
property list<string> audio: ["pavucontrol"]
property list<string> playback: ["mpv"]
property list<string> explorer: ["thunar"]
property list<string> image: ["swappy", "-f"]
}

component Idle: JsonObject {
Expand Down
2 changes: 1 addition & 1 deletion config/UtilitiesConfig.qml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ JsonObject {
property bool capsLockChanged: true
property bool numLockChanged: true
}
}
}
25 changes: 25 additions & 0 deletions install-system.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e

# Configure with system prefix
rm -rf build
cmake -S . -B build \
-G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_COMPILER=clazy \
-DCMAKE_INSTALL_PREFIX=/usr \
-DINSTALL_LIBDIR=lib/caelestia \
-DINSTALL_QMLDIR=lib/qt6/qml \
-DINSTALL_QSCONFDIR=etc/xdg/quickshell/caelestia \
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON

# Build
cmake --build build

# Install system-wide (requires sudo)
sudo cmake --install build

echo "✅ Caelestia installed system-wide into /usr"
echo "👉 After reboot, run QuickShell with:"
echo " QS_CONFIG_NAME=caelestia quickshell"
28 changes: 28 additions & 0 deletions modules/areapicker/AreaPicker.qml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound

import qs.components.containers
import qs.components.misc
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
Expand All @@ -12,6 +13,10 @@ Scope {

property bool freeze
property bool closing
property bool recording: false
property bool recordWithSound: false

// no-op

Variants {
model: Quickshell.screens
Expand Down Expand Up @@ -40,6 +45,8 @@ Scope {
Picker {
loader: root
screen: win.modelData
recording: root.recording
recordWithSound: root.recordWithSound
}
}
}
Expand All @@ -51,12 +58,33 @@ Scope {
function open(): void {
root.freeze = false;
root.closing = false;
root.recording = false;
root.recordWithSound = false;
root.activeAsync = true;
}

function openFreeze(): void {
root.freeze = true;
root.closing = false;
root.recording = false;
root.recordWithSound = false;
root.activeAsync = true;
}

// Recording variants reuse the same area picker to select geometry, then start Recorder
function openRecord(): void {
root.freeze = false;
root.closing = false;
root.recording = true;
root.recordWithSound = false;
root.activeAsync = true;
}

function openRecordSound(): void {
root.freeze = false;
root.closing = false;
root.recording = true;
root.recordWithSound = true;
root.activeAsync = true;
}
}
Expand Down
44 changes: 36 additions & 8 deletions modules/areapicker/Picker.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import qs.utils
import Caelestia
import Quickshell
import Quickshell.Wayland
Expand All @@ -14,6 +15,10 @@ MouseArea {

required property LazyLoader loader
required property ShellScreen screen
// When true, we will start a recording of the selected region instead of taking a screenshot
property bool recording: false
// Only used when recording=true. If true, include default output+input audio.
property bool recordWithSound: false

property bool onClient

Expand Down Expand Up @@ -72,8 +77,36 @@ MouseArea {
}

function save(): void {
const tmpfile = Qt.resolvedUrl(`/tmp/caelestia-picker-${Quickshell.processId}-${Date.now()}.png`);
CUtils.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached(["swappy", "-f", path]));
// Compute logical coordinates (same for both screenshots and recordings)
// Use consistent rounding to avoid invalid regions
const screenRelX = Math.floor(rsx);
const screenRelY = Math.floor(rsy);
const screenRelW = Math.max(1, Math.ceil(sw)); // Ensure minimum 1px width
const screenRelH = Math.max(1, Math.ceil(sh)); // Ensure minimum 1px height

// Convert to global logical coordinates
const globalX = screenRelX + screen.x;
const globalY = screenRelY + screen.y;
const region = `${screenRelW}x${screenRelH}+${globalX}+${globalY}`;

if (root.loader.recording) {
// Use CLI for recording - it handles fractional scaling conversion to physical pixels
const cmd = ["caelestia", "record", "-r", region];
if (root.loader.recordWithSound) {
cmd.push("-s");
}
// Start fast polling for instant recording state detection
Recorder.startFastPolling();
Quickshell.execDetached(cmd);
} else {
// Use CLI for screenshot - it handles notifications and actions
const cmd = ["caelestia", "screenshot", "-r", region];
if (root.loader.freeze) {
cmd.push("-f");
}
Quickshell.execDetached(cmd);
}

closeAnim.start();
}

Expand Down Expand Up @@ -195,12 +228,7 @@ MouseArea {
sourceComponent: ScreencopyView {
captureSource: root.screen

onHasContentChanged: {
if (hasContent && !root.loader.freeze) {
overlay.visible = border.visible = true;
root.save();
}
}
onHasContentChanged: hasContent && !root.loader.freeze && (overlay.visible = border.visible = true, root.save())
}
}

Expand Down
11 changes: 6 additions & 5 deletions modules/bar/Bar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ ColumnLayout {
}

function handleWheel(y: real, angleDelta: point): void {
const invert = Config.bar.scrollActions.invertScrollDirection;
const ch = childAt(width / 2, y) as WrappedLoader;
if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) {
// Workspace scroll
Expand All @@ -84,19 +85,19 @@ ColumnLayout {
if (specialWs?.length > 0)
Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`);
else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1)
Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`);
Hypr.dispatch(`workspace r${angleDelta.y > 0 ? (invert ? "+" : "-") : (invert ? "-" : "+")}1`);
} else if (y < screen.height / 2 && Config.bar.scrollActions.volume) {
// Volume scroll on top half
if (angleDelta.y > 0)
if ((angleDelta.y > 0) !== invert)
Audio.incrementVolume();
else if (angleDelta.y < 0)
else if ((angleDelta.y < 0) !== invert)
Audio.decrementVolume();
} else if (Config.bar.scrollActions.brightness) {
// Brightness scroll on bottom half
const monitor = Brightness.getMonitorForScreen(screen);
if (angleDelta.y > 0)
if ((angleDelta.y > 0) !== invert)
monitor.setBrightness(monitor.brightness + 0.1);
else if (angleDelta.y < 0)
else if ((angleDelta.y < 0) !== invert)
monitor.setBrightness(monitor.brightness - 0.1);
}
}
Expand Down
13 changes: 9 additions & 4 deletions modules/utilities/Content.qml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import "cards"
import "cards" as UtilCards
import qs.config
import QtQuick
import QtQuick.Layouts
Expand All @@ -18,20 +18,25 @@ Item {
anchors.fill: parent
spacing: Appearance.spacing.normal

IdleInhibit {}
UtilCards.IdleInhibit {}

Record {
// Combined media card: Screenshots + Recordings in tabs
UtilCards.Media {
props: root.props
visibilities: root.visibilities
z: 1
}

Toggles {
UtilCards.Toggles {
visibilities: root.visibilities
}
}

RecordingDeleteModal {
props: root.props
}

ScreenshotDeleteModal {
props: root.props
}
}
1 change: 1 addition & 0 deletions modules/utilities/RecordingDeleteModal.qml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Loader {
required property var props

anchors.fill: parent
z: 1000

opacity: root.props.recordingConfirmDelete ? 1 : 0
active: opacity > 0
Expand Down
Loading