mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
Compare commits
7 Commits
2e1bed5fb5
...
d23fc9f2df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d23fc9f2df | ||
|
|
7ac5191e8d | ||
|
|
29d27ebd6d | ||
|
|
e45075dd84 | ||
|
|
80bc87e76b | ||
|
|
76d88517ec | ||
|
|
151d695212 |
@@ -16,3 +16,4 @@ This file is more of a quick reference so I know what to account for before next
|
||||
- Initial RTL support/i18n
|
||||
- Theme registry
|
||||
- Notification persistence & history
|
||||
- **BREAKING** vscode theme needs re-installed
|
||||
|
||||
@@ -68,3 +68,9 @@ packages:
|
||||
outpkg: mocks_wlclient
|
||||
interfaces:
|
||||
WaylandDisplay:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
||||
config:
|
||||
dir: "internal/mocks/utils"
|
||||
outpkg: mocks_utils
|
||||
interfaces:
|
||||
AppChecker:
|
||||
|
||||
@@ -676,7 +676,7 @@ func checkOptionalDependencies() []checkResult {
|
||||
}{
|
||||
{"matugen", "matugen", "", "Dynamic theming", true},
|
||||
{"dgop", "dgop", "", "System monitoring", true},
|
||||
{"cava", "cava", "", "Audio waveform", false},
|
||||
{"cava", "cava", "", "Audio visualizer", true},
|
||||
{"khal", "khal", "", "Calendar events", false},
|
||||
{"Network", "nmcli", "iwctl", "Network management", false},
|
||||
{"danksearch", "dsearch", "", "File search", false},
|
||||
|
||||
@@ -51,6 +51,7 @@ type Options struct {
|
||||
SyncModeWithPortal bool
|
||||
TerminalsAlwaysDark bool
|
||||
SkipTemplates string
|
||||
AppChecker utils.AppChecker
|
||||
}
|
||||
|
||||
type ColorsOutput struct {
|
||||
@@ -101,6 +102,9 @@ func Run(opts Options) error {
|
||||
if opts.IconTheme == "" {
|
||||
opts.IconTheme = "System Default"
|
||||
}
|
||||
if opts.AppChecker == nil {
|
||||
opts.AppChecker = utils.DefaultAppChecker{}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create state dir: %w", err)
|
||||
@@ -309,11 +313,11 @@ output_path = '%s'
|
||||
|
||||
if !opts.ShouldSkipTemplate("vscode") {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
||||
}
|
||||
|
||||
if opts.RunUserTemplates {
|
||||
@@ -353,10 +357,7 @@ func appendConfig(
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return
|
||||
}
|
||||
cmdExists := checkCmd == nil || utils.AnyCommandExists(checkCmd...)
|
||||
flatpakExists := checkFlatpaks == nil || utils.AnyFlatpakExists(checkFlatpaks...)
|
||||
|
||||
if !cmdExists && !flatpakExists {
|
||||
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
@@ -372,10 +373,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return
|
||||
}
|
||||
cmdExists := checkCmd == nil || utils.AnyCommandExists(checkCmd...)
|
||||
flatpakExists := checkFlatpaks == nil || utils.AnyFlatpakExists(checkFlatpaks...)
|
||||
|
||||
if !cmdExists && !flatpakExists {
|
||||
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
@@ -428,10 +426,28 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
||||
cfgFile.WriteString("\n")
|
||||
}
|
||||
|
||||
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
|
||||
if _, err := os.Stat(extDir); err != nil {
|
||||
func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool {
|
||||
// Both nil is treated as "skip check" / unconditionally run
|
||||
if checkCmd == nil && checkFlatpaks == nil {
|
||||
return true
|
||||
}
|
||||
if checkCmd != nil && checker.AnyCommandExists(checkCmd...) {
|
||||
return true
|
||||
}
|
||||
if checkFlatpaks != nil && checker.AnyFlatpakExists(checkFlatpaks...) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendVSCodeConfig(cfgFile *os.File, name, extBaseDir, shellDir string) {
|
||||
pattern := filepath.Join(extBaseDir, "danklinux.dms-theme-*")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil || len(matches) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
extDir := matches[0]
|
||||
templateDir := filepath.Join(shellDir, "matugen", "templates")
|
||||
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
|
||||
input_path = '%s/vscode-color-theme-default.json'
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||
)
|
||||
|
||||
func TestAppendConfigBinaryExists(t *testing.T) {
|
||||
@@ -28,7 +30,10 @@ func TestAppendConfigBinaryExists(t *testing.T) {
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyCommandExists("sh").Return(true)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml")
|
||||
|
||||
@@ -68,7 +73,11 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
||||
mockChecker.EXPECT().AnyFlatpakExists().Return(false)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml")
|
||||
|
||||
@@ -105,7 +114,10 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyFlatpakExists("app.zen_browser.zen").Return(true)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml")
|
||||
|
||||
@@ -142,7 +154,11 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyCommandExists().Return(false)
|
||||
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
||||
|
||||
@@ -179,7 +195,10 @@ func TestAppendConfigBothExist(t *testing.T) {
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyCommandExists("sh").Return(true)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml")
|
||||
|
||||
@@ -216,7 +235,11 @@ func TestAppendConfigNeitherExists(t *testing.T) {
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
||||
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
||||
|
||||
|
||||
242
core/internal/mocks/utils/mock_AppChecker.go
Normal file
242
core/internal/mocks/utils/mock_AppChecker.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package mocks_utils
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
// MockAppChecker is an autogenerated mock type for the AppChecker type
|
||||
type MockAppChecker struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockAppChecker_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockAppChecker) EXPECT() *MockAppChecker_Expecter {
|
||||
return &MockAppChecker_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// AnyCommandExists provides a mock function with given fields: cmds
|
||||
func (_m *MockAppChecker) AnyCommandExists(cmds ...string) bool {
|
||||
_va := make([]interface{}, len(cmds))
|
||||
for _i := range cmds {
|
||||
_va[_i] = cmds[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AnyCommandExists")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(...string) bool); ok {
|
||||
r0 = rf(cmds...)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAppChecker_AnyCommandExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyCommandExists'
|
||||
type MockAppChecker_AnyCommandExists_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AnyCommandExists is a helper method to define mock.On call
|
||||
// - cmds ...string
|
||||
func (_e *MockAppChecker_Expecter) AnyCommandExists(cmds ...interface{}) *MockAppChecker_AnyCommandExists_Call {
|
||||
return &MockAppChecker_AnyCommandExists_Call{Call: _e.mock.On("AnyCommandExists",
|
||||
append([]interface{}{}, cmds...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_AnyCommandExists_Call) Run(run func(cmds ...string)) *MockAppChecker_AnyCommandExists_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]string, len(args)-0)
|
||||
for i, a := range args[0:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(string)
|
||||
}
|
||||
}
|
||||
run(variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_AnyCommandExists_Call) Return(_a0 bool) *MockAppChecker_AnyCommandExists_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_AnyCommandExists_Call) RunAndReturn(run func(...string) bool) *MockAppChecker_AnyCommandExists_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// AnyFlatpakExists provides a mock function with given fields: flatpaks
|
||||
func (_m *MockAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
|
||||
_va := make([]interface{}, len(flatpaks))
|
||||
for _i := range flatpaks {
|
||||
_va[_i] = flatpaks[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AnyFlatpakExists")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(...string) bool); ok {
|
||||
r0 = rf(flatpaks...)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAppChecker_AnyFlatpakExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AnyFlatpakExists'
|
||||
type MockAppChecker_AnyFlatpakExists_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AnyFlatpakExists is a helper method to define mock.On call
|
||||
// - flatpaks ...string
|
||||
func (_e *MockAppChecker_Expecter) AnyFlatpakExists(flatpaks ...interface{}) *MockAppChecker_AnyFlatpakExists_Call {
|
||||
return &MockAppChecker_AnyFlatpakExists_Call{Call: _e.mock.On("AnyFlatpakExists",
|
||||
append([]interface{}{}, flatpaks...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_AnyFlatpakExists_Call) Run(run func(flatpaks ...string)) *MockAppChecker_AnyFlatpakExists_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]string, len(args)-0)
|
||||
for i, a := range args[0:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(string)
|
||||
}
|
||||
}
|
||||
run(variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_AnyFlatpakExists_Call) Return(_a0 bool) *MockAppChecker_AnyFlatpakExists_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_AnyFlatpakExists_Call) RunAndReturn(run func(...string) bool) *MockAppChecker_AnyFlatpakExists_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// CommandExists provides a mock function with given fields: cmd
|
||||
func (_m *MockAppChecker) CommandExists(cmd string) bool {
|
||||
ret := _m.Called(cmd)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CommandExists")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(cmd)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAppChecker_CommandExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommandExists'
|
||||
type MockAppChecker_CommandExists_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CommandExists is a helper method to define mock.On call
|
||||
// - cmd string
|
||||
func (_e *MockAppChecker_Expecter) CommandExists(cmd interface{}) *MockAppChecker_CommandExists_Call {
|
||||
return &MockAppChecker_CommandExists_Call{Call: _e.mock.On("CommandExists", cmd)}
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_CommandExists_Call) Run(run func(cmd string)) *MockAppChecker_CommandExists_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_CommandExists_Call) Return(_a0 bool) *MockAppChecker_CommandExists_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_CommandExists_Call) RunAndReturn(run func(string) bool) *MockAppChecker_CommandExists_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// FlatpakExists provides a mock function with given fields: name
|
||||
func (_m *MockAppChecker) FlatpakExists(name string) bool {
|
||||
ret := _m.Called(name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for FlatpakExists")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAppChecker_FlatpakExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FlatpakExists'
|
||||
type MockAppChecker_FlatpakExists_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// FlatpakExists is a helper method to define mock.On call
|
||||
// - name string
|
||||
func (_e *MockAppChecker_Expecter) FlatpakExists(name interface{}) *MockAppChecker_FlatpakExists_Call {
|
||||
return &MockAppChecker_FlatpakExists_Call{Call: _e.mock.On("FlatpakExists", name)}
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_FlatpakExists_Call) Run(run func(name string)) *MockAppChecker_FlatpakExists_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_FlatpakExists_Call) Return(_a0 bool) *MockAppChecker_FlatpakExists_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAppChecker_FlatpakExists_Call) RunAndReturn(run func(string) bool) *MockAppChecker_FlatpakExists_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockAppChecker creates a new instance of MockAppChecker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockAppChecker(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockAppChecker {
|
||||
mock := &MockAppChecker{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -5,6 +5,31 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AppChecker interface {
|
||||
CommandExists(cmd string) bool
|
||||
AnyCommandExists(cmds ...string) bool
|
||||
FlatpakExists(name string) bool
|
||||
AnyFlatpakExists(flatpaks ...string) bool
|
||||
}
|
||||
|
||||
type DefaultAppChecker struct{}
|
||||
|
||||
func (DefaultAppChecker) CommandExists(cmd string) bool {
|
||||
return CommandExists(cmd)
|
||||
}
|
||||
|
||||
func (DefaultAppChecker) AnyCommandExists(cmds ...string) bool {
|
||||
return AnyCommandExists(cmds...)
|
||||
}
|
||||
|
||||
func (DefaultAppChecker) FlatpakExists(name string) bool {
|
||||
return FlatpakExists(name)
|
||||
}
|
||||
|
||||
func (DefaultAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
|
||||
return AnyFlatpakExists(flatpaks...)
|
||||
}
|
||||
|
||||
func CommandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
|
||||
@@ -210,21 +210,43 @@ func TestFlatpakInstallationDirCommandFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnyFlatpakExistsSomeExist(t *testing.T) {
|
||||
if !FlatpakInPath() {
|
||||
t.Skip("flatpak not in PATH")
|
||||
tempDir := t.TempDir()
|
||||
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||
|
||||
// Script that succeeds only for "app.exists.test"
|
||||
script := `#!/bin/sh
|
||||
if [ "$1" = "info" ] && [ "$2" = "app.exists.test" ]; then
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
`
|
||||
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||
}
|
||||
|
||||
result := AnyFlatpakExists("com.nonexistent.flatpak", "app.zen_browser.zen", "com.another.nonexistent")
|
||||
originalPath := os.Getenv("PATH")
|
||||
t.Setenv("PATH", tempDir+":"+originalPath)
|
||||
|
||||
result := AnyFlatpakExists("com.nonexistent.flatpak", "app.exists.test", "com.another.nonexistent")
|
||||
if !result {
|
||||
t.Errorf("expected true when at least one flatpak exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnyFlatpakExistsNoneExist(t *testing.T) {
|
||||
if !FlatpakInPath() {
|
||||
t.Skip("flatpak not in PATH")
|
||||
tempDir := t.TempDir()
|
||||
fakeFlatpak := filepath.Join(tempDir, "flatpak")
|
||||
|
||||
script := "#!/bin/sh\nexit 1\n"
|
||||
err := os.WriteFile(fakeFlatpak, []byte(script), 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create fake flatpak: %v", err)
|
||||
}
|
||||
|
||||
originalPath := os.Getenv("PATH")
|
||||
t.Setenv("PATH", tempDir+":"+originalPath)
|
||||
|
||||
result := AnyFlatpakExists("com.nonexistent.flatpak1", "com.nonexistent.flatpak2")
|
||||
if result {
|
||||
t.Errorf("expected false when no flatpaks exist")
|
||||
|
||||
@@ -336,7 +336,7 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.theme) {
|
||||
Theme.switchTheme(SettingsData.theme);
|
||||
} else {
|
||||
Theme.switchTheme("blue");
|
||||
Theme.switchTheme("purple");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ Singleton {
|
||||
property alias dankBarCenterWidgetsModel: centerWidgetsModel
|
||||
property alias dankBarRightWidgetsModel: rightWidgetsModel
|
||||
|
||||
property string currentThemeName: "blue"
|
||||
property string currentThemeName: "purple"
|
||||
property string currentThemeCategory: "generic"
|
||||
property string customThemeFile: ""
|
||||
property var registryThemeVariants: ({})
|
||||
|
||||
@@ -30,7 +30,7 @@ Singleton {
|
||||
return useAuto ? Math.max(4, spacing) : manualValue;
|
||||
}
|
||||
|
||||
property string currentTheme: "blue"
|
||||
property string currentTheme: "purple"
|
||||
property string currentThemeCategory: "generic"
|
||||
property bool isLightMode: typeof SessionData !== "undefined" ? SessionData.isLightMode : false
|
||||
property bool colorsFileLoadFailed: false
|
||||
@@ -196,7 +196,7 @@ Singleton {
|
||||
|
||||
readonly property var currentThemeData: {
|
||||
if (currentTheme === "custom") {
|
||||
return customThemeData || StockThemes.getThemeByName("blue", isLightMode);
|
||||
return customThemeData || StockThemes.getThemeByName("purple", isLightMode);
|
||||
} else if (currentTheme === dynamic) {
|
||||
return {
|
||||
"primary": getMatugenColor("primary", "#42a5f5"),
|
||||
|
||||
@@ -6,7 +6,7 @@ function percentToUnit(v) {
|
||||
}
|
||||
|
||||
var SPEC = {
|
||||
currentThemeName: { def: "blue", onChange: "applyStoredTheme" },
|
||||
currentThemeName: { def: "purple", onChange: "applyStoredTheme" },
|
||||
currentThemeCategory: { def: "generic" },
|
||||
customThemeFile: { def: "" },
|
||||
registryThemeVariants: { def: {} },
|
||||
|
||||
@@ -3,6 +3,7 @@ import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Greeter
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.Spotlight
|
||||
import qs.Modules
|
||||
@@ -816,4 +817,20 @@ Item {
|
||||
id: niriOverviewOverlay
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: greeterLoader
|
||||
active: false
|
||||
sourceComponent: GreeterModal {
|
||||
onGreeterCompleted: greeterLoader.active = false
|
||||
Component.onCompleted: show()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: FirstLaunchService
|
||||
function onGreeterRequested() {
|
||||
greeterLoader.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,14 @@ FloatingWindow {
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
if (parentModal) {
|
||||
if (parentModal && "shouldHaveFocus" in parentModal) {
|
||||
parentModal.shouldHaveFocus = false;
|
||||
parentModal.allowFocusOverride = true;
|
||||
}
|
||||
content.reset();
|
||||
Qt.callLater(() => content.forceActiveFocus());
|
||||
} else {
|
||||
if (parentModal) {
|
||||
if (parentModal && "allowFocusOverride" in parentModal) {
|
||||
parentModal.allowFocusOverride = false;
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
|
||||
}
|
||||
|
||||
492
quickshell/Modals/Greeter/GreeterCompletePage.qml
Normal file
492
quickshell/Modals/Greeter/GreeterCompletePage.qml
Normal file
@@ -0,0 +1,492 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var greeterRoot: parent ? parent.greeterRoot : null
|
||||
|
||||
readonly property real headerIconContainerSize: Math.round(Theme.iconSize * 2)
|
||||
readonly property real sectionIconSize: Theme.iconSizeSmall + 2
|
||||
readonly property real keybindRowHeight: Math.round(Theme.fontSizeMedium * 2)
|
||||
readonly property real keyBadgeHeight: Math.round(Theme.fontSizeSmall * 1.83)
|
||||
|
||||
readonly property var featureNames: ({
|
||||
"spotlight": "App Launcher",
|
||||
"clipboard": "Clipboard",
|
||||
"processlist": "Task Manager",
|
||||
"settings": "Settings",
|
||||
"notifications": "Notifications",
|
||||
"notepad": "Notepad",
|
||||
"hotkeys": "Keybinds",
|
||||
"lock": "Lock Screen",
|
||||
"dankdash": "Dashboard"
|
||||
})
|
||||
|
||||
function getFeatureDesc(action) {
|
||||
const match = action.match(/dms\s+ipc\s+call\s+(\w+)/);
|
||||
if (match && featureNames[match[1]])
|
||||
return featureNames[match[1]];
|
||||
return null;
|
||||
}
|
||||
|
||||
readonly property var dmsKeybinds: {
|
||||
if (!greeterRoot || !greeterRoot.cheatsheetLoaded || !greeterRoot.cheatsheetData || !greeterRoot.cheatsheetData.binds)
|
||||
return [];
|
||||
const seen = new Set();
|
||||
const binds = [];
|
||||
const allBinds = greeterRoot.cheatsheetData.binds;
|
||||
for (const category in allBinds) {
|
||||
const categoryBinds = allBinds[category];
|
||||
for (let i = 0; i < categoryBinds.length; i++) {
|
||||
const bind = categoryBinds[i];
|
||||
if (!bind.key || !bind.action)
|
||||
continue;
|
||||
if (!bind.action.includes("dms"))
|
||||
continue;
|
||||
if (!(bind.action.includes("spawn") || bind.action.includes("exec")))
|
||||
continue;
|
||||
const feature = getFeatureDesc(bind.action);
|
||||
if (!feature)
|
||||
continue;
|
||||
if (seen.has(feature))
|
||||
continue;
|
||||
seen.add(feature);
|
||||
binds.push({
|
||||
key: bind.key,
|
||||
desc: feature
|
||||
});
|
||||
}
|
||||
}
|
||||
return binds;
|
||||
}
|
||||
|
||||
readonly property bool hasKeybinds: dmsKeybinds.length > 0
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingL * 2
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.min(640, parent.width - Theme.spacingXL * 2)
|
||||
topPadding: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: root.headerIconContainerSize
|
||||
height: root.headerIconContainerSize
|
||||
radius: Math.round(root.headerIconContainerSize * 0.29)
|
||||
color: Theme.withAlpha(Theme.success, 0.15)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize + 4
|
||||
color: Theme.success
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("You're All Set!", "greeter completion page title")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("DankMaterialShell is ready to use", "greeter completion page subtitle")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: root.hasKeybinds
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "keyboard"
|
||||
size: root.sectionIconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("DMS Shortcuts", "greeter keybinds section header")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: keybindsRect
|
||||
width: parent.width
|
||||
height: keybindsGrid.height + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
readonly property bool useTwoColumns: width > 500
|
||||
readonly property int columnCount: useTwoColumns ? 2 : 1
|
||||
readonly property real itemWidth: useTwoColumns ? (width - Theme.spacingM * 3) / 2 : width - Theme.spacingM * 2
|
||||
property real maxKeyWidth: 0
|
||||
|
||||
Grid {
|
||||
id: keybindsGrid
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
columns: keybindsRect.columnCount
|
||||
rowSpacing: Theme.spacingS
|
||||
columnSpacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: root.dmsKeybinds
|
||||
|
||||
Row {
|
||||
width: keybindsRect.itemWidth
|
||||
height: root.keybindRowHeight
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: keybindsRect.maxKeyWidth
|
||||
height: parent.height
|
||||
|
||||
Row {
|
||||
id: keysRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property real naturalWidth: {
|
||||
let w = 0;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (children[i].visible)
|
||||
w += children[i].width + (i > 0 ? Theme.spacingXS : 0);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(() => {
|
||||
if (naturalWidth > keybindsRect.maxKeyWidth)
|
||||
keybindsRect.maxKeyWidth = naturalWidth;
|
||||
});
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: (modelData.key || "").split("+")
|
||||
|
||||
Rectangle {
|
||||
width: singleKeyText.implicitWidth + Theme.spacingM
|
||||
height: root.keyBadgeHeight
|
||||
radius: Theme.spacingXS
|
||||
color: Theme.surfaceContainerHighest
|
||||
border.width: 1
|
||||
border.color: Theme.outline
|
||||
|
||||
StyledText {
|
||||
id: singleKeyText
|
||||
anchors.centerIn: parent
|
||||
color: Theme.secondary
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - keybindsRect.maxKeyWidth - Theme.spacingS
|
||||
text: modelData.desc || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: noKeybindsColumn.height + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
visible: !root.hasKeybinds
|
||||
|
||||
Column {
|
||||
id: noKeybindsColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "keyboard"
|
||||
size: root.sectionIconSize
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No DMS shortcuts configured", "greeter no keybinds message")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: Math.round(Theme.fontSizeMedium * 2.85)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHighest
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
opacity: noKeybindsLinkMouse.containsMouse ? 0.12 : 0
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "menu_book"
|
||||
size: root.sectionIconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Configure Keybinds", "greeter configure keybinds link")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "open_in_new"
|
||||
size: Theme.iconSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: noKeybindsLinkMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let url = "https://danklinux.com/docs/dankmaterialshell/keybinds-ipc";
|
||||
if (CompositorService.isNiri)
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings";
|
||||
else if (CompositorService.isHyprland)
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1";
|
||||
else if (CompositorService.isDwl)
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
|
||||
Qt.openUrlExternally(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
opacity: 0.3
|
||||
visible: root.hasKeybinds
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: root.sectionIconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Configure", "greeter settings section header")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
width: parent.width
|
||||
columns: 2
|
||||
rowSpacing: Theme.spacingS
|
||||
columnSpacing: Theme.spacingS
|
||||
|
||||
GreeterSettingsCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "display_settings"
|
||||
title: I18n.tr("Displays", "greeter settings link")
|
||||
description: I18n.tr("Resolution, position, scale", "greeter displays description")
|
||||
onClicked: PopoutService.openSettingsWithTab("display_config")
|
||||
}
|
||||
|
||||
GreeterSettingsCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "wallpaper"
|
||||
title: I18n.tr("Wallpaper", "greeter settings link")
|
||||
description: I18n.tr("Background image", "greeter wallpaper description")
|
||||
onClicked: PopoutService.openSettingsWithTab("wallpaper")
|
||||
}
|
||||
|
||||
GreeterSettingsCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "format_paint"
|
||||
title: I18n.tr("Theme & Colors", "greeter settings link")
|
||||
description: I18n.tr("Dynamic colors, presets", "greeter theme description")
|
||||
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||
}
|
||||
|
||||
GreeterSettingsCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "notifications"
|
||||
title: I18n.tr("Notifications", "greeter settings link")
|
||||
description: I18n.tr("Popup behavior, position", "greeter notifications description")
|
||||
onClicked: PopoutService.openSettingsWithTab("notifications")
|
||||
}
|
||||
|
||||
GreeterSettingsCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "toolbar"
|
||||
title: I18n.tr("DankBar", "greeter settings link")
|
||||
description: I18n.tr("Widgets, layout, style", "greeter dankbar description")
|
||||
onClicked: PopoutService.openSettingsWithTab("dankbar_settings")
|
||||
}
|
||||
|
||||
GreeterSettingsCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "keyboard"
|
||||
title: I18n.tr("Keybinds", "greeter settings link")
|
||||
description: I18n.tr("niri shortcuts config", "greeter keybinds niri description")
|
||||
visible: KeybindsService.available
|
||||
onClicked: PopoutService.openSettingsWithTab("keybinds")
|
||||
}
|
||||
|
||||
GreeterSettingsCard {
|
||||
width: (parent.width - Theme.spacingS) / 2
|
||||
iconName: "dock_to_bottom"
|
||||
title: I18n.tr("Dock", "greeter settings link")
|
||||
description: I18n.tr("Position, pinned apps", "greeter dock description")
|
||||
visible: !KeybindsService.available
|
||||
onClicked: PopoutService.openSettingsWithTab("dock")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "explore"
|
||||
size: root.sectionIconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Explore", "greeter explore section header")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
GreeterQuickLink {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "menu_book"
|
||||
title: I18n.tr("Docs", "greeter documentation link")
|
||||
isExternal: true
|
||||
onClicked: Qt.openUrlExternally("https://danklinux.com/docs")
|
||||
}
|
||||
|
||||
GreeterQuickLink {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "extension"
|
||||
title: I18n.tr("Plugins", "greeter plugins link")
|
||||
isExternal: true
|
||||
onClicked: Qt.openUrlExternally("https://danklinux.com/plugins")
|
||||
}
|
||||
|
||||
GreeterQuickLink {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "palette"
|
||||
title: I18n.tr("Themes", "greeter themes link")
|
||||
isExternal: true
|
||||
onClicked: Qt.openUrlExternally("https://danklinux.com/plugins?tab=themes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
421
quickshell/Modals/Greeter/GreeterDoctorPage.qml
Normal file
421
quickshell/Modals/Greeter/GreeterDoctorPage.qml
Normal file
@@ -0,0 +1,421 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool isRunning: false
|
||||
property bool hasRun: false
|
||||
property var doctorResults: null
|
||||
property int errorCount: 0
|
||||
property int warningCount: 0
|
||||
property int okCount: 0
|
||||
property int infoCount: 0
|
||||
property string selectedFilter: "error"
|
||||
|
||||
readonly property real loadingContainerSize: Math.round(Theme.iconSize * 5)
|
||||
readonly property real pulseRingSize: Math.round(Theme.iconSize * 3.3)
|
||||
readonly property real centerIconContainerSize: Math.round(Theme.iconSize * 2.67)
|
||||
readonly property real headerIconContainerSize: Math.round(Theme.iconSize * 2)
|
||||
|
||||
readonly property var filteredResults: {
|
||||
if (!doctorResults?.results)
|
||||
return [];
|
||||
return doctorResults.results.filter(r => r.status === selectedFilter);
|
||||
}
|
||||
|
||||
function runDoctor() {
|
||||
hasRun = false;
|
||||
isRunning = true;
|
||||
doctorProcess.running = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: runDoctor()
|
||||
|
||||
Item {
|
||||
id: loadingView
|
||||
anchors.fill: parent
|
||||
visible: root.isRunning
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Item {
|
||||
width: root.loadingContainerSize
|
||||
height: root.loadingContainerSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Rectangle {
|
||||
id: pulseRing1
|
||||
anchors.centerIn: parent
|
||||
width: root.pulseRingSize
|
||||
height: root.pulseRingSize
|
||||
radius: root.pulseRingSize / 2
|
||||
color: "transparent"
|
||||
border.width: Math.round(Theme.spacingXS * 0.75)
|
||||
border.color: Theme.primary
|
||||
opacity: 0
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.8
|
||||
to: 0
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.5
|
||||
to: 1.5
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pulseRing2
|
||||
anchors.centerIn: parent
|
||||
width: root.pulseRingSize
|
||||
height: root.pulseRingSize
|
||||
radius: root.pulseRingSize / 2
|
||||
color: "transparent"
|
||||
border.width: Math.round(Theme.spacingXS * 0.75)
|
||||
border.color: Theme.secondary
|
||||
opacity: 0
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.8
|
||||
to: 0
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 0.3
|
||||
to: 1.3
|
||||
duration: 1500
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: root.centerIconContainerSize
|
||||
height: root.centerIconContainerSize
|
||||
radius: root.centerIconContainerSize / 2
|
||||
color: Theme.primaryContainer
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "vital_signs"
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
SequentialAnimation on scale {
|
||||
running: root.isRunning
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation {
|
||||
from: 1
|
||||
to: 1.1
|
||||
duration: 750
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
from: 1.1
|
||||
to: 1
|
||||
duration: 750
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Check", "greeter doctor page title")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Analyzing configuration...", "greeter doctor page loading text")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: resultsView
|
||||
anchors.fill: parent
|
||||
visible: root.hasRun && !root.isRunning
|
||||
opacity: (root.hasRun && !root.isRunning) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: headerSection
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingL
|
||||
anchors.leftMargin: Theme.spacingXL
|
||||
anchors.rightMargin: Theme.spacingXL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: root.headerIconContainerSize
|
||||
height: root.headerIconContainerSize
|
||||
radius: Math.round(root.headerIconContainerSize * 0.29)
|
||||
color: root.errorCount > 0 ? Theme.errorContainer : Theme.primaryContainer
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.errorCount > 0 ? "warning" : "check_circle"
|
||||
size: Theme.iconSize + 4
|
||||
color: root.errorCount > 0 ? Theme.error : Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("System Check", "greeter doctor page title")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.errorCount > 0 ? I18n.tr("%1 issue(s) found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("All checks passed", "greeter doctor page success")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
GreeterStatusCard {
|
||||
width: (parent.width - Theme.spacingS * 3) / 4
|
||||
count: root.errorCount
|
||||
label: I18n.tr("Errors", "greeter doctor page status card")
|
||||
iconName: "error"
|
||||
iconColor: Theme.error
|
||||
bgColor: Theme.errorContainer || Theme.withAlpha(Theme.error, 0.15)
|
||||
selected: root.selectedFilter === "error"
|
||||
onClicked: root.selectedFilter = "error"
|
||||
}
|
||||
|
||||
GreeterStatusCard {
|
||||
width: (parent.width - Theme.spacingS * 3) / 4
|
||||
count: root.warningCount
|
||||
label: I18n.tr("Warnings", "greeter doctor page status card")
|
||||
iconName: "warning"
|
||||
iconColor: Theme.warning
|
||||
bgColor: Theme.withAlpha(Theme.warning, 0.15)
|
||||
selected: root.selectedFilter === "warn"
|
||||
onClicked: root.selectedFilter = "warn"
|
||||
}
|
||||
|
||||
GreeterStatusCard {
|
||||
width: (parent.width - Theme.spacingS * 3) / 4
|
||||
count: root.infoCount
|
||||
label: I18n.tr("Info", "greeter doctor page status card")
|
||||
iconName: "info"
|
||||
iconColor: Theme.secondary
|
||||
bgColor: Theme.withAlpha(Theme.secondary, 0.15)
|
||||
selected: root.selectedFilter === "info"
|
||||
onClicked: root.selectedFilter = "info"
|
||||
}
|
||||
|
||||
GreeterStatusCard {
|
||||
width: (parent.width - Theme.spacingS * 3) / 4
|
||||
count: root.okCount
|
||||
label: I18n.tr("OK", "greeter doctor page status card")
|
||||
iconName: "check_circle"
|
||||
iconColor: Theme.success
|
||||
bgColor: Theme.withAlpha(Theme.success, 0.15)
|
||||
selected: root.selectedFilter === "ok"
|
||||
onClicked: root.selectedFilter = "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
anchors.top: headerSection.bottom
|
||||
anchors.bottom: footerSection.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
anchors.leftMargin: Theme.spacingXL
|
||||
anchors.rightMargin: Theme.spacingXL
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: root.filteredResults.length === 0
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
switch (root.selectedFilter) {
|
||||
case "error":
|
||||
return "check_circle";
|
||||
case "warn":
|
||||
return "thumb_up";
|
||||
case "info":
|
||||
return "info";
|
||||
default:
|
||||
return "verified";
|
||||
}
|
||||
}
|
||||
size: Math.round(Theme.iconSize * 1.67)
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
switch (root.selectedFilter) {
|
||||
case "error":
|
||||
return I18n.tr("No errors", "greeter doctor page empty state");
|
||||
case "warn":
|
||||
return I18n.tr("No warnings", "greeter doctor page empty state");
|
||||
case "info":
|
||||
return I18n.tr("No info items", "greeter doctor page empty state");
|
||||
default:
|
||||
return I18n.tr("No checks passed", "greeter doctor page empty state");
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
clip: true
|
||||
contentHeight: resultsColumn.height
|
||||
contentWidth: width
|
||||
visible: root.filteredResults.length > 0
|
||||
|
||||
Column {
|
||||
id: resultsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.filteredResults
|
||||
|
||||
GreeterDoctorResultItem {
|
||||
width: resultsColumn.width
|
||||
resultData: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: footerSection
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
text: I18n.tr("Run Again", "greeter doctor page button")
|
||||
iconName: "refresh"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
textColor: Theme.surfaceText
|
||||
onClicked: root.runDoctor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: doctorProcess
|
||||
command: ["dms", "doctor", "--json"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.isRunning = false;
|
||||
root.hasRun = true;
|
||||
try {
|
||||
root.doctorResults = JSON.parse(text);
|
||||
if (root.doctorResults?.summary) {
|
||||
root.errorCount = root.doctorResults.summary.errors || 0;
|
||||
root.warningCount = root.doctorResults.summary.warnings || 0;
|
||||
root.okCount = root.doctorResults.summary.ok || 0;
|
||||
root.infoCount = root.doctorResults.summary.info || 0;
|
||||
}
|
||||
if (root.errorCount > 0)
|
||||
root.selectedFilter = "error";
|
||||
else if (root.warningCount > 0)
|
||||
root.selectedFilter = "warn";
|
||||
else if (root.infoCount > 0)
|
||||
root.selectedFilter = "info";
|
||||
else
|
||||
root.selectedFilter = "ok";
|
||||
} catch (e) {
|
||||
console.error("GreeterDoctorPage: Failed to parse doctor output:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.isRunning = false;
|
||||
root.hasRun = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
quickshell/Modals/Greeter/GreeterDoctorResultItem.qml
Normal file
96
quickshell/Modals/Greeter/GreeterDoctorResultItem.qml
Normal file
@@ -0,0 +1,96 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var resultData: null
|
||||
|
||||
readonly property string status: resultData?.status || "ok"
|
||||
readonly property string statusIcon: {
|
||||
switch (status) {
|
||||
case "error":
|
||||
return "error";
|
||||
case "warn":
|
||||
return "warning";
|
||||
case "info":
|
||||
return "info";
|
||||
default:
|
||||
return "check_circle";
|
||||
}
|
||||
}
|
||||
readonly property color statusColor: {
|
||||
switch (status) {
|
||||
case "error":
|
||||
return Theme.error;
|
||||
case "warn":
|
||||
return Theme.warning;
|
||||
case "info":
|
||||
return Theme.secondary;
|
||||
default:
|
||||
return Theme.success;
|
||||
}
|
||||
}
|
||||
|
||||
height: Math.round(Theme.fontSizeMedium * 3.4)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(statusColor, 0.08)
|
||||
|
||||
DankIcon {
|
||||
id: statusIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: root.statusIcon
|
||||
size: Theme.iconSize - 4
|
||||
color: root.statusColor
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: statusIcon.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: categoryChip.visible ? categoryChip.left : parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 1
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.resultData?.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.resultData?.message || ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: categoryChip
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: Math.round(Theme.fontSizeSmall * 1.67)
|
||||
width: categoryText.implicitWidth + Theme.spacingS
|
||||
radius: Theme.spacingXS
|
||||
color: Theme.surfaceContainerHighest
|
||||
visible: !!(root.resultData?.category)
|
||||
|
||||
StyledText {
|
||||
id: categoryText
|
||||
anchors.centerIn: parent
|
||||
text: root.resultData?.category || ""
|
||||
font.pixelSize: Theme.fontSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
57
quickshell/Modals/Greeter/GreeterFeatureCard.qml
Normal file
57
quickshell/Modals/Greeter/GreeterFeatureCard.qml
Normal file
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property string title: ""
|
||||
property string description: ""
|
||||
|
||||
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
|
||||
|
||||
height: Math.round(Theme.fontSizeMedium * 6.4)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: root.iconContainerSize
|
||||
height: root.iconContainerSize
|
||||
radius: Math.round(root.iconContainerSize * 0.28)
|
||||
color: Theme.primaryContainer
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.iconName
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
327
quickshell/Modals/Greeter/GreeterModal.qml
Normal file
327
quickshell/Modals/Greeter/GreeterModal.qml
Normal file
@@ -0,0 +1,327 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
FloatingWindow {
|
||||
id: root
|
||||
|
||||
property int currentPage: 0
|
||||
readonly property int totalPages: 3
|
||||
readonly property var pageComponents: [welcomePage, doctorPage, completePage]
|
||||
|
||||
property var cheatsheetData: ({})
|
||||
property bool cheatsheetLoaded: false
|
||||
|
||||
readonly property int modalWidth: 720
|
||||
readonly property int modalHeight: screen ? Math.min(760, screen.height - 80) : 760
|
||||
|
||||
signal greeterCompleted
|
||||
|
||||
Component.onCompleted: Qt.callLater(loadCheatsheet)
|
||||
|
||||
function loadCheatsheet() {
|
||||
const provider = KeybindsService.cheatsheetProvider;
|
||||
if (KeybindsService.cheatsheetAvailable && provider && !cheatsheetLoaded) {
|
||||
cheatsheetProcess.command = ["dms", "keybinds", "show", provider];
|
||||
cheatsheetProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: KeybindsService
|
||||
function onCheatsheetAvailableChanged() {
|
||||
if (KeybindsService.cheatsheetAvailable && !root.cheatsheetLoaded)
|
||||
loadCheatsheet();
|
||||
}
|
||||
}
|
||||
|
||||
function getKeybind(actionPattern) {
|
||||
if (!cheatsheetLoaded || !cheatsheetData.binds)
|
||||
return "";
|
||||
for (const category in cheatsheetData.binds) {
|
||||
const binds = cheatsheetData.binds[category];
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
const bind = binds[i];
|
||||
if (bind.action && bind.action.includes(actionPattern))
|
||||
return bind.key || "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function show() {
|
||||
currentPage = 0;
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (currentPage < totalPages - 1)
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (currentPage > 0)
|
||||
currentPage--;
|
||||
}
|
||||
|
||||
function finish() {
|
||||
FirstLaunchService.markFirstLaunchComplete();
|
||||
greeterCompleted();
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function skip() {
|
||||
FirstLaunchService.markFirstLaunchComplete();
|
||||
greeterCompleted();
|
||||
visible = false;
|
||||
}
|
||||
|
||||
objectName: "greeterModal"
|
||||
title: I18n.tr("Welcome", "greeter modal window title")
|
||||
minimumSize: Qt.size(modalWidth, modalHeight)
|
||||
maximumSize: Qt.size(modalWidth, modalHeight)
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
Process {
|
||||
id: cheatsheetProcess
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length === 0)
|
||||
return;
|
||||
try {
|
||||
root.cheatsheetData = JSON.parse(trimmed);
|
||||
root.cheatsheetLoaded = true;
|
||||
} catch (e) {
|
||||
console.warn("Greeter: Failed to parse cheatsheet:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: contentFocusScope
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.skip();
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
if (root.currentPage < root.totalPages - 1)
|
||||
root.nextPage();
|
||||
else
|
||||
root.finish();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Left:
|
||||
if (root.currentPage > 0)
|
||||
root.prevPage();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Right:
|
||||
if (root.currentPage < root.totalPages - 1)
|
||||
root.nextPage();
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: headerRow.height + Theme.spacingM
|
||||
onPressed: windowControls.tryStartMove()
|
||||
onDoubleClicked: windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: headerRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
height: Math.round(Theme.fontSizeMedium * 2.85)
|
||||
|
||||
Rectangle {
|
||||
id: pageIndicatorContainer
|
||||
readonly property real indicatorHeight: Math.round(Theme.fontSizeMedium * 2)
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: pageIndicatorRow.width + Theme.spacingM * 2
|
||||
height: indicatorHeight
|
||||
radius: indicatorHeight / 2
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Row {
|
||||
id: pageIndicatorRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.totalPages
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
property bool isActive: index === root.currentPage
|
||||
readonly property real dotSize: Math.round(Theme.spacingS * 1.3)
|
||||
|
||||
width: isActive ? dotSize * 3 : dotSize
|
||||
height: dotSize
|
||||
radius: dotSize / 2
|
||||
color: isActive ? Theme.primary : Theme.surfaceTextAlpha
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls.supported
|
||||
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.skip()
|
||||
|
||||
DankTooltip {
|
||||
text: I18n.tr("Skip setup", "greeter skip button tooltip")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.bottom: footerRow.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
|
||||
Loader {
|
||||
id: pageLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.pageComponents[root.currentPage]
|
||||
|
||||
property var greeterRoot: root
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: footerRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: Math.round(Theme.fontSizeMedium * 4.5)
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
visible: root.currentPage < root.totalPages - 1
|
||||
text: I18n.tr("Skip", "greeter skip button")
|
||||
backgroundColor: "transparent"
|
||||
textColor: Theme.surfaceVariantText
|
||||
onClicked: root.skip()
|
||||
}
|
||||
|
||||
DankButton {
|
||||
visible: root.currentPage > 0
|
||||
text: I18n.tr("Back", "greeter back button")
|
||||
iconName: "arrow_back"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
textColor: Theme.surfaceText
|
||||
onClicked: root.prevPage()
|
||||
}
|
||||
|
||||
DankButton {
|
||||
visible: root.currentPage < root.totalPages - 1
|
||||
enabled: !(root.currentPage === 1 && pageLoader.item && pageLoader.item.isRunning)
|
||||
text: root.currentPage === 0 ? I18n.tr("Get Started", "greeter first page button") : I18n.tr("Next", "greeter next button")
|
||||
iconName: "arrow_forward"
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.primaryText
|
||||
onClicked: root.nextPage()
|
||||
}
|
||||
|
||||
DankButton {
|
||||
visible: root.currentPage === root.totalPages - 1
|
||||
text: I18n.tr("Finish", "greeter finish button")
|
||||
iconName: "check"
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.primaryText
|
||||
onClicked: root.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingWindowControls {
|
||||
id: windowControls
|
||||
targetWindow: root
|
||||
}
|
||||
|
||||
Component {
|
||||
id: welcomePage
|
||||
GreeterWelcomePage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: doctorPage
|
||||
GreeterDoctorPage {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: completePage
|
||||
GreeterCompletePage {}
|
||||
}
|
||||
}
|
||||
60
quickshell/Modals/Greeter/GreeterQuickLink.qml
Normal file
60
quickshell/Modals/Greeter/GreeterQuickLink.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property string title: ""
|
||||
property bool isExternal: false
|
||||
|
||||
signal clicked
|
||||
|
||||
height: Math.round(Theme.fontSizeMedium * 3.1)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
opacity: mouseArea.containsMouse ? 0.12 : 0
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSizeSmall + 2
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
visible: root.isExternal
|
||||
name: "open_in_new"
|
||||
size: Theme.iconSizeSmall - 2
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
78
quickshell/Modals/Greeter/GreeterSettingsCard.qml
Normal file
78
quickshell/Modals/Greeter/GreeterSettingsCard.qml
Normal file
@@ -0,0 +1,78 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property string title: ""
|
||||
property string description: ""
|
||||
|
||||
signal clicked
|
||||
|
||||
readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
|
||||
|
||||
height: Math.round(Theme.fontSizeMedium * 4.5)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
opacity: mouseArea.containsMouse ? 0.12 : 0
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: root.iconContainerSize
|
||||
height: root.iconContainerSize
|
||||
radius: Math.round(root.iconContainerSize * 0.28)
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.iconName
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.primaryText
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
width: parent.width - root.iconContainerSize - Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
75
quickshell/Modals/Greeter/GreeterStatusCard.qml
Normal file
75
quickshell/Modals/Greeter/GreeterStatusCard.qml
Normal file
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int count: 0
|
||||
property string label: ""
|
||||
property string iconName: ""
|
||||
property color iconColor: Theme.surfaceText
|
||||
property color bgColor: Theme.surfaceContainerHigh
|
||||
property bool selected: false
|
||||
|
||||
signal clicked
|
||||
|
||||
height: Math.round(Theme.fontSizeMedium * 5)
|
||||
radius: Theme.cornerRadius
|
||||
color: bgColor
|
||||
border.width: selected ? 2 : 0
|
||||
border.color: selected ? iconColor : "transparent"
|
||||
scale: mouseArea.pressed ? 0.97 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: Theme.iconSize - 4
|
||||
color: root.iconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.count.toString()
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.weight: Font.Bold
|
||||
color: root.iconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
150
quickshell/Modals/Greeter/GreeterWelcomePage.qml
Normal file
150
quickshell/Modals/Greeter/GreeterWelcomePage.qml
Normal file
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property real logoSize: Math.round(Theme.iconSize * 5.3)
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(Math.round(Theme.fontSizeMedium * 43), parent.width - Theme.spacingXL * 2)
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Image {
|
||||
width: root.logoSize
|
||||
height: width * (569.94629 / 506.50931)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg"
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
saturation: 0
|
||||
colorization: 1
|
||||
colorizationColor: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Welcome to DankMaterialShell", "greeter welcome page title")
|
||||
font.pixelSize: Theme.fontSizeXLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("A modern desktop shell for Wayland compositors", "greeter welcome page tagline")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outlineMedium
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Features", "greeter welcome page section header")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Grid {
|
||||
width: parent.width
|
||||
columns: 3
|
||||
rowSpacing: Theme.spacingS
|
||||
columnSpacing: Theme.spacingS
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "auto_awesome"
|
||||
title: I18n.tr("Dynamic Theming", "greeter feature card title")
|
||||
description: I18n.tr("Colors from wallpaper", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "format_paint"
|
||||
title: I18n.tr("App Theming", "greeter feature card title")
|
||||
description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "download"
|
||||
title: I18n.tr("Theme Registry", "greeter feature card title")
|
||||
description: I18n.tr("Community themes", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "view_carousel"
|
||||
title: I18n.tr("DankBar", "greeter feature card title")
|
||||
description: I18n.tr("Modular widget bar", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "extension"
|
||||
title: I18n.tr("Plugins", "greeter feature card title")
|
||||
description: I18n.tr("Extensible architecture", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "layers"
|
||||
title: I18n.tr("Multi-Monitor", "greeter feature card title")
|
||||
description: I18n.tr("Per-screen config", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "nightlight"
|
||||
title: I18n.tr("Display Control", "greeter feature card title")
|
||||
description: I18n.tr("Night mode & gamma", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "tune"
|
||||
title: I18n.tr("Control Center", "greeter feature card title")
|
||||
description: I18n.tr("Quick system toggles", "greeter feature card description")
|
||||
}
|
||||
|
||||
GreeterFeatureCard {
|
||||
width: (parent.width - Theme.spacingS * 2) / 3
|
||||
iconName: "density_small"
|
||||
title: I18n.tr("System Tray", "greeter feature card title")
|
||||
description: I18n.tr("Background app icons", "greeter feature card description")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,6 +467,7 @@ Item {
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
opacity: parentModal?.isClosing ? 0 : 1
|
||||
|
||||
SpotlightResults {
|
||||
id: resultsView
|
||||
|
||||
@@ -3,7 +3,6 @@ import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
|
||||
DankModal {
|
||||
id: spotlightModal
|
||||
@@ -18,62 +17,69 @@ DankModal {
|
||||
property bool spotlightOpen: false
|
||||
property alias spotlightContent: spotlightContentInstance
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
|
||||
function resetContent() {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = "";
|
||||
spotlightContent.appLauncher.selectedIndex = 0;
|
||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"));
|
||||
}
|
||||
if (spotlightContent.fileSearchController)
|
||||
spotlightContent.fileSearchController.reset();
|
||||
if (spotlightContent.resetScroll)
|
||||
spotlightContent.resetScroll();
|
||||
if (spotlightContent.searchField)
|
||||
spotlightContent.searchField.text = "";
|
||||
spotlightContent.searchMode = "apps";
|
||||
}
|
||||
|
||||
function show() {
|
||||
openedFromOverview = false;
|
||||
isClosing = false;
|
||||
resetContent();
|
||||
spotlightOpen = true;
|
||||
if (spotlightContent?.appLauncher)
|
||||
spotlightContent.appLauncher.ensureInitialized();
|
||||
open();
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
if (spotlightContent?.searchField)
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = query;
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
}
|
||||
}
|
||||
|
||||
openedFromOverview = false;
|
||||
isClosing = false;
|
||||
resetContent();
|
||||
spotlightOpen = true;
|
||||
if (spotlightContent?.appLauncher) {
|
||||
spotlightContent.appLauncher.ensureInitialized();
|
||||
spotlightContent.appLauncher.searchQuery = query;
|
||||
}
|
||||
if (spotlightContent?.searchField)
|
||||
spotlightContent.searchField.text = query;
|
||||
open();
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
if (spotlightContent?.searchField)
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
spotlightOpen = false;
|
||||
close();
|
||||
}
|
||||
|
||||
onDialogClosed: {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = "";
|
||||
spotlightContent.appLauncher.selectedIndex = 0;
|
||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"));
|
||||
}
|
||||
if (spotlightContent.fileSearchController) {
|
||||
spotlightContent.fileSearchController.reset();
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = "";
|
||||
}
|
||||
}
|
||||
isClosing = false;
|
||||
resetContent();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
|
||||
@@ -52,6 +52,7 @@ DankPopout {
|
||||
|
||||
onOpened: {
|
||||
searchMode = "apps";
|
||||
appLauncher.ensureInitialized();
|
||||
appLauncher.searchQuery = "";
|
||||
appLauncher.selectedIndex = 0;
|
||||
appLauncher.setCategory(I18n.tr("All"));
|
||||
@@ -344,7 +345,7 @@ DankPopout {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 40
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: searchField.text.length === 0 && appDrawerPopout.searchMode === "apps"
|
||||
visible: appDrawerPopout.searchMode === "apps"
|
||||
|
||||
Rectangle {
|
||||
width: 180
|
||||
@@ -404,7 +405,7 @@ DankPopout {
|
||||
height: {
|
||||
let usedHeight = 40 + Theme.spacingS;
|
||||
usedHeight += 52 + Theme.spacingS;
|
||||
usedHeight += (searchField.text.length === 0 && appDrawerPopout.searchMode === "apps" ? 40 : 0);
|
||||
usedHeight += appDrawerPopout.searchMode === "apps" ? 40 : 0;
|
||||
return parent.height - usedHeight;
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -22,16 +20,12 @@ Item {
|
||||
property int debounceInterval: 50
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool suppressUpdatesWhileLaunching: false
|
||||
property var categories: {
|
||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||
const result = [I18n.tr("All")]
|
||||
return result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
|
||||
}
|
||||
property var categories: []
|
||||
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
|
||||
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
|
||||
property alias model: filteredModel
|
||||
property var _watchApplications: AppSearchService.applications
|
||||
property var _uniqueApps: []
|
||||
property bool _initialized: false
|
||||
property bool _isTriggered: false
|
||||
property string _triggeredCategory: ""
|
||||
property bool _updatingFromTrigger: false
|
||||
@@ -40,98 +34,110 @@ Item {
|
||||
signal categorySelected(string category)
|
||||
signal viewModeSelected(string mode)
|
||||
|
||||
function ensureInitialized() {
|
||||
if (_initialized)
|
||||
return;
|
||||
_initialized = true;
|
||||
updateCategories();
|
||||
updateFilteredModel();
|
||||
}
|
||||
|
||||
function updateCategories() {
|
||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
|
||||
const result = [I18n.tr("All")]
|
||||
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
|
||||
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science");
|
||||
const result = [I18n.tr("All")];
|
||||
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")));
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded() { updateCategories() }
|
||||
function onPluginUnloaded() { updateCategories() }
|
||||
function onPluginListUpdated() { updateCategories() }
|
||||
function onPluginLoaded() {
|
||||
updateCategories();
|
||||
}
|
||||
function onPluginUnloaded() {
|
||||
updateCategories();
|
||||
}
|
||||
function onPluginListUpdated() {
|
||||
updateCategories();
|
||||
}
|
||||
function onRequestLauncherUpdate(pluginId) {
|
||||
// Only update if we are actually looking at this plugin or in All category
|
||||
updateFilteredModel()
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onSortAppsAlphabeticallyChanged() {
|
||||
updateFilteredModel()
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateFilteredModel() {
|
||||
if (suppressUpdatesWhileLaunching) {
|
||||
suppressUpdatesWhileLaunching = false
|
||||
return
|
||||
suppressUpdatesWhileLaunching = false;
|
||||
return;
|
||||
}
|
||||
filteredModel.clear()
|
||||
selectedIndex = 0
|
||||
keyboardNavigationActive = false
|
||||
filteredModel.clear();
|
||||
selectedIndex = 0;
|
||||
keyboardNavigationActive = false;
|
||||
|
||||
const triggerResult = checkPluginTriggers(searchQuery)
|
||||
const triggerResult = checkPluginTriggers(searchQuery);
|
||||
if (triggerResult.triggered) {
|
||||
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId)
|
||||
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId);
|
||||
}
|
||||
|
||||
let apps = []
|
||||
const allCategory = I18n.tr("All")
|
||||
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : []
|
||||
let apps = [];
|
||||
const allCategory = I18n.tr("All");
|
||||
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : [];
|
||||
|
||||
if (triggerResult.triggered) {
|
||||
_isTriggered = true
|
||||
_triggeredCategory = triggerResult.pluginCategory
|
||||
_updatingFromTrigger = true
|
||||
selectedCategory = triggerResult.pluginCategory
|
||||
_updatingFromTrigger = false
|
||||
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query)
|
||||
_isTriggered = true;
|
||||
_triggeredCategory = triggerResult.pluginCategory;
|
||||
_updatingFromTrigger = true;
|
||||
selectedCategory = triggerResult.pluginCategory;
|
||||
_updatingFromTrigger = false;
|
||||
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query);
|
||||
} else {
|
||||
if (_isTriggered) {
|
||||
_updatingFromTrigger = true
|
||||
selectedCategory = allCategory
|
||||
_updatingFromTrigger = false
|
||||
_isTriggered = false
|
||||
_triggeredCategory = ""
|
||||
_updatingFromTrigger = true;
|
||||
selectedCategory = allCategory;
|
||||
_updatingFromTrigger = false;
|
||||
_isTriggered = false;
|
||||
_triggeredCategory = "";
|
||||
}
|
||||
if (searchQuery.length === 0) {
|
||||
if (selectedCategory === allCategory) {
|
||||
let emptyTriggerItems = []
|
||||
let emptyTriggerItems = [];
|
||||
emptyTriggerPlugins.forEach(pluginId => {
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||
const pluginCategory = plugin.name || pluginId
|
||||
const items = AppSearchService.getPluginItems(pluginCategory, "")
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items)
|
||||
})
|
||||
apps = AppSearchService.applications.concat(emptyTriggerItems)
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId);
|
||||
const pluginCategory = plugin.name || pluginId;
|
||||
const items = AppSearchService.getPluginItems(pluginCategory, "");
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
||||
});
|
||||
apps = AppSearchService.applications.concat(emptyTriggerItems);
|
||||
} else {
|
||||
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
|
||||
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults);
|
||||
}
|
||||
} else {
|
||||
if (selectedCategory === allCategory) {
|
||||
apps = AppSearchService.searchApplications(searchQuery)
|
||||
apps = AppSearchService.searchApplications(searchQuery);
|
||||
|
||||
let emptyTriggerItems = []
|
||||
let emptyTriggerItems = [];
|
||||
emptyTriggerPlugins.forEach(pluginId => {
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||
const pluginCategory = plugin.name || pluginId
|
||||
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery)
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items)
|
||||
})
|
||||
apps = apps.concat(emptyTriggerItems)
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId);
|
||||
const pluginCategory = plugin.name || pluginId;
|
||||
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery);
|
||||
emptyTriggerItems = emptyTriggerItems.concat(items);
|
||||
});
|
||||
apps = apps.concat(emptyTriggerItems);
|
||||
} else {
|
||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
|
||||
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
|
||||
if (categoryApps.length > 0) {
|
||||
const allSearchResults = AppSearchService.searchApplications(searchQuery)
|
||||
const categoryNames = new Set(categoryApps.map(app => app.name))
|
||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
|
||||
const allSearchResults = AppSearchService.searchApplications(searchQuery);
|
||||
const categoryNames = new Set(categoryApps.map(app => app.name));
|
||||
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults);
|
||||
} else {
|
||||
apps = []
|
||||
apps = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,138 +146,150 @@ Item {
|
||||
if (searchQuery.length === 0) {
|
||||
if (SettingsData.sortAppsAlphabetically) {
|
||||
apps = apps.sort((a, b) => {
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
return (a.name || "").localeCompare(b.name || "");
|
||||
});
|
||||
} else {
|
||||
apps = apps.sort((a, b) => {
|
||||
const aId = a.id || a.execString || a.exec || ""
|
||||
const bId = b.id || b.execString || b.exec || ""
|
||||
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
|
||||
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
|
||||
if (aUsage !== bUsage) {
|
||||
return bUsage - aUsage
|
||||
}
|
||||
return (a.name || "").localeCompare(b.name || "")
|
||||
})
|
||||
const aId = a.id || a.execString || a.exec || "";
|
||||
const bId = b.id || b.execString || b.exec || "";
|
||||
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0;
|
||||
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0;
|
||||
if (aUsage !== bUsage) {
|
||||
return bUsage - aUsage;
|
||||
}
|
||||
return (a.name || "").localeCompare(b.name || "");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const seenNames = new Set()
|
||||
const uniqueApps = []
|
||||
const seenNames = new Set();
|
||||
const uniqueApps = [];
|
||||
apps.forEach(app => {
|
||||
if (app) {
|
||||
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "")
|
||||
if (seenNames.has(itemKey)) {
|
||||
return
|
||||
}
|
||||
seenNames.add(itemKey)
|
||||
uniqueApps.push(app)
|
||||
if (app) {
|
||||
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "");
|
||||
if (seenNames.has(itemKey)) {
|
||||
return;
|
||||
}
|
||||
seenNames.add(itemKey);
|
||||
uniqueApps.push(app);
|
||||
|
||||
const isPluginItem = app.action !== undefined
|
||||
filteredModel.append({
|
||||
"name": app.name || "",
|
||||
"exec": app.execString || app.exec || app.action || "",
|
||||
"icon": app.icon !== undefined ? app.icon : (isPluginItem ? "" : "application-x-executable"),
|
||||
"comment": app.comment || "",
|
||||
"categories": app.categories || [],
|
||||
"isPlugin": isPluginItem,
|
||||
"appIndex": uniqueApps.length - 1
|
||||
})
|
||||
}
|
||||
})
|
||||
const isPluginItem = app.action !== undefined;
|
||||
filteredModel.append({
|
||||
"name": app.name || "",
|
||||
"exec": app.execString || app.exec || app.action || "",
|
||||
"icon": app.icon !== undefined ? app.icon : (isPluginItem ? "" : "application-x-executable"),
|
||||
"comment": app.comment || "",
|
||||
"categories": app.categories || [],
|
||||
"isPlugin": isPluginItem,
|
||||
"appIndex": uniqueApps.length - 1
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
root._uniqueApps = uniqueApps
|
||||
root._uniqueApps = uniqueApps;
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredModel.count === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||
keyboardNavigationActive = true;
|
||||
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1);
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredModel.count === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
|
||||
keyboardNavigationActive = true;
|
||||
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0);
|
||||
}
|
||||
|
||||
function selectNextInRow() {
|
||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||
return
|
||||
return;
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
|
||||
keyboardNavigationActive = true;
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1);
|
||||
}
|
||||
|
||||
function selectPreviousInRow() {
|
||||
if (filteredModel.count === 0 || viewMode !== "grid") {
|
||||
return
|
||||
return;
|
||||
}
|
||||
keyboardNavigationActive = true
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0)
|
||||
keyboardNavigationActive = true;
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||
}
|
||||
|
||||
function launchSelected() {
|
||||
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const selectedApp = filteredModel.get(selectedIndex)
|
||||
launchApp(selectedApp)
|
||||
const selectedApp = filteredModel.get(selectedIndex);
|
||||
launchApp(selectedApp);
|
||||
}
|
||||
|
||||
function launchApp(appData) {
|
||||
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
suppressUpdatesWhileLaunching = true
|
||||
suppressUpdatesWhileLaunching = true;
|
||||
|
||||
const actualApp = _uniqueApps[appData.appIndex]
|
||||
const actualApp = _uniqueApps[appData.appIndex];
|
||||
|
||||
if (appData.isPlugin) {
|
||||
const pluginId = getPluginIdForItem(actualApp)
|
||||
const pluginId = getPluginIdForItem(actualApp);
|
||||
if (pluginId) {
|
||||
AppSearchService.executePluginItem(actualApp, pluginId)
|
||||
appLaunched(appData)
|
||||
return
|
||||
AppSearchService.executePluginItem(actualApp, pluginId);
|
||||
appLaunched(appData);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
SessionService.launchDesktopEntry(actualApp)
|
||||
appLaunched(appData)
|
||||
AppUsageHistoryData.addAppUsage(actualApp)
|
||||
SessionService.launchDesktopEntry(actualApp);
|
||||
appLaunched(appData);
|
||||
AppUsageHistoryData.addAppUsage(actualApp);
|
||||
}
|
||||
}
|
||||
|
||||
function setCategory(category) {
|
||||
selectedCategory = category
|
||||
categorySelected(category)
|
||||
selectedCategory = category;
|
||||
categorySelected(category);
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
viewMode = mode
|
||||
viewModeSelected(mode)
|
||||
viewMode = mode;
|
||||
viewModeSelected(mode);
|
||||
}
|
||||
|
||||
onSearchQueryChanged: {
|
||||
if (!_initialized)
|
||||
return;
|
||||
if (debounceSearch) {
|
||||
searchDebounceTimer.restart()
|
||||
searchDebounceTimer.restart();
|
||||
} else {
|
||||
updateFilteredModel()
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
onSelectedCategoryChanged: {
|
||||
if (_updatingFromTrigger) {
|
||||
return
|
||||
}
|
||||
updateFilteredModel()
|
||||
if (_updatingFromTrigger || !_initialized)
|
||||
return;
|
||||
updateFilteredModel();
|
||||
}
|
||||
onAppUsageRankingChanged: updateFilteredModel()
|
||||
on_WatchApplicationsChanged: updateFilteredModel()
|
||||
Component.onCompleted: {
|
||||
updateFilteredModel()
|
||||
|
||||
onAppUsageRankingChanged: {
|
||||
if (_initialized)
|
||||
updateFilteredModel();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
if (!root._initialized)
|
||||
return;
|
||||
root.updateCategories();
|
||||
root.updateFilteredModel();
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
@@ -289,59 +307,67 @@ Item {
|
||||
// Plugin trigger system functions
|
||||
function checkPluginTriggers(query) {
|
||||
if (!query || typeof PluginService === "undefined") {
|
||||
return { triggered: false, pluginCategory: "", query: "" }
|
||||
return {
|
||||
triggered: false,
|
||||
pluginCategory: "",
|
||||
query: ""
|
||||
};
|
||||
}
|
||||
|
||||
const triggers = PluginService.getAllPluginTriggers()
|
||||
const triggers = PluginService.getAllPluginTriggers();
|
||||
|
||||
for (const trigger in triggers) {
|
||||
if (query.startsWith(trigger)) {
|
||||
const pluginId = triggers[trigger]
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId)
|
||||
const pluginId = triggers[trigger];
|
||||
const plugin = PluginService.getLauncherPlugin(pluginId);
|
||||
|
||||
if (plugin) {
|
||||
const remainingQuery = query.substring(trigger.length).trim()
|
||||
const remainingQuery = query.substring(trigger.length).trim();
|
||||
const result = {
|
||||
triggered: true,
|
||||
pluginId: pluginId,
|
||||
pluginCategory: plugin.name || pluginId,
|
||||
query: remainingQuery,
|
||||
trigger: trigger
|
||||
}
|
||||
return result
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { triggered: false, pluginCategory: "", query: "" }
|
||||
return {
|
||||
triggered: false,
|
||||
pluginCategory: "",
|
||||
query: ""
|
||||
};
|
||||
}
|
||||
|
||||
function getPluginIdForItem(item) {
|
||||
if (!item || !item.categories || typeof PluginService === "undefined") {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins()
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId]
|
||||
const pluginCategory = plugin.name || pluginId
|
||||
const plugin = launchers[pluginId];
|
||||
const pluginCategory = plugin.name || pluginId;
|
||||
|
||||
let hasCategory = false
|
||||
let hasCategory = false;
|
||||
if (Array.isArray(item.categories)) {
|
||||
hasCategory = item.categories.includes(pluginCategory)
|
||||
hasCategory = item.categories.includes(pluginCategory);
|
||||
} else if (item.categories && typeof item.categories.count !== "undefined") {
|
||||
for (let i = 0; i < item.categories.count; i++) {
|
||||
if (item.categories.get(i) === pluginCategory) {
|
||||
hasCategory = true
|
||||
break
|
||||
hasCategory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCategory) {
|
||||
return pluginId
|
||||
return pluginId;
|
||||
}
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -56,6 +57,7 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -69,6 +71,7 @@ BasePill {
|
||||
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -77,6 +80,7 @@ BasePill {
|
||||
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -91,6 +95,7 @@ BasePill {
|
||||
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -99,6 +104,7 @@ BasePill {
|
||||
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.widgetTextColor
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -133,6 +139,7 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.primary
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -147,6 +154,7 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.primary
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -167,6 +175,7 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.primary
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
@@ -181,6 +190,7 @@ BasePill {
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale)
|
||||
color: Theme.primary
|
||||
width: Math.round(font.pixelSize * 0.6)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignBottom
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
@@ -15,7 +14,7 @@ Singleton {
|
||||
return greetCfgDir + "/settings.json";
|
||||
}
|
||||
|
||||
property string currentThemeName: "blue"
|
||||
property string currentThemeName: "purple"
|
||||
property bool settingsLoaded: false
|
||||
property string customThemeFile: ""
|
||||
property string matugenScheme: "scheme-tonal-spot"
|
||||
@@ -48,7 +47,7 @@ Singleton {
|
||||
try {
|
||||
if (content && content.trim()) {
|
||||
const settings = JSON.parse(content);
|
||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue";
|
||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||
|
||||
@@ -24,6 +24,7 @@ PanelWindow {
|
||||
readonly property string clearText: I18n.tr("Dismiss")
|
||||
|
||||
signal entered
|
||||
signal exitStarted
|
||||
signal exitFinished
|
||||
|
||||
function startExit() {
|
||||
@@ -31,6 +32,7 @@ PanelWindow {
|
||||
return;
|
||||
}
|
||||
exiting = true;
|
||||
exitStarted();
|
||||
exitAnim.restart();
|
||||
exitWatchdog.restart();
|
||||
if (NotificationService.removeFromVisibleNotifications)
|
||||
@@ -61,7 +63,7 @@ PanelWindow {
|
||||
win.exitFinished();
|
||||
}
|
||||
|
||||
visible: hasValidData
|
||||
visible: !_finalized
|
||||
WlrLayershell.layer: {
|
||||
const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER");
|
||||
if (envLayer) {
|
||||
@@ -211,7 +213,7 @@ PanelWindow {
|
||||
y: Theme.snap((win.height - alignedHeight) / 2, dpr)
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
visible: win.hasValidData
|
||||
visible: !win._finalized
|
||||
|
||||
property real swipeOffset: 0
|
||||
readonly property real dismissThreshold: isTopCenter ? height * 0.4 : width * 0.35
|
||||
|
||||
@@ -20,6 +20,7 @@ QtObject {
|
||||
popupComponent: Component {
|
||||
NotificationPopup {
|
||||
onEntered: manager._onPopupEntered(this)
|
||||
onExitStarted: manager._onPopupExitStarted(this)
|
||||
onExitFinished: manager._onPopupExitFinished(this)
|
||||
}
|
||||
}
|
||||
@@ -276,6 +277,14 @@ QtObject {
|
||||
function _onPopupEntered(p) {
|
||||
}
|
||||
|
||||
function _onPopupExitStarted(p) {
|
||||
if (!p)
|
||||
return;
|
||||
const survivors = _active().sort((a, b) => a.screenY - b.screenY);
|
||||
for (let k = 0; k < survivors.length; ++k)
|
||||
survivors[k].screenY = topMargin + k * baseNotificationHeight;
|
||||
}
|
||||
|
||||
function _onPopupExitFinished(p) {
|
||||
if (!p) {
|
||||
return;
|
||||
|
||||
@@ -125,7 +125,12 @@ Scope {
|
||||
}
|
||||
|
||||
onShouldShowSpotlightChanged: {
|
||||
if (shouldShowSpotlight || !isActiveScreen)
|
||||
if (shouldShowSpotlight) {
|
||||
if (spotlightContent?.appLauncher)
|
||||
spotlightContent.appLauncher.ensureInitialized();
|
||||
return;
|
||||
}
|
||||
if (!isActiveScreen)
|
||||
return;
|
||||
Qt.callLater(() => keyboardFocusScope.forceActiveFocus());
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import qs.Common
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay)
|
||||
property var applications: []
|
||||
property var _cachedCategories: null
|
||||
|
||||
readonly property int maxResults: 10
|
||||
readonly property int frecencySampleSize: 10
|
||||
@@ -36,6 +37,20 @@ Singleton {
|
||||
}
|
||||
]
|
||||
|
||||
function refreshApplications() {
|
||||
applications = DesktopEntries.applications.values;
|
||||
_cachedCategories = null;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
root.refreshApplications();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshApplications()
|
||||
|
||||
function tokenize(text) {
|
||||
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(w => w.length > 0);
|
||||
}
|
||||
@@ -316,19 +331,20 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAllCategories() {
|
||||
const categories = new Set([I18n.tr("All")]);
|
||||
if (_cachedCategories)
|
||||
return _cachedCategories;
|
||||
|
||||
const categories = new Set([I18n.tr("All")]);
|
||||
for (const app of applications) {
|
||||
const appCategories = getCategoriesForApp(app);
|
||||
appCategories.forEach(cat => categories.add(cat));
|
||||
}
|
||||
|
||||
// Add plugin categories
|
||||
const pluginCategories = getPluginCategories();
|
||||
pluginCategories.forEach(cat => categories.add(cat));
|
||||
|
||||
const result = Array.from(categories).sort();
|
||||
return result;
|
||||
_cachedCategories = Array.from(categories).sort();
|
||||
return _cachedCategories;
|
||||
}
|
||||
|
||||
function getAppsInCategory(category) {
|
||||
|
||||
94
quickshell/Services/FirstLaunchService.qml
Normal file
94
quickshell/Services/FirstLaunchService.qml
Normal file
@@ -0,0 +1,94 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
|
||||
readonly property string settingsPath: configDir + "/settings.json"
|
||||
readonly property string firstLaunchMarkerPath: configDir + "/.firstlaunch"
|
||||
|
||||
property bool isFirstLaunch: false
|
||||
property bool checkComplete: false
|
||||
property bool greeterDismissed: false
|
||||
|
||||
readonly property bool shouldShowGreeter: checkComplete && isFirstLaunch && !greeterDismissed
|
||||
|
||||
signal greeterRequested
|
||||
signal greeterCompleted
|
||||
|
||||
Component.onCompleted: {
|
||||
checkFirstLaunch();
|
||||
}
|
||||
|
||||
function checkFirstLaunch() {
|
||||
firstLaunchCheckProcess.running = true;
|
||||
}
|
||||
|
||||
function markFirstLaunchComplete() {
|
||||
greeterDismissed = true;
|
||||
touchMarkerProcess.running = true;
|
||||
greeterCompleted();
|
||||
}
|
||||
|
||||
function dismissGreeter() {
|
||||
greeterDismissed = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: firstLaunchCheckProcess
|
||||
|
||||
command: ["sh", "-c", `
|
||||
SETTINGS='` + settingsPath + `'
|
||||
MARKER='` + firstLaunchMarkerPath + `'
|
||||
if [ -f "$MARKER" ]; then
|
||||
echo 'skip'
|
||||
elif [ -f "$SETTINGS" ]; then
|
||||
echo 'existing_user'
|
||||
else
|
||||
echo 'first'
|
||||
fi
|
||||
`]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const result = data.trim();
|
||||
root.checkComplete = true;
|
||||
|
||||
if (result === "first") {
|
||||
root.isFirstLaunch = true;
|
||||
console.info("FirstLaunchService: First launch detected, greeter will be shown");
|
||||
root.greeterRequested();
|
||||
} else if (result === "existing_user") {
|
||||
root.isFirstLaunch = false;
|
||||
console.info("FirstLaunchService: Existing user detected, silently creating marker");
|
||||
touchMarkerProcess.running = true;
|
||||
} else {
|
||||
root.isFirstLaunch = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: touchMarkerProcess
|
||||
|
||||
command: ["sh", "-c", "mkdir -p '" + configDir + "' && touch '" + firstLaunchMarkerPath + "'"]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
console.info("FirstLaunchService: First launch marker created");
|
||||
} else {
|
||||
console.warn("FirstLaunchService: Failed to create first launch marker");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
quickshell/matugen/dms-theme.vsix
Normal file
BIN
quickshell/matugen/dms-theme.vsix
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
# Dynamic Base16 DankShell Theme
|
||||
|
||||
A VSCode theme for [DankMaterialShell](https://github.com/EverydayCodeAlchemy/DankMaterialShellGit).
|
||||
A VSCode theme for [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell).
|
||||
|
||||
## How It Works
|
||||
|
||||
|
||||
BIN
quickshell/matugen/vsix-build/danklogo.png
Normal file
BIN
quickshell/matugen/vsix-build/danklogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -1,86 +0,0 @@
|
||||
const vscode = require("vscode");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
let watcher = null;
|
||||
let reloadTimeout = null;
|
||||
|
||||
function activate(context) {
|
||||
const themesDir = path.join(context.extensionPath, "themes");
|
||||
|
||||
try {
|
||||
watcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(themesDir, "*.json")
|
||||
);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!watcher) return;
|
||||
|
||||
const reloadTheme = () => {
|
||||
if (reloadTimeout) clearTimeout(reloadTimeout);
|
||||
|
||||
reloadTimeout = setTimeout(async () => {
|
||||
const config = vscode.workspace.getConfiguration("workbench");
|
||||
const currentTheme = config.get("colorTheme");
|
||||
|
||||
if (!currentTheme?.includes("DankShell")) return;
|
||||
|
||||
let themeFile;
|
||||
switch (true) {
|
||||
case currentTheme.includes("Light"):
|
||||
themeFile = path.join(themesDir, "dankshell-light.json");
|
||||
break;
|
||||
case currentTheme.includes("Dark"):
|
||||
themeFile = path.join(themesDir, "dankshell-dark.json");
|
||||
break;
|
||||
default:
|
||||
themeFile = path.join(themesDir, "dankshell-default.json");
|
||||
}
|
||||
|
||||
let themeData;
|
||||
try {
|
||||
const content = fs.readFileSync(themeFile, "utf8");
|
||||
themeData = JSON.parse(content);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const themeKey = `[${currentTheme}]`;
|
||||
|
||||
if (themeData.colors) {
|
||||
const colorConfig = config.get("colorCustomizations") || {};
|
||||
colorConfig[themeKey] = themeData.colors;
|
||||
await config.update(
|
||||
"colorCustomizations",
|
||||
colorConfig,
|
||||
vscode.ConfigurationTarget.Global
|
||||
);
|
||||
}
|
||||
|
||||
if (themeData.tokenColors) {
|
||||
const editorConfig = vscode.workspace.getConfiguration("editor");
|
||||
const tokenConfig = editorConfig.get("tokenColorCustomizations") || {};
|
||||
tokenConfig[themeKey] = { textMateRules: themeData.tokenColors };
|
||||
await editorConfig.update(
|
||||
"tokenColorCustomizations",
|
||||
tokenConfig,
|
||||
vscode.ConfigurationTarget.Global
|
||||
);
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
watcher.onDidChange(reloadTheme);
|
||||
watcher.onDidCreate(reloadTheme);
|
||||
|
||||
context.subscriptions.push(watcher);
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
if (reloadTimeout) clearTimeout(reloadTimeout);
|
||||
if (watcher) watcher.dispose();
|
||||
}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
@@ -1,47 +1,46 @@
|
||||
{
|
||||
"name": "dynamic-base16-dankshell",
|
||||
"displayName": "Dynamic Base16 DankShell",
|
||||
"description": "Dynamic Material You theme with base16 terminal colors - auto-updated by DankMaterialShell",
|
||||
"publisher": "local",
|
||||
"version": "0.0.1",
|
||||
"name": "dms-theme",
|
||||
"displayName": "DMS - Dank Material Shell Theme",
|
||||
"description": "Dynamic theme using matugen & dank16 terminal colors - auto-updated by DankMaterialShell",
|
||||
"publisher": "DankLinux",
|
||||
"version": "0.0.3",
|
||||
"icon": "danklogo.png",
|
||||
"engines": {
|
||||
"vscode": "^1.70.0"
|
||||
},
|
||||
"main": "./extension.js",
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
"categories": [
|
||||
"Themes"
|
||||
],
|
||||
"keywords": [
|
||||
"theme",
|
||||
"dms",
|
||||
"dank",
|
||||
"material",
|
||||
"material you",
|
||||
"base16",
|
||||
"dynamic",
|
||||
"dankshell"
|
||||
"dankshell",
|
||||
"danklinux"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/AvengeMedia/DankMaterialShellGit"
|
||||
"url": "https://github.com/AvengeMedia/DankMaterialShell"
|
||||
},
|
||||
"contributes": {
|
||||
"themes": [
|
||||
{
|
||||
"label": "Dynamic Base16 DankShell",
|
||||
"uiTheme": "vs-dark",
|
||||
"path": "./themes/dankshell-default.json"
|
||||
"path": "./themes/dankshell-default.json",
|
||||
"_watch": true
|
||||
},
|
||||
{
|
||||
"label": "Dynamic Base16 DankShell (Dark)",
|
||||
"uiTheme": "vs-dark",
|
||||
"path": "./themes/dankshell-dark.json"
|
||||
"path": "./themes/dankshell-dark.json",
|
||||
"_watch": true
|
||||
},
|
||||
{
|
||||
"label": "Dynamic Base16 DankShell (Light)",
|
||||
"uiTheme": "vs",
|
||||
"path": "./themes/dankshell-light.json"
|
||||
"path": "./themes/dankshell-light.json",
|
||||
"_watch": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user