diff --git a/core/.mockery.yml b/core/.mockery.yml index 4b6f6253..18be7864 100644 --- a/core/.mockery.yml +++ b/core/.mockery.yml @@ -34,12 +34,16 @@ packages: outpkg: mocks_network interfaces: Backend: - github.com/AvengeMedia/danklinux/internal/server/cups: + github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups: config: dir: "internal/mocks/cups" outpkg: mocks_cups interfaces: CUPSClientInterface: + PkHelper: + config: + dir: "internal/mocks/cups_pkhelper" + outpkg: mocks_cups_pkhelper github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev: config: dir: "internal/mocks/evdev" diff --git a/core/cmd/dms/commands_common.go b/core/cmd/dms/commands_common.go index b262a269..861515c0 100644 --- a/core/cmd/dms/commands_common.go +++ b/core/cmd/dms/commands_common.go @@ -140,6 +140,7 @@ func runVersion(cmd *cobra.Command, args []string) { } func startDebugServer() error { + server.CLIVersion = Version return server.Start(true) } diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index abb4ab37..7da8ba0b 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -298,7 +298,8 @@ func ensureGreetdEnabled() error { fmt.Println(" ✓ Unmasked greetd") } - if state.EnabledState == "disabled" || state.EnabledState == "masked" || state.EnabledState == "masked-runtime" { + switch state.EnabledState { + case "disabled", "masked", "masked-runtime": fmt.Println(" Enabling greetd service...") enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd") enableCmd.Stdout = os.Stdout @@ -307,9 +308,9 @@ func ensureGreetdEnabled() error { return fmt.Errorf("failed to enable greetd: %w", err) } fmt.Println(" ✓ Enabled greetd service") - } else if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" { + case "enabled", "enabled-runtime": fmt.Println(" ✓ greetd is already enabled") - } else { + default: fmt.Printf(" ℹ greetd is in state '%s' (should work, no action needed)\n", state.EnabledState) } diff --git a/core/cmd/dms/shell.go b/core/cmd/dms/shell.go index ae750d58..86dd8c13 100644 --- a/core/cmd/dms/shell.go +++ b/core/cmd/dms/shell.go @@ -383,6 +383,7 @@ func runShellDaemon(session bool) { errChan <- fmt.Errorf("server panic: %v", r) } }() + server.CLIVersion = Version if err := server.Start(false); err != nil { errChan <- fmt.Errorf("server error: %w", err) } diff --git a/core/internal/config/embedded/testpage.pdf b/core/internal/config/embedded/testpage.pdf new file mode 100644 index 00000000..03e05c49 Binary files /dev/null and b/core/internal/config/embedded/testpage.pdf differ diff --git a/core/internal/config/testpage.go b/core/internal/config/testpage.go new file mode 100644 index 00000000..2c5bbc79 --- /dev/null +++ b/core/internal/config/testpage.go @@ -0,0 +1,6 @@ +package config + +import _ "embed" + +//go:embed embedded/testpage.pdf +var TestPage string diff --git a/core/internal/mocks/cups/mock_CUPSClientInterface.go b/core/internal/mocks/cups/mock_CUPSClientInterface.go index 3e198496..3c980747 100644 --- a/core/internal/mocks/cups/mock_CUPSClientInterface.go +++ b/core/internal/mocks/cups/mock_CUPSClientInterface.go @@ -22,6 +22,99 @@ func (_m *MockCUPSClientInterface) EXPECT() *MockCUPSClientInterface_Expecter { return &MockCUPSClientInterface_Expecter{mock: &_m.Mock} } +// AcceptJobs provides a mock function with given fields: printer +func (_m *MockCUPSClientInterface) AcceptJobs(printer string) error { + ret := _m.Called(printer) + + if len(ret) == 0 { + panic("no return value specified for AcceptJobs") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(printer) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_AcceptJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AcceptJobs' +type MockCUPSClientInterface_AcceptJobs_Call struct { + *mock.Call +} + +// AcceptJobs is a helper method to define mock.On call +// - printer string +func (_e *MockCUPSClientInterface_Expecter) AcceptJobs(printer interface{}) *MockCUPSClientInterface_AcceptJobs_Call { + return &MockCUPSClientInterface_AcceptJobs_Call{Call: _e.mock.On("AcceptJobs", printer)} +} + +func (_c *MockCUPSClientInterface_AcceptJobs_Call) Run(run func(printer string)) *MockCUPSClientInterface_AcceptJobs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_AcceptJobs_Call) Return(_a0 error) *MockCUPSClientInterface_AcceptJobs_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_AcceptJobs_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_AcceptJobs_Call { + _c.Call.Return(run) + return _c +} + +// AddPrinterToClass provides a mock function with given fields: class, printer +func (_m *MockCUPSClientInterface) AddPrinterToClass(class string, printer string) error { + ret := _m.Called(class, printer) + + if len(ret) == 0 { + panic("no return value specified for AddPrinterToClass") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(class, printer) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_AddPrinterToClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddPrinterToClass' +type MockCUPSClientInterface_AddPrinterToClass_Call struct { + *mock.Call +} + +// AddPrinterToClass is a helper method to define mock.On call +// - class string +// - printer string +func (_e *MockCUPSClientInterface_Expecter) AddPrinterToClass(class interface{}, printer interface{}) *MockCUPSClientInterface_AddPrinterToClass_Call { + return &MockCUPSClientInterface_AddPrinterToClass_Call{Call: _e.mock.On("AddPrinterToClass", class, printer)} +} + +func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) Run(run func(class string, printer string)) *MockCUPSClientInterface_AddPrinterToClass_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) Return(_a0 error) *MockCUPSClientInterface_AddPrinterToClass_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_AddPrinterToClass_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_AddPrinterToClass_Call { + _c.Call.Return(run) + return _c +} + // CancelAllJob provides a mock function with given fields: printer, purge func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error { ret := _m.Called(printer, purge) @@ -116,6 +209,312 @@ func (_c *MockCUPSClientInterface_CancelJob_Call) RunAndReturn(run func(int, boo return _c } +// CreatePrinter provides a mock function with given fields: name, deviceURI, ppd, shared, errorPolicy, information, location +func (_m *MockCUPSClientInterface) CreatePrinter(name string, deviceURI string, ppd string, shared bool, errorPolicy string, information string, location string) error { + ret := _m.Called(name, deviceURI, ppd, shared, errorPolicy, information, location) + + if len(ret) == 0 { + panic("no return value specified for CreatePrinter") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, bool, string, string, string) error); ok { + r0 = rf(name, deviceURI, ppd, shared, errorPolicy, information, location) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_CreatePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePrinter' +type MockCUPSClientInterface_CreatePrinter_Call struct { + *mock.Call +} + +// CreatePrinter is a helper method to define mock.On call +// - name string +// - deviceURI string +// - ppd string +// - shared bool +// - errorPolicy string +// - information string +// - location string +func (_e *MockCUPSClientInterface_Expecter) CreatePrinter(name interface{}, deviceURI interface{}, ppd interface{}, shared interface{}, errorPolicy interface{}, information interface{}, location interface{}) *MockCUPSClientInterface_CreatePrinter_Call { + return &MockCUPSClientInterface_CreatePrinter_Call{Call: _e.mock.On("CreatePrinter", name, deviceURI, ppd, shared, errorPolicy, information, location)} +} + +func (_c *MockCUPSClientInterface_CreatePrinter_Call) Run(run func(name string, deviceURI string, ppd string, shared bool, errorPolicy string, information string, location string)) *MockCUPSClientInterface_CreatePrinter_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string), args[3].(bool), args[4].(string), args[5].(string), args[6].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_CreatePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_CreatePrinter_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_CreatePrinter_Call) RunAndReturn(run func(string, string, string, bool, string, string, string) error) *MockCUPSClientInterface_CreatePrinter_Call { + _c.Call.Return(run) + return _c +} + +// DeleteClass provides a mock function with given fields: class +func (_m *MockCUPSClientInterface) DeleteClass(class string) error { + ret := _m.Called(class) + + if len(ret) == 0 { + panic("no return value specified for DeleteClass") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(class) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_DeleteClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteClass' +type MockCUPSClientInterface_DeleteClass_Call struct { + *mock.Call +} + +// DeleteClass is a helper method to define mock.On call +// - class string +func (_e *MockCUPSClientInterface_Expecter) DeleteClass(class interface{}) *MockCUPSClientInterface_DeleteClass_Call { + return &MockCUPSClientInterface_DeleteClass_Call{Call: _e.mock.On("DeleteClass", class)} +} + +func (_c *MockCUPSClientInterface_DeleteClass_Call) Run(run func(class string)) *MockCUPSClientInterface_DeleteClass_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_DeleteClass_Call) Return(_a0 error) *MockCUPSClientInterface_DeleteClass_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_DeleteClass_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_DeleteClass_Call { + _c.Call.Return(run) + return _c +} + +// DeletePrinter provides a mock function with given fields: printer +func (_m *MockCUPSClientInterface) DeletePrinter(printer string) error { + ret := _m.Called(printer) + + if len(ret) == 0 { + panic("no return value specified for DeletePrinter") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(printer) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_DeletePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePrinter' +type MockCUPSClientInterface_DeletePrinter_Call struct { + *mock.Call +} + +// DeletePrinter is a helper method to define mock.On call +// - printer string +func (_e *MockCUPSClientInterface_Expecter) DeletePrinter(printer interface{}) *MockCUPSClientInterface_DeletePrinter_Call { + return &MockCUPSClientInterface_DeletePrinter_Call{Call: _e.mock.On("DeletePrinter", printer)} +} + +func (_c *MockCUPSClientInterface_DeletePrinter_Call) Run(run func(printer string)) *MockCUPSClientInterface_DeletePrinter_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_DeletePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_DeletePrinter_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_DeletePrinter_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_DeletePrinter_Call { + _c.Call.Return(run) + return _c +} + +// DeletePrinterFromClass provides a mock function with given fields: class, printer +func (_m *MockCUPSClientInterface) DeletePrinterFromClass(class string, printer string) error { + ret := _m.Called(class, printer) + + if len(ret) == 0 { + panic("no return value specified for DeletePrinterFromClass") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(class, printer) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_DeletePrinterFromClass_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePrinterFromClass' +type MockCUPSClientInterface_DeletePrinterFromClass_Call struct { + *mock.Call +} + +// DeletePrinterFromClass is a helper method to define mock.On call +// - class string +// - printer string +func (_e *MockCUPSClientInterface_Expecter) DeletePrinterFromClass(class interface{}, printer interface{}) *MockCUPSClientInterface_DeletePrinterFromClass_Call { + return &MockCUPSClientInterface_DeletePrinterFromClass_Call{Call: _e.mock.On("DeletePrinterFromClass", class, printer)} +} + +func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) Run(run func(class string, printer string)) *MockCUPSClientInterface_DeletePrinterFromClass_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) Return(_a0 error) *MockCUPSClientInterface_DeletePrinterFromClass_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_DeletePrinterFromClass_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_DeletePrinterFromClass_Call { + _c.Call.Return(run) + return _c +} + +// GetClasses provides a mock function with given fields: attributes +func (_m *MockCUPSClientInterface) GetClasses(attributes []string) (map[string]ipp.Attributes, error) { + ret := _m.Called(attributes) + + if len(ret) == 0 { + panic("no return value specified for GetClasses") + } + + var r0 map[string]ipp.Attributes + var r1 error + if rf, ok := ret.Get(0).(func([]string) (map[string]ipp.Attributes, error)); ok { + return rf(attributes) + } + if rf, ok := ret.Get(0).(func([]string) map[string]ipp.Attributes); ok { + r0 = rf(attributes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]ipp.Attributes) + } + } + + if rf, ok := ret.Get(1).(func([]string) error); ok { + r1 = rf(attributes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCUPSClientInterface_GetClasses_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetClasses' +type MockCUPSClientInterface_GetClasses_Call struct { + *mock.Call +} + +// GetClasses is a helper method to define mock.On call +// - attributes []string +func (_e *MockCUPSClientInterface_Expecter) GetClasses(attributes interface{}) *MockCUPSClientInterface_GetClasses_Call { + return &MockCUPSClientInterface_GetClasses_Call{Call: _e.mock.On("GetClasses", attributes)} +} + +func (_c *MockCUPSClientInterface_GetClasses_Call) Run(run func(attributes []string)) *MockCUPSClientInterface_GetClasses_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_GetClasses_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetClasses_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCUPSClientInterface_GetClasses_Call) RunAndReturn(run func([]string) (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetClasses_Call { + _c.Call.Return(run) + return _c +} + +// GetDevices provides a mock function with no fields +func (_m *MockCUPSClientInterface) GetDevices() (map[string]ipp.Attributes, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetDevices") + } + + var r0 map[string]ipp.Attributes + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]ipp.Attributes, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]ipp.Attributes); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]ipp.Attributes) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCUPSClientInterface_GetDevices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDevices' +type MockCUPSClientInterface_GetDevices_Call struct { + *mock.Call +} + +// GetDevices is a helper method to define mock.On call +func (_e *MockCUPSClientInterface_Expecter) GetDevices() *MockCUPSClientInterface_GetDevices_Call { + return &MockCUPSClientInterface_GetDevices_Call{Call: _e.mock.On("GetDevices")} +} + +func (_c *MockCUPSClientInterface_GetDevices_Call) Run(run func()) *MockCUPSClientInterface_GetDevices_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCUPSClientInterface_GetDevices_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetDevices_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCUPSClientInterface_GetDevices_Call) RunAndReturn(run func() (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetDevices_Call { + _c.Call.Return(run) + return _c +} + // GetJobs provides a mock function with given fields: printer, class, whichJobs, myJobs, firstJobId, limit, attributes func (_m *MockCUPSClientInterface) GetJobs(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string) (map[int]ipp.Attributes, error) { ret := _m.Called(printer, class, whichJobs, myJobs, firstJobId, limit, attributes) @@ -180,6 +579,63 @@ func (_c *MockCUPSClientInterface_GetJobs_Call) RunAndReturn(run func(string, st return _c } +// GetPPDs provides a mock function with no fields +func (_m *MockCUPSClientInterface) GetPPDs() (map[string]ipp.Attributes, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetPPDs") + } + + var r0 map[string]ipp.Attributes + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]ipp.Attributes, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]ipp.Attributes); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]ipp.Attributes) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCUPSClientInterface_GetPPDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPPDs' +type MockCUPSClientInterface_GetPPDs_Call struct { + *mock.Call +} + +// GetPPDs is a helper method to define mock.On call +func (_e *MockCUPSClientInterface_Expecter) GetPPDs() *MockCUPSClientInterface_GetPPDs_Call { + return &MockCUPSClientInterface_GetPPDs_Call{Call: _e.mock.On("GetPPDs")} +} + +func (_c *MockCUPSClientInterface_GetPPDs_Call) Run(run func()) *MockCUPSClientInterface_GetPPDs_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCUPSClientInterface_GetPPDs_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetPPDs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCUPSClientInterface_GetPPDs_Call) RunAndReturn(run func() (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetPPDs_Call { + _c.Call.Return(run) + return _c +} + // GetPrinters provides a mock function with given fields: attributes func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) { ret := _m.Called(attributes) @@ -238,6 +694,100 @@ func (_c *MockCUPSClientInterface_GetPrinters_Call) RunAndReturn(run func([]stri return _c } +// HoldJobUntil provides a mock function with given fields: jobID, holdUntil +func (_m *MockCUPSClientInterface) HoldJobUntil(jobID int, holdUntil string) error { + ret := _m.Called(jobID, holdUntil) + + if len(ret) == 0 { + panic("no return value specified for HoldJobUntil") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int, string) error); ok { + r0 = rf(jobID, holdUntil) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_HoldJobUntil_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HoldJobUntil' +type MockCUPSClientInterface_HoldJobUntil_Call struct { + *mock.Call +} + +// HoldJobUntil is a helper method to define mock.On call +// - jobID int +// - holdUntil string +func (_e *MockCUPSClientInterface_Expecter) HoldJobUntil(jobID interface{}, holdUntil interface{}) *MockCUPSClientInterface_HoldJobUntil_Call { + return &MockCUPSClientInterface_HoldJobUntil_Call{Call: _e.mock.On("HoldJobUntil", jobID, holdUntil)} +} + +func (_c *MockCUPSClientInterface_HoldJobUntil_Call) Run(run func(jobID int, holdUntil string)) *MockCUPSClientInterface_HoldJobUntil_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_HoldJobUntil_Call) Return(_a0 error) *MockCUPSClientInterface_HoldJobUntil_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_HoldJobUntil_Call) RunAndReturn(run func(int, string) error) *MockCUPSClientInterface_HoldJobUntil_Call { + _c.Call.Return(run) + return _c +} + +// MoveJob provides a mock function with given fields: jobID, destPrinter +func (_m *MockCUPSClientInterface) MoveJob(jobID int, destPrinter string) error { + ret := _m.Called(jobID, destPrinter) + + if len(ret) == 0 { + panic("no return value specified for MoveJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int, string) error); ok { + r0 = rf(jobID, destPrinter) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_MoveJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MoveJob' +type MockCUPSClientInterface_MoveJob_Call struct { + *mock.Call +} + +// MoveJob is a helper method to define mock.On call +// - jobID int +// - destPrinter string +func (_e *MockCUPSClientInterface_Expecter) MoveJob(jobID interface{}, destPrinter interface{}) *MockCUPSClientInterface_MoveJob_Call { + return &MockCUPSClientInterface_MoveJob_Call{Call: _e.mock.On("MoveJob", jobID, destPrinter)} +} + +func (_c *MockCUPSClientInterface_MoveJob_Call) Run(run func(jobID int, destPrinter string)) *MockCUPSClientInterface_MoveJob_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_MoveJob_Call) Return(_a0 error) *MockCUPSClientInterface_MoveJob_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_MoveJob_Call) RunAndReturn(run func(int, string) error) *MockCUPSClientInterface_MoveJob_Call { + _c.Call.Return(run) + return _c +} + // PausePrinter provides a mock function with given fields: printer func (_m *MockCUPSClientInterface) PausePrinter(printer string) error { ret := _m.Called(printer) @@ -284,6 +834,156 @@ func (_c *MockCUPSClientInterface_PausePrinter_Call) RunAndReturn(run func(strin return _c } +// PrintTestPage provides a mock function with given fields: printer, testPageData, size +func (_m *MockCUPSClientInterface) PrintTestPage(printer string, testPageData io.Reader, size int) (int, error) { + ret := _m.Called(printer, testPageData, size) + + if len(ret) == 0 { + panic("no return value specified for PrintTestPage") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(string, io.Reader, int) (int, error)); ok { + return rf(printer, testPageData, size) + } + if rf, ok := ret.Get(0).(func(string, io.Reader, int) int); ok { + r0 = rf(printer, testPageData, size) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(string, io.Reader, int) error); ok { + r1 = rf(printer, testPageData, size) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCUPSClientInterface_PrintTestPage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrintTestPage' +type MockCUPSClientInterface_PrintTestPage_Call struct { + *mock.Call +} + +// PrintTestPage is a helper method to define mock.On call +// - printer string +// - testPageData io.Reader +// - size int +func (_e *MockCUPSClientInterface_Expecter) PrintTestPage(printer interface{}, testPageData interface{}, size interface{}) *MockCUPSClientInterface_PrintTestPage_Call { + return &MockCUPSClientInterface_PrintTestPage_Call{Call: _e.mock.On("PrintTestPage", printer, testPageData, size)} +} + +func (_c *MockCUPSClientInterface_PrintTestPage_Call) Run(run func(printer string, testPageData io.Reader, size int)) *MockCUPSClientInterface_PrintTestPage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(io.Reader), args[2].(int)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_PrintTestPage_Call) Return(_a0 int, _a1 error) *MockCUPSClientInterface_PrintTestPage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCUPSClientInterface_PrintTestPage_Call) RunAndReturn(run func(string, io.Reader, int) (int, error)) *MockCUPSClientInterface_PrintTestPage_Call { + _c.Call.Return(run) + return _c +} + +// RejectJobs provides a mock function with given fields: printer +func (_m *MockCUPSClientInterface) RejectJobs(printer string) error { + ret := _m.Called(printer) + + if len(ret) == 0 { + panic("no return value specified for RejectJobs") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(printer) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_RejectJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RejectJobs' +type MockCUPSClientInterface_RejectJobs_Call struct { + *mock.Call +} + +// RejectJobs is a helper method to define mock.On call +// - printer string +func (_e *MockCUPSClientInterface_Expecter) RejectJobs(printer interface{}) *MockCUPSClientInterface_RejectJobs_Call { + return &MockCUPSClientInterface_RejectJobs_Call{Call: _e.mock.On("RejectJobs", printer)} +} + +func (_c *MockCUPSClientInterface_RejectJobs_Call) Run(run func(printer string)) *MockCUPSClientInterface_RejectJobs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_RejectJobs_Call) Return(_a0 error) *MockCUPSClientInterface_RejectJobs_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_RejectJobs_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_RejectJobs_Call { + _c.Call.Return(run) + return _c +} + +// RestartJob provides a mock function with given fields: jobID +func (_m *MockCUPSClientInterface) RestartJob(jobID int) error { + ret := _m.Called(jobID) + + if len(ret) == 0 { + panic("no return value specified for RestartJob") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int) error); ok { + r0 = rf(jobID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_RestartJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestartJob' +type MockCUPSClientInterface_RestartJob_Call struct { + *mock.Call +} + +// RestartJob is a helper method to define mock.On call +// - jobID int +func (_e *MockCUPSClientInterface_Expecter) RestartJob(jobID interface{}) *MockCUPSClientInterface_RestartJob_Call { + return &MockCUPSClientInterface_RestartJob_Call{Call: _e.mock.On("RestartJob", jobID)} +} + +func (_c *MockCUPSClientInterface_RestartJob_Call) Run(run func(jobID int)) *MockCUPSClientInterface_RestartJob_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_RestartJob_Call) Return(_a0 error) *MockCUPSClientInterface_RestartJob_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_RestartJob_Call) RunAndReturn(run func(int) error) *MockCUPSClientInterface_RestartJob_Call { + _c.Call.Return(run) + return _c +} + // ResumePrinter provides a mock function with given fields: printer func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error { ret := _m.Called(printer) @@ -390,6 +1090,147 @@ func (_c *MockCUPSClientInterface_SendRequest_Call) RunAndReturn(run func(string return _c } +// SetPrinterInformation provides a mock function with given fields: printer, information +func (_m *MockCUPSClientInterface) SetPrinterInformation(printer string, information string) error { + ret := _m.Called(printer, information) + + if len(ret) == 0 { + panic("no return value specified for SetPrinterInformation") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(printer, information) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_SetPrinterInformation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterInformation' +type MockCUPSClientInterface_SetPrinterInformation_Call struct { + *mock.Call +} + +// SetPrinterInformation is a helper method to define mock.On call +// - printer string +// - information string +func (_e *MockCUPSClientInterface_Expecter) SetPrinterInformation(printer interface{}, information interface{}) *MockCUPSClientInterface_SetPrinterInformation_Call { + return &MockCUPSClientInterface_SetPrinterInformation_Call{Call: _e.mock.On("SetPrinterInformation", printer, information)} +} + +func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) Run(run func(printer string, information string)) *MockCUPSClientInterface_SetPrinterInformation_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterInformation_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_SetPrinterInformation_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_SetPrinterInformation_Call { + _c.Call.Return(run) + return _c +} + +// SetPrinterIsShared provides a mock function with given fields: printer, shared +func (_m *MockCUPSClientInterface) SetPrinterIsShared(printer string, shared bool) error { + ret := _m.Called(printer, shared) + + if len(ret) == 0 { + panic("no return value specified for SetPrinterIsShared") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, bool) error); ok { + r0 = rf(printer, shared) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_SetPrinterIsShared_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterIsShared' +type MockCUPSClientInterface_SetPrinterIsShared_Call struct { + *mock.Call +} + +// SetPrinterIsShared is a helper method to define mock.On call +// - printer string +// - shared bool +func (_e *MockCUPSClientInterface_Expecter) SetPrinterIsShared(printer interface{}, shared interface{}) *MockCUPSClientInterface_SetPrinterIsShared_Call { + return &MockCUPSClientInterface_SetPrinterIsShared_Call{Call: _e.mock.On("SetPrinterIsShared", printer, shared)} +} + +func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) Run(run func(printer string, shared bool)) *MockCUPSClientInterface_SetPrinterIsShared_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterIsShared_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_SetPrinterIsShared_Call) RunAndReturn(run func(string, bool) error) *MockCUPSClientInterface_SetPrinterIsShared_Call { + _c.Call.Return(run) + return _c +} + +// SetPrinterLocation provides a mock function with given fields: printer, location +func (_m *MockCUPSClientInterface) SetPrinterLocation(printer string, location string) error { + ret := _m.Called(printer, location) + + if len(ret) == 0 { + panic("no return value specified for SetPrinterLocation") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(printer, location) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCUPSClientInterface_SetPrinterLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPrinterLocation' +type MockCUPSClientInterface_SetPrinterLocation_Call struct { + *mock.Call +} + +// SetPrinterLocation is a helper method to define mock.On call +// - printer string +// - location string +func (_e *MockCUPSClientInterface_Expecter) SetPrinterLocation(printer interface{}, location interface{}) *MockCUPSClientInterface_SetPrinterLocation_Call { + return &MockCUPSClientInterface_SetPrinterLocation_Call{Call: _e.mock.On("SetPrinterLocation", printer, location)} +} + +func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) Run(run func(printer string, location string)) *MockCUPSClientInterface_SetPrinterLocation_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) Return(_a0 error) *MockCUPSClientInterface_SetPrinterLocation_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCUPSClientInterface_SetPrinterLocation_Call) RunAndReturn(run func(string, string) error) *MockCUPSClientInterface_SetPrinterLocation_Call { + _c.Call.Return(run) + return _c +} + // NewMockCUPSClientInterface creates a new instance of MockCUPSClientInterface. 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 NewMockCUPSClientInterface(t interface { diff --git a/core/internal/mocks/cups_pkhelper/mock_PkHelper.go b/core/internal/mocks/cups_pkhelper/mock_PkHelper.go new file mode 100644 index 00000000..cad5bfcd --- /dev/null +++ b/core/internal/mocks/cups_pkhelper/mock_PkHelper.go @@ -0,0 +1,708 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks_cups_pkhelper + +import ( + cups "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" + mock "github.com/stretchr/testify/mock" +) + +// MockPkHelper is an autogenerated mock type for the PkHelper type +type MockPkHelper struct { + mock.Mock +} + +type MockPkHelper_Expecter struct { + mock *mock.Mock +} + +func (_m *MockPkHelper) EXPECT() *MockPkHelper_Expecter { + return &MockPkHelper_Expecter{mock: &_m.Mock} +} + +// ClassAddPrinter provides a mock function with given fields: className, printerName +func (_m *MockPkHelper) ClassAddPrinter(className string, printerName string) error { + ret := _m.Called(className, printerName) + + if len(ret) == 0 { + panic("no return value specified for ClassAddPrinter") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(className, printerName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_ClassAddPrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassAddPrinter' +type MockPkHelper_ClassAddPrinter_Call struct { + *mock.Call +} + +// ClassAddPrinter is a helper method to define mock.On call +// - className string +// - printerName string +func (_e *MockPkHelper_Expecter) ClassAddPrinter(className interface{}, printerName interface{}) *MockPkHelper_ClassAddPrinter_Call { + return &MockPkHelper_ClassAddPrinter_Call{Call: _e.mock.On("ClassAddPrinter", className, printerName)} +} + +func (_c *MockPkHelper_ClassAddPrinter_Call) Run(run func(className string, printerName string)) *MockPkHelper_ClassAddPrinter_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockPkHelper_ClassAddPrinter_Call) Return(_a0 error) *MockPkHelper_ClassAddPrinter_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_ClassAddPrinter_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_ClassAddPrinter_Call { + _c.Call.Return(run) + return _c +} + +// ClassDelete provides a mock function with given fields: className +func (_m *MockPkHelper) ClassDelete(className string) error { + ret := _m.Called(className) + + if len(ret) == 0 { + panic("no return value specified for ClassDelete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(className) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_ClassDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassDelete' +type MockPkHelper_ClassDelete_Call struct { + *mock.Call +} + +// ClassDelete is a helper method to define mock.On call +// - className string +func (_e *MockPkHelper_Expecter) ClassDelete(className interface{}) *MockPkHelper_ClassDelete_Call { + return &MockPkHelper_ClassDelete_Call{Call: _e.mock.On("ClassDelete", className)} +} + +func (_c *MockPkHelper_ClassDelete_Call) Run(run func(className string)) *MockPkHelper_ClassDelete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockPkHelper_ClassDelete_Call) Return(_a0 error) *MockPkHelper_ClassDelete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_ClassDelete_Call) RunAndReturn(run func(string) error) *MockPkHelper_ClassDelete_Call { + _c.Call.Return(run) + return _c +} + +// ClassDeletePrinter provides a mock function with given fields: className, printerName +func (_m *MockPkHelper) ClassDeletePrinter(className string, printerName string) error { + ret := _m.Called(className, printerName) + + if len(ret) == 0 { + panic("no return value specified for ClassDeletePrinter") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(className, printerName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_ClassDeletePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClassDeletePrinter' +type MockPkHelper_ClassDeletePrinter_Call struct { + *mock.Call +} + +// ClassDeletePrinter is a helper method to define mock.On call +// - className string +// - printerName string +func (_e *MockPkHelper_Expecter) ClassDeletePrinter(className interface{}, printerName interface{}) *MockPkHelper_ClassDeletePrinter_Call { + return &MockPkHelper_ClassDeletePrinter_Call{Call: _e.mock.On("ClassDeletePrinter", className, printerName)} +} + +func (_c *MockPkHelper_ClassDeletePrinter_Call) Run(run func(className string, printerName string)) *MockPkHelper_ClassDeletePrinter_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockPkHelper_ClassDeletePrinter_Call) Return(_a0 error) *MockPkHelper_ClassDeletePrinter_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_ClassDeletePrinter_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_ClassDeletePrinter_Call { + _c.Call.Return(run) + return _c +} + +// DevicesGet provides a mock function with given fields: timeout, limit, includeSchemes, excludeSchemes +func (_m *MockPkHelper) DevicesGet(timeout int, limit int, includeSchemes []string, excludeSchemes []string) ([]cups.Device, error) { + ret := _m.Called(timeout, limit, includeSchemes, excludeSchemes) + + if len(ret) == 0 { + panic("no return value specified for DevicesGet") + } + + var r0 []cups.Device + var r1 error + if rf, ok := ret.Get(0).(func(int, int, []string, []string) ([]cups.Device, error)); ok { + return rf(timeout, limit, includeSchemes, excludeSchemes) + } + if rf, ok := ret.Get(0).(func(int, int, []string, []string) []cups.Device); ok { + r0 = rf(timeout, limit, includeSchemes, excludeSchemes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]cups.Device) + } + } + + if rf, ok := ret.Get(1).(func(int, int, []string, []string) error); ok { + r1 = rf(timeout, limit, includeSchemes, excludeSchemes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockPkHelper_DevicesGet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DevicesGet' +type MockPkHelper_DevicesGet_Call struct { + *mock.Call +} + +// DevicesGet is a helper method to define mock.On call +// - timeout int +// - limit int +// - includeSchemes []string +// - excludeSchemes []string +func (_e *MockPkHelper_Expecter) DevicesGet(timeout interface{}, limit interface{}, includeSchemes interface{}, excludeSchemes interface{}) *MockPkHelper_DevicesGet_Call { + return &MockPkHelper_DevicesGet_Call{Call: _e.mock.On("DevicesGet", timeout, limit, includeSchemes, excludeSchemes)} +} + +func (_c *MockPkHelper_DevicesGet_Call) Run(run func(timeout int, limit int, includeSchemes []string, excludeSchemes []string)) *MockPkHelper_DevicesGet_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(int), args[2].([]string), args[3].([]string)) + }) + return _c +} + +func (_c *MockPkHelper_DevicesGet_Call) Return(_a0 []cups.Device, _a1 error) *MockPkHelper_DevicesGet_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockPkHelper_DevicesGet_Call) RunAndReturn(run func(int, int, []string, []string) ([]cups.Device, error)) *MockPkHelper_DevicesGet_Call { + _c.Call.Return(run) + return _c +} + +// JobCancelPurge provides a mock function with given fields: jobID, purge +func (_m *MockPkHelper) JobCancelPurge(jobID int, purge bool) error { + ret := _m.Called(jobID, purge) + + if len(ret) == 0 { + panic("no return value specified for JobCancelPurge") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int, bool) error); ok { + r0 = rf(jobID, purge) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_JobCancelPurge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobCancelPurge' +type MockPkHelper_JobCancelPurge_Call struct { + *mock.Call +} + +// JobCancelPurge is a helper method to define mock.On call +// - jobID int +// - purge bool +func (_e *MockPkHelper_Expecter) JobCancelPurge(jobID interface{}, purge interface{}) *MockPkHelper_JobCancelPurge_Call { + return &MockPkHelper_JobCancelPurge_Call{Call: _e.mock.On("JobCancelPurge", jobID, purge)} +} + +func (_c *MockPkHelper_JobCancelPurge_Call) Run(run func(jobID int, purge bool)) *MockPkHelper_JobCancelPurge_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(bool)) + }) + return _c +} + +func (_c *MockPkHelper_JobCancelPurge_Call) Return(_a0 error) *MockPkHelper_JobCancelPurge_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_JobCancelPurge_Call) RunAndReturn(run func(int, bool) error) *MockPkHelper_JobCancelPurge_Call { + _c.Call.Return(run) + return _c +} + +// JobRestart provides a mock function with given fields: jobID +func (_m *MockPkHelper) JobRestart(jobID int) error { + ret := _m.Called(jobID) + + if len(ret) == 0 { + panic("no return value specified for JobRestart") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int) error); ok { + r0 = rf(jobID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_JobRestart_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobRestart' +type MockPkHelper_JobRestart_Call struct { + *mock.Call +} + +// JobRestart is a helper method to define mock.On call +// - jobID int +func (_e *MockPkHelper_Expecter) JobRestart(jobID interface{}) *MockPkHelper_JobRestart_Call { + return &MockPkHelper_JobRestart_Call{Call: _e.mock.On("JobRestart", jobID)} +} + +func (_c *MockPkHelper_JobRestart_Call) Run(run func(jobID int)) *MockPkHelper_JobRestart_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int)) + }) + return _c +} + +func (_c *MockPkHelper_JobRestart_Call) Return(_a0 error) *MockPkHelper_JobRestart_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_JobRestart_Call) RunAndReturn(run func(int) error) *MockPkHelper_JobRestart_Call { + _c.Call.Return(run) + return _c +} + +// JobSetHoldUntil provides a mock function with given fields: jobID, holdUntil +func (_m *MockPkHelper) JobSetHoldUntil(jobID int, holdUntil string) error { + ret := _m.Called(jobID, holdUntil) + + if len(ret) == 0 { + panic("no return value specified for JobSetHoldUntil") + } + + var r0 error + if rf, ok := ret.Get(0).(func(int, string) error); ok { + r0 = rf(jobID, holdUntil) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_JobSetHoldUntil_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JobSetHoldUntil' +type MockPkHelper_JobSetHoldUntil_Call struct { + *mock.Call +} + +// JobSetHoldUntil is a helper method to define mock.On call +// - jobID int +// - holdUntil string +func (_e *MockPkHelper_Expecter) JobSetHoldUntil(jobID interface{}, holdUntil interface{}) *MockPkHelper_JobSetHoldUntil_Call { + return &MockPkHelper_JobSetHoldUntil_Call{Call: _e.mock.On("JobSetHoldUntil", jobID, holdUntil)} +} + +func (_c *MockPkHelper_JobSetHoldUntil_Call) Run(run func(jobID int, holdUntil string)) *MockPkHelper_JobSetHoldUntil_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(string)) + }) + return _c +} + +func (_c *MockPkHelper_JobSetHoldUntil_Call) Return(_a0 error) *MockPkHelper_JobSetHoldUntil_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_JobSetHoldUntil_Call) RunAndReturn(run func(int, string) error) *MockPkHelper_JobSetHoldUntil_Call { + _c.Call.Return(run) + return _c +} + +// PrinterAdd provides a mock function with given fields: name, uri, ppd, info, location +func (_m *MockPkHelper) PrinterAdd(name string, uri string, ppd string, info string, location string) error { + ret := _m.Called(name, uri, ppd, info, location) + + if len(ret) == 0 { + panic("no return value specified for PrinterAdd") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok { + r0 = rf(name, uri, ppd, info, location) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_PrinterAdd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterAdd' +type MockPkHelper_PrinterAdd_Call struct { + *mock.Call +} + +// PrinterAdd is a helper method to define mock.On call +// - name string +// - uri string +// - ppd string +// - info string +// - location string +func (_e *MockPkHelper_Expecter) PrinterAdd(name interface{}, uri interface{}, ppd interface{}, info interface{}, location interface{}) *MockPkHelper_PrinterAdd_Call { + return &MockPkHelper_PrinterAdd_Call{Call: _e.mock.On("PrinterAdd", name, uri, ppd, info, location)} +} + +func (_c *MockPkHelper_PrinterAdd_Call) Run(run func(name string, uri string, ppd string, info string, location string)) *MockPkHelper_PrinterAdd_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockPkHelper_PrinterAdd_Call) Return(_a0 error) *MockPkHelper_PrinterAdd_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_PrinterAdd_Call) RunAndReturn(run func(string, string, string, string, string) error) *MockPkHelper_PrinterAdd_Call { + _c.Call.Return(run) + return _c +} + +// PrinterDelete provides a mock function with given fields: name +func (_m *MockPkHelper) PrinterDelete(name string) error { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for PrinterDelete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_PrinterDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterDelete' +type MockPkHelper_PrinterDelete_Call struct { + *mock.Call +} + +// PrinterDelete is a helper method to define mock.On call +// - name string +func (_e *MockPkHelper_Expecter) PrinterDelete(name interface{}) *MockPkHelper_PrinterDelete_Call { + return &MockPkHelper_PrinterDelete_Call{Call: _e.mock.On("PrinterDelete", name)} +} + +func (_c *MockPkHelper_PrinterDelete_Call) Run(run func(name string)) *MockPkHelper_PrinterDelete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockPkHelper_PrinterDelete_Call) Return(_a0 error) *MockPkHelper_PrinterDelete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_PrinterDelete_Call) RunAndReturn(run func(string) error) *MockPkHelper_PrinterDelete_Call { + _c.Call.Return(run) + return _c +} + +// PrinterSetAcceptJobs provides a mock function with given fields: name, enabled, reason +func (_m *MockPkHelper) PrinterSetAcceptJobs(name string, enabled bool, reason string) error { + ret := _m.Called(name, enabled, reason) + + if len(ret) == 0 { + panic("no return value specified for PrinterSetAcceptJobs") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, bool, string) error); ok { + r0 = rf(name, enabled, reason) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_PrinterSetAcceptJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetAcceptJobs' +type MockPkHelper_PrinterSetAcceptJobs_Call struct { + *mock.Call +} + +// PrinterSetAcceptJobs is a helper method to define mock.On call +// - name string +// - enabled bool +// - reason string +func (_e *MockPkHelper_Expecter) PrinterSetAcceptJobs(name interface{}, enabled interface{}, reason interface{}) *MockPkHelper_PrinterSetAcceptJobs_Call { + return &MockPkHelper_PrinterSetAcceptJobs_Call{Call: _e.mock.On("PrinterSetAcceptJobs", name, enabled, reason)} +} + +func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) Run(run func(name string, enabled bool, reason string)) *MockPkHelper_PrinterSetAcceptJobs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool), args[2].(string)) + }) + return _c +} + +func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) Return(_a0 error) *MockPkHelper_PrinterSetAcceptJobs_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_PrinterSetAcceptJobs_Call) RunAndReturn(run func(string, bool, string) error) *MockPkHelper_PrinterSetAcceptJobs_Call { + _c.Call.Return(run) + return _c +} + +// PrinterSetEnabled provides a mock function with given fields: name, enabled +func (_m *MockPkHelper) PrinterSetEnabled(name string, enabled bool) error { + ret := _m.Called(name, enabled) + + if len(ret) == 0 { + panic("no return value specified for PrinterSetEnabled") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, bool) error); ok { + r0 = rf(name, enabled) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_PrinterSetEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetEnabled' +type MockPkHelper_PrinterSetEnabled_Call struct { + *mock.Call +} + +// PrinterSetEnabled is a helper method to define mock.On call +// - name string +// - enabled bool +func (_e *MockPkHelper_Expecter) PrinterSetEnabled(name interface{}, enabled interface{}) *MockPkHelper_PrinterSetEnabled_Call { + return &MockPkHelper_PrinterSetEnabled_Call{Call: _e.mock.On("PrinterSetEnabled", name, enabled)} +} + +func (_c *MockPkHelper_PrinterSetEnabled_Call) Run(run func(name string, enabled bool)) *MockPkHelper_PrinterSetEnabled_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *MockPkHelper_PrinterSetEnabled_Call) Return(_a0 error) *MockPkHelper_PrinterSetEnabled_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_PrinterSetEnabled_Call) RunAndReturn(run func(string, bool) error) *MockPkHelper_PrinterSetEnabled_Call { + _c.Call.Return(run) + return _c +} + +// PrinterSetInfo provides a mock function with given fields: name, info +func (_m *MockPkHelper) PrinterSetInfo(name string, info string) error { + ret := _m.Called(name, info) + + if len(ret) == 0 { + panic("no return value specified for PrinterSetInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(name, info) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_PrinterSetInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetInfo' +type MockPkHelper_PrinterSetInfo_Call struct { + *mock.Call +} + +// PrinterSetInfo is a helper method to define mock.On call +// - name string +// - info string +func (_e *MockPkHelper_Expecter) PrinterSetInfo(name interface{}, info interface{}) *MockPkHelper_PrinterSetInfo_Call { + return &MockPkHelper_PrinterSetInfo_Call{Call: _e.mock.On("PrinterSetInfo", name, info)} +} + +func (_c *MockPkHelper_PrinterSetInfo_Call) Run(run func(name string, info string)) *MockPkHelper_PrinterSetInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockPkHelper_PrinterSetInfo_Call) Return(_a0 error) *MockPkHelper_PrinterSetInfo_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_PrinterSetInfo_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_PrinterSetInfo_Call { + _c.Call.Return(run) + return _c +} + +// PrinterSetLocation provides a mock function with given fields: name, location +func (_m *MockPkHelper) PrinterSetLocation(name string, location string) error { + ret := _m.Called(name, location) + + if len(ret) == 0 { + panic("no return value specified for PrinterSetLocation") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(name, location) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_PrinterSetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetLocation' +type MockPkHelper_PrinterSetLocation_Call struct { + *mock.Call +} + +// PrinterSetLocation is a helper method to define mock.On call +// - name string +// - location string +func (_e *MockPkHelper_Expecter) PrinterSetLocation(name interface{}, location interface{}) *MockPkHelper_PrinterSetLocation_Call { + return &MockPkHelper_PrinterSetLocation_Call{Call: _e.mock.On("PrinterSetLocation", name, location)} +} + +func (_c *MockPkHelper_PrinterSetLocation_Call) Run(run func(name string, location string)) *MockPkHelper_PrinterSetLocation_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockPkHelper_PrinterSetLocation_Call) Return(_a0 error) *MockPkHelper_PrinterSetLocation_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_PrinterSetLocation_Call) RunAndReturn(run func(string, string) error) *MockPkHelper_PrinterSetLocation_Call { + _c.Call.Return(run) + return _c +} + +// PrinterSetShared provides a mock function with given fields: name, shared +func (_m *MockPkHelper) PrinterSetShared(name string, shared bool) error { + ret := _m.Called(name, shared) + + if len(ret) == 0 { + panic("no return value specified for PrinterSetShared") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, bool) error); ok { + r0 = rf(name, shared) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPkHelper_PrinterSetShared_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrinterSetShared' +type MockPkHelper_PrinterSetShared_Call struct { + *mock.Call +} + +// PrinterSetShared is a helper method to define mock.On call +// - name string +// - shared bool +func (_e *MockPkHelper_Expecter) PrinterSetShared(name interface{}, shared interface{}) *MockPkHelper_PrinterSetShared_Call { + return &MockPkHelper_PrinterSetShared_Call{Call: _e.mock.On("PrinterSetShared", name, shared)} +} + +func (_c *MockPkHelper_PrinterSetShared_Call) Run(run func(name string, shared bool)) *MockPkHelper_PrinterSetShared_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *MockPkHelper_PrinterSetShared_Call) Return(_a0 error) *MockPkHelper_PrinterSetShared_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPkHelper_PrinterSetShared_Call) RunAndReturn(run func(string, bool) error) *MockPkHelper_PrinterSetShared_Call { + _c.Call.Return(run) + return _c +} + +// NewMockPkHelper creates a new instance of MockPkHelper. 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 NewMockPkHelper(t interface { + mock.TestingT + Cleanup(func()) +}) *MockPkHelper { + mock := &MockPkHelper{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/internal/server/cups/actions.go b/core/internal/server/cups/actions.go index 6c204051..a3296eea 100644 --- a/core/internal/server/cups/actions.go +++ b/core/internal/server/cups/actions.go @@ -1,12 +1,34 @@ package cups import ( + "errors" "strings" "time" + "github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" ) +func isAuthError(err error) bool { + if err == nil { + return false + } + + var httpErr ipp.HTTPError + if errors.As(err, &httpErr) { + return httpErr.Code == 401 || httpErr.Code == 403 + } + + var ippErr ipp.IPPError + if errors.As(err, &ippErr) { + return ippErr.Status == ipp.StatusErrorForbidden || + ippErr.Status == ipp.StatusErrorNotAuthenticated || + ippErr.Status == ipp.StatusErrorNotAuthorized + } + + return false +} + func (m *Manager) GetPrinters() ([]Printer, error) { attributes := []string{ ipp.AttributePrinterName, @@ -21,6 +43,9 @@ func (m *Manager) GetPrinters() ([]Printer, error) { printerAttrs, err := m.client.GetPrinters(attributes) if err != nil { + if isNoPrintersError(err) { + return []Printer{}, nil + } return nil, err } @@ -91,17 +116,289 @@ func (m *Manager) GetJobs(printerName string, whichJobs string) ([]Job, error) { } func (m *Manager) CancelJob(jobID int) error { - return m.client.CancelJob(jobID, false) + err := m.client.CancelJob(jobID, false) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.JobCancelPurge(jobID, false) + } + if err == nil { + m.RefreshState() + } + return err } func (m *Manager) PausePrinter(printerName string) error { - return m.client.PausePrinter(printerName) + err := m.client.PausePrinter(printerName) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterSetEnabled(printerName, false) + } + if err == nil { + m.RefreshState() + } + return err } func (m *Manager) ResumePrinter(printerName string) error { - return m.client.ResumePrinter(printerName) + err := m.client.ResumePrinter(printerName) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterSetEnabled(printerName, true) + } + if err == nil { + m.RefreshState() + } + return err } func (m *Manager) PurgeJobs(printerName string) error { - return m.client.CancelAllJob(printerName, true) + err := m.client.CancelAllJob(printerName, true) + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) GetDevices() ([]Device, error) { + if m.pkHelper != nil { + return m.pkHelper.DevicesGet(10, 0, nil, nil) + } + + deviceAttrs, err := m.client.GetDevices() + if err != nil { + return nil, err + } + + devices := make([]Device, 0, len(deviceAttrs)) + for uri, attrs := range deviceAttrs { + device := Device{ + URI: uri, + Class: getStringAttr(attrs, "device-class"), + Info: getStringAttr(attrs, "device-info"), + MakeModel: getStringAttr(attrs, "device-make-and-model"), + ID: getStringAttr(attrs, "device-id"), + Location: getStringAttr(attrs, "device-location"), + } + devices = append(devices, device) + } + + return devices, nil +} + +func (m *Manager) GetPPDs() ([]PPD, error) { + ppdAttrs, err := m.client.GetPPDs() + if err != nil { + return nil, err + } + + ppds := make([]PPD, 0, len(ppdAttrs)) + for name, attrs := range ppdAttrs { + ppd := PPD{ + Name: name, + NaturalLanguage: getStringAttr(attrs, "ppd-natural-language"), + MakeModel: getStringAttr(attrs, ipp.AttributePPDMakeAndModel), + DeviceID: getStringAttr(attrs, "ppd-device-id"), + Product: getStringAttr(attrs, "ppd-product"), + PSVersion: getStringAttr(attrs, "ppd-psversion"), + Type: getStringAttr(attrs, "ppd-type"), + } + ppds = append(ppds, ppd) + } + + return ppds, nil +} + +func (m *Manager) GetClasses() ([]PrinterClass, error) { + attributes := []string{ + ipp.AttributePrinterName, + ipp.AttributePrinterUriSupported, + ipp.AttributePrinterState, + ipp.AttributeMemberURIs, + ipp.AttributeMemberNames, + ipp.AttributePrinterLocation, + ipp.AttributePrinterInfo, + } + + classAttrs, err := m.client.GetClasses(attributes) + if err != nil { + return nil, err + } + + classes := make([]PrinterClass, 0, len(classAttrs)) + for _, attrs := range classAttrs { + class := PrinterClass{ + Name: getStringAttr(attrs, ipp.AttributePrinterName), + URI: getStringAttr(attrs, ipp.AttributePrinterUriSupported), + State: parsePrinterState(attrs), + Location: getStringAttr(attrs, ipp.AttributePrinterLocation), + Info: getStringAttr(attrs, ipp.AttributePrinterInfo), + Members: getStringSliceAttr(attrs, ipp.AttributeMemberNames), + } + classes = append(classes, class) + } + + return classes, nil +} + +func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error { + usedPkHelper := false + + err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location) + if isAuthError(err) && m.pkHelper != nil { + if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil { + return err + } + usedPkHelper = true + } else if err != nil { + return err + } + + if usedPkHelper { + m.pkHelper.PrinterSetEnabled(name, true) + m.pkHelper.PrinterSetAcceptJobs(name, true, "") + } else { + if err := m.client.ResumePrinter(name); isAuthError(err) && m.pkHelper != nil { + m.pkHelper.PrinterSetEnabled(name, true) + } + if err := m.client.AcceptJobs(name); isAuthError(err) && m.pkHelper != nil { + m.pkHelper.PrinterSetAcceptJobs(name, true, "") + } + } + + m.RefreshState() + return nil +} + +func (m *Manager) DeletePrinter(printerName string) error { + err := m.client.DeletePrinter(printerName) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterDelete(printerName) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) AcceptJobs(printerName string) error { + err := m.client.AcceptJobs(printerName) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterSetAcceptJobs(printerName, true, "") + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) RejectJobs(printerName string) error { + err := m.client.RejectJobs(printerName) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterSetAcceptJobs(printerName, false, "") + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) SetPrinterShared(printerName string, shared bool) error { + err := m.client.SetPrinterIsShared(printerName, shared) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterSetShared(printerName, shared) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) SetPrinterLocation(printerName, location string) error { + err := m.client.SetPrinterLocation(printerName, location) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterSetLocation(printerName, location) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) SetPrinterInfo(printerName, info string) error { + err := m.client.SetPrinterInformation(printerName, info) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.PrinterSetInfo(printerName, info) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) MoveJob(jobID int, destPrinter string) error { + err := m.client.MoveJob(jobID, destPrinter) + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) PrintTestPage(printerName string) (int, error) { + jobID, err := m.client.PrintTestPage(printerName, strings.NewReader(config.TestPage), len(config.TestPage)) + if err == nil { + m.RefreshState() + } + return jobID, err +} + +func (m *Manager) AddPrinterToClass(className, printerName string) error { + err := m.client.AddPrinterToClass(className, printerName) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.ClassAddPrinter(className, printerName) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) RemovePrinterFromClass(className, printerName string) error { + err := m.client.DeletePrinterFromClass(className, printerName) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.ClassDeletePrinter(className, printerName) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) DeleteClass(className string) error { + err := m.client.DeleteClass(className) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.ClassDelete(className) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) RestartJob(jobID int) error { + err := m.client.RestartJob(jobID) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.JobRestart(jobID) + } + if err == nil { + m.RefreshState() + } + return err +} + +func (m *Manager) HoldJob(jobID int, holdUntil string) error { + err := m.client.HoldJobUntil(jobID, holdUntil) + if isAuthError(err) && m.pkHelper != nil { + err = m.pkHelper.JobSetHoldUntil(jobID, holdUntil) + } + if err == nil { + m.RefreshState() + } + return err } diff --git a/core/internal/server/cups/actions_pkhelper_test.go b/core/internal/server/cups/actions_pkhelper_test.go new file mode 100644 index 00000000..ad16a8a0 --- /dev/null +++ b/core/internal/server/cups/actions_pkhelper_test.go @@ -0,0 +1,235 @@ +package cups_test + +import ( + "testing" + + mocks_cups "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups" + mocks_pkhelper "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/cups_pkhelper" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" + "github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func authErr() error { + return ipp.IPPError{Status: ipp.StatusErrorForbidden} +} + +func TestManager_CancelJob_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().CancelJob(1, false).Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().JobCancelPurge(1, false).Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.CancelJob(1)) +} + +func TestManager_CancelJob_PkHelperError(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().CancelJob(1, false).Return(authErr()) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().JobCancelPurge(1, false).Return(assert.AnError) + + m := cups.NewTestManager(mockClient, mockPk) + assert.Error(t, m.CancelJob(1)) +} + +func TestManager_PausePrinter_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().PausePrinter("printer1").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterSetEnabled("printer1", false).Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.PausePrinter("printer1")) +} + +func TestManager_ResumePrinter_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().ResumePrinter("printer1").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterSetEnabled("printer1", true).Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.ResumePrinter("printer1")) +} + +func TestManager_GetDevices_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().DevicesGet(10, 0, []string(nil), []string(nil)).Return([]cups.Device{ + {URI: "usb://HP/LaserJet", Class: "direct"}, + }, nil) + + m := cups.NewTestManager(mockClient, mockPk) + got, err := m.GetDevices() + assert.NoError(t, err) + assert.Len(t, got, 1) + assert.Equal(t, "usb://HP/LaserJet", got[0].URI) +} + +func TestManager_GetDevices_PkHelperError(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().DevicesGet(10, 0, []string(nil), []string(nil)).Return(nil, assert.AnError) + + m := cups.NewTestManager(mockClient, mockPk) + _, err := m.GetDevices() + assert.Error(t, err) +} + +func TestManager_CreatePrinter_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterAdd("newprinter", "usb://HP", "generic.ppd", "info", "location").Return(nil) + mockPk.EXPECT().PrinterSetEnabled("newprinter", true).Return(nil) + mockPk.EXPECT().PrinterSetAcceptJobs("newprinter", true, "").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location")) +} + +func TestManager_DeletePrinter_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeletePrinter("printer1").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterDelete("printer1").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.DeletePrinter("printer1")) +} + +func TestManager_AcceptJobs_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().AcceptJobs("printer1").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterSetAcceptJobs("printer1", true, "").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.AcceptJobs("printer1")) +} + +func TestManager_RejectJobs_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().RejectJobs("printer1").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterSetAcceptJobs("printer1", false, "").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.RejectJobs("printer1")) +} + +func TestManager_SetPrinterShared_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterSetShared("printer1", true).Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.SetPrinterShared("printer1", true)) +} + +func TestManager_SetPrinterLocation_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterSetLocation("printer1", "Office").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.SetPrinterLocation("printer1", "Office")) +} + +func TestManager_SetPrinterInfo_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().PrinterSetInfo("printer1", "Main Printer").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.SetPrinterInfo("printer1", "Main Printer")) +} + +func TestManager_AddPrinterToClass_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().ClassAddPrinter("office", "printer1").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.AddPrinterToClass("office", "printer1")) +} + +func TestManager_RemovePrinterFromClass_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().ClassDeletePrinter("office", "printer1").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.RemovePrinterFromClass("office", "printer1")) +} + +func TestManager_DeleteClass_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeleteClass("office").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().ClassDelete("office").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.DeleteClass("office")) +} + +func TestManager_RestartJob_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().RestartJob(1).Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().JobRestart(1).Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.RestartJob(1)) +} + +func TestManager_HoldJob_WithPkHelper(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(authErr()) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + mockPk := mocks_pkhelper.NewMockPkHelper(t) + mockPk.EXPECT().JobSetHoldUntil(1, "indefinite").Return(nil) + + m := cups.NewTestManager(mockClient, mockPk) + assert.NoError(t, m.HoldJob(1, "indefinite")) +} diff --git a/core/internal/server/cups/actions_test.go b/core/internal/server/cups/actions_test.go index 133ee9c2..70f4f218 100644 --- a/core/internal/server/cups/actions_test.go +++ b/core/internal/server/cups/actions_test.go @@ -137,114 +137,30 @@ func TestManager_GetJobs(t *testing.T) { } func TestManager_CancelJob(t *testing.T) { - tests := []struct { - name string - mockErr error - wantErr bool - }{ - { - name: "success", - mockErr: nil, - wantErr: false, - }, - { - name: "error", - mockErr: errors.New("test error"), - wantErr: true, - }, - } + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().CancelJob(1, false).Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().CancelJob(1, false).Return(tt.mockErr) - - m := &Manager{ - client: mockClient, - } - - err := m.CancelJob(1) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.CancelJob(1)) } func TestManager_PausePrinter(t *testing.T) { - tests := []struct { - name string - mockErr error - wantErr bool - }{ - { - name: "success", - mockErr: nil, - wantErr: false, - }, - { - name: "error", - mockErr: errors.New("test error"), - wantErr: true, - }, - } + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().PausePrinter("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().PausePrinter("printer1").Return(tt.mockErr) - - m := &Manager{ - client: mockClient, - } - - err := m.PausePrinter("printer1") - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.PausePrinter("printer1")) } func TestManager_ResumePrinter(t *testing.T) { - tests := []struct { - name string - mockErr error - wantErr bool - }{ - { - name: "success", - mockErr: nil, - wantErr: false, - }, - { - name: "error", - mockErr: errors.New("test error"), - wantErr: true, - }, - } + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().ResumePrinter("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockClient := mocks_cups.NewMockCUPSClientInterface(t) - mockClient.EXPECT().ResumePrinter("printer1").Return(tt.mockErr) - - m := &Manager{ - client: mockClient, - } - - err := m.ResumePrinter("printer1") - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.ResumePrinter("printer1")) } func TestManager_PurgeJobs(t *testing.T) { @@ -269,11 +185,12 @@ func TestManager_PurgeJobs(t *testing.T) { t.Run(tt.name, func(t *testing.T) { mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient.EXPECT().CancelAllJob("printer1", true).Return(tt.mockErr) - - m := &Manager{ - client: mockClient, + if !tt.wantErr { + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) } + m := NewTestManager(mockClient, nil) + err := m.PurgeJobs("printer1") if tt.wantErr { assert.Error(t, err) @@ -283,3 +200,251 @@ func TestManager_PurgeJobs(t *testing.T) { }) } } + +func TestManager_GetDevices(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().GetDevices().Return(map[string]ipp.Attributes{ + "usb://HP/LaserJet": { + "device-class": []ipp.Attribute{{Value: "direct"}}, + "device-info": []ipp.Attribute{{Value: "HP LaserJet"}}, + "device-make-and-model": []ipp.Attribute{{Value: "HP LaserJet 1020"}}, + }, + }, nil) + + m := &Manager{client: mockClient} + got, err := m.GetDevices() + assert.NoError(t, err) + assert.Len(t, got, 1) + assert.Equal(t, "usb://HP/LaserJet", got[0].URI) + assert.Equal(t, "direct", got[0].Class) +} + +func TestManager_GetPPDs(t *testing.T) { + tests := []struct { + name string + mockRet map[string]ipp.Attributes + mockErr error + want int + wantErr bool + }{ + { + name: "success", + mockRet: map[string]ipp.Attributes{ + "drv:///sample.drv/generic.ppd": { + "ppd-make-and-model": []ipp.Attribute{{Value: "Generic PostScript"}}, + "ppd-type": []ipp.Attribute{{Value: "ppd"}}, + }, + }, + mockErr: nil, + want: 1, + wantErr: false, + }, + { + name: "error", + mockRet: nil, + mockErr: errors.New("test error"), + want: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().GetPPDs().Return(tt.mockRet, tt.mockErr) + + m := &Manager{client: mockClient} + + got, err := m.GetPPDs() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, len(got)) + }) + } +} + +func TestManager_GetClasses(t *testing.T) { + tests := []struct { + name string + mockRet map[string]ipp.Attributes + mockErr error + want int + wantErr bool + }{ + { + name: "success", + mockRet: map[string]ipp.Attributes{ + "office": { + ipp.AttributePrinterName: []ipp.Attribute{{Value: "office"}}, + ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}}, + ipp.AttributeMemberNames: []ipp.Attribute{{Value: "printer1"}, {Value: "printer2"}}, + }, + }, + mockErr: nil, + want: 1, + wantErr: false, + }, + { + name: "error", + mockRet: nil, + mockErr: errors.New("test error"), + want: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().GetClasses(mock.Anything).Return(tt.mockRet, tt.mockErr) + + m := &Manager{client: mockClient} + + got, err := m.GetClasses() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, len(got)) + if len(got) > 0 { + assert.Equal(t, "office", got[0].Name) + assert.Equal(t, 2, len(got[0].Members)) + } + }) + } +} + +func TestManager_CreatePrinter(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location").Return(nil) + mockClient.EXPECT().ResumePrinter("newprinter").Return(nil) + mockClient.EXPECT().AcceptJobs("newprinter").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.CreatePrinter("newprinter", "usb://HP", "generic.ppd", true, "stop-printer", "info", "location")) +} + +func TestManager_DeletePrinter(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeletePrinter("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.DeletePrinter("printer1")) +} + +func TestManager_AcceptJobs(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().AcceptJobs("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.AcceptJobs("printer1")) +} + +func TestManager_RejectJobs(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().RejectJobs("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.RejectJobs("printer1")) +} + +func TestManager_SetPrinterShared(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.SetPrinterShared("printer1", true)) +} + +func TestManager_SetPrinterLocation(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.SetPrinterLocation("printer1", "Office")) +} + +func TestManager_SetPrinterInfo(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.SetPrinterInfo("printer1", "Main Printer")) +} + +func TestManager_MoveJob(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().MoveJob(1, "printer2").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + err := m.MoveJob(1, "printer2") + assert.NoError(t, err) +} + +func TestManager_PrintTestPage(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().PrintTestPage("printer1", mock.Anything, mock.Anything).Return(42, nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + jobID, err := m.PrintTestPage("printer1") + assert.NoError(t, err) + assert.Equal(t, 42, jobID) +} + +func TestManager_AddPrinterToClass(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.AddPrinterToClass("office", "printer1")) +} + +func TestManager_RemovePrinterFromClass(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.RemovePrinterFromClass("office", "printer1")) +} + +func TestManager_DeleteClass(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeleteClass("office").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.DeleteClass("office")) +} + +func TestManager_RestartJob(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().RestartJob(1).Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.RestartJob(1)) +} + +func TestManager_HoldJob(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + assert.NoError(t, m.HoldJob(1, "indefinite")) +} diff --git a/core/internal/server/cups/handlers.go b/core/internal/server/cups/handlers.go index 347e22a7..5e216a8d 100644 --- a/core/internal/server/cups/handlers.go +++ b/core/internal/server/cups/handlers.go @@ -40,6 +40,40 @@ func HandleRequest(conn net.Conn, req Request, manager *Manager) { handleCancelJob(conn, req, manager) case "cups.purgeJobs": handlePurgeJobs(conn, req, manager) + case "cups.getDevices": + handleGetDevices(conn, req, manager) + case "cups.getPPDs": + handleGetPPDs(conn, req, manager) + case "cups.getClasses": + handleGetClasses(conn, req, manager) + case "cups.createPrinter": + handleCreatePrinter(conn, req, manager) + case "cups.deletePrinter": + handleDeletePrinter(conn, req, manager) + case "cups.acceptJobs": + handleAcceptJobs(conn, req, manager) + case "cups.rejectJobs": + handleRejectJobs(conn, req, manager) + case "cups.setPrinterShared": + handleSetPrinterShared(conn, req, manager) + case "cups.setPrinterLocation": + handleSetPrinterLocation(conn, req, manager) + case "cups.setPrinterInfo": + handleSetPrinterInfo(conn, req, manager) + case "cups.moveJob": + handleMoveJob(conn, req, manager) + case "cups.printTestPage": + handlePrintTestPage(conn, req, manager) + case "cups.addPrinterToClass": + handleAddPrinterToClass(conn, req, manager) + case "cups.removePrinterFromClass": + handleRemovePrinterFromClass(conn, req, manager) + case "cups.deleteClass": + handleDeleteClass(conn, req, manager) + case "cups.restartJob": + handleRestartJob(conn, req, manager) + case "cups.holdJob": + handleHoldJob(conn, req, manager) default: models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) } @@ -158,3 +192,291 @@ func handleSubscribe(conn net.Conn, req Request, manager *Manager) { } } } + +func handleGetDevices(conn net.Conn, req Request, manager *Manager) { + devices, err := manager.GetDevices() + if err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, devices) +} + +func handleGetPPDs(conn net.Conn, req Request, manager *Manager) { + ppds, err := manager.GetPPDs() + if err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, ppds) +} + +func handleGetClasses(conn net.Conn, req Request, manager *Manager) { + classes, err := manager.GetClasses() + if err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, classes) +} + +func handleCreatePrinter(conn net.Conn, req Request, manager *Manager) { + name, ok := req.Params["name"].(string) + if !ok || name == "" { + models.RespondError(conn, req.ID, "missing or invalid 'name' parameter") + return + } + + deviceURI, ok := req.Params["deviceURI"].(string) + if !ok || deviceURI == "" { + models.RespondError(conn, req.ID, "missing or invalid 'deviceURI' parameter") + return + } + + ppd, ok := req.Params["ppd"].(string) + if !ok || ppd == "" { + models.RespondError(conn, req.ID, "missing or invalid 'ppd' parameter") + return + } + + shared, _ := req.Params["shared"].(bool) + errorPolicy, _ := req.Params["errorPolicy"].(string) + information, _ := req.Params["information"].(string) + location, _ := req.Params["location"].(string) + + if err := manager.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer created"}) +} + +func handleDeletePrinter(conn net.Conn, req Request, manager *Manager) { + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + if err := manager.DeletePrinter(printerName); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer deleted"}) +} + +func handleAcceptJobs(conn net.Conn, req Request, manager *Manager) { + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + if err := manager.AcceptJobs(printerName); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "accepting jobs"}) +} + +func handleRejectJobs(conn net.Conn, req Request, manager *Manager) { + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + if err := manager.RejectJobs(printerName); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "rejecting jobs"}) +} + +func handleSetPrinterShared(conn net.Conn, req Request, manager *Manager) { + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + shared, ok := req.Params["shared"].(bool) + if !ok { + models.RespondError(conn, req.ID, "missing or invalid 'shared' parameter") + return + } + + if err := manager.SetPrinterShared(printerName, shared); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "sharing updated"}) +} + +func handleSetPrinterLocation(conn net.Conn, req Request, manager *Manager) { + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + location, ok := req.Params["location"].(string) + if !ok { + models.RespondError(conn, req.ID, "missing or invalid 'location' parameter") + return + } + + if err := manager.SetPrinterLocation(printerName, location); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location updated"}) +} + +func handleSetPrinterInfo(conn net.Conn, req Request, manager *Manager) { + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + info, ok := req.Params["info"].(string) + if !ok { + models.RespondError(conn, req.ID, "missing or invalid 'info' parameter") + return + } + + if err := manager.SetPrinterInfo(printerName, info); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "info updated"}) +} + +func handleMoveJob(conn net.Conn, req Request, manager *Manager) { + jobIDFloat, ok := req.Params["jobID"].(float64) + if !ok { + models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter") + return + } + + destPrinter, ok := req.Params["destPrinter"].(string) + if !ok || destPrinter == "" { + models.RespondError(conn, req.ID, "missing or invalid 'destPrinter' parameter") + return + } + + if err := manager.MoveJob(int(jobIDFloat), destPrinter); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job moved"}) +} + +type TestPageResult struct { + Success bool `json:"success"` + JobID int `json:"jobId"` + Message string `json:"message"` +} + +func handlePrintTestPage(conn net.Conn, req Request, manager *Manager) { + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + jobID, err := manager.PrintTestPage(printerName) + if err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, TestPageResult{Success: true, JobID: jobID, Message: "test page queued"}) +} + +func handleAddPrinterToClass(conn net.Conn, req Request, manager *Manager) { + className, ok := req.Params["className"].(string) + if !ok || className == "" { + models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") + return + } + + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + if err := manager.AddPrinterToClass(className, printerName); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer added to class"}) +} + +func handleRemovePrinterFromClass(conn net.Conn, req Request, manager *Manager) { + className, ok := req.Params["className"].(string) + if !ok || className == "" { + models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") + return + } + + printerName, ok := req.Params["printerName"].(string) + if !ok || printerName == "" { + models.RespondError(conn, req.ID, "missing or invalid 'printerName' parameter") + return + } + + if err := manager.RemovePrinterFromClass(className, printerName); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "printer removed from class"}) +} + +func handleDeleteClass(conn net.Conn, req Request, manager *Manager) { + className, ok := req.Params["className"].(string) + if !ok || className == "" { + models.RespondError(conn, req.ID, "missing or invalid 'className' parameter") + return + } + + if err := manager.DeleteClass(className); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "class deleted"}) +} + +func handleRestartJob(conn net.Conn, req Request, manager *Manager) { + jobIDFloat, ok := req.Params["jobID"].(float64) + if !ok { + models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter") + return + } + + if err := manager.RestartJob(int(jobIDFloat)); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job restarted"}) +} + +func handleHoldJob(conn net.Conn, req Request, manager *Manager) { + jobIDFloat, ok := req.Params["jobID"].(float64) + if !ok { + models.RespondError(conn, req.ID, "missing or invalid 'jobID' parameter") + return + } + + holdUntil, _ := req.Params["holdUntil"].(string) + if holdUntil == "" { + holdUntil = "indefinite" + } + + if err := manager.HoldJob(int(jobIDFloat), holdUntil); err != nil { + models.RespondError(conn, req.ID, err.Error()) + return + } + models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "job held"}) +} diff --git a/core/internal/server/cups/handlers_test.go b/core/internal/server/cups/handlers_test.go index d2f57848..797bc313 100644 --- a/core/internal/server/cups/handlers_test.go +++ b/core/internal/server/cups/handlers_test.go @@ -145,10 +145,9 @@ func TestHandleGetJobs_MissingParam(t *testing.T) { func TestHandlePausePrinter(t *testing.T) { mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient.EXPECT().PausePrinter("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - m := &Manager{ - client: mockClient, - } + m := NewTestManager(mockClient, nil) buf := &bytes.Buffer{} conn := &mockConn{Buffer: buf} @@ -173,10 +172,9 @@ func TestHandlePausePrinter(t *testing.T) { func TestHandleResumePrinter(t *testing.T) { mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient.EXPECT().ResumePrinter("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - m := &Manager{ - client: mockClient, - } + m := NewTestManager(mockClient, nil) buf := &bytes.Buffer{} conn := &mockConn{Buffer: buf} @@ -201,10 +199,9 @@ func TestHandleResumePrinter(t *testing.T) { func TestHandleCancelJob(t *testing.T) { mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient.EXPECT().CancelJob(1, false).Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - m := &Manager{ - client: mockClient, - } + m := NewTestManager(mockClient, nil) buf := &bytes.Buffer{} conn := &mockConn{Buffer: buf} @@ -229,10 +226,9 @@ func TestHandleCancelJob(t *testing.T) { func TestHandlePurgeJobs(t *testing.T) { mockClient := mocks_cups.NewMockCUPSClientInterface(t) mockClient.EXPECT().CancelAllJob("printer1", true).Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) - m := &Manager{ - client: mockClient, - } + m := NewTestManager(mockClient, nil) buf := &bytes.Buffer{} conn := &mockConn{Buffer: buf} @@ -277,3 +273,439 @@ func TestHandleRequest_UnknownMethod(t *testing.T) { assert.Nil(t, resp.Result) assert.NotNil(t, resp.Error) } + +func TestHandleGetDevices(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().GetDevices().Return(map[string]ipp.Attributes{ + "usb://HP/LaserJet": { + "device-class": []ipp.Attribute{{Value: "direct"}}, + "device-info": []ipp.Attribute{{Value: "HP LaserJet"}}, + }, + }, nil) + + m := &Manager{client: mockClient} + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ID: 1, Method: "cups.getDevices"} + handleGetDevices(conn, req, m) + + var resp models.Response[[]Device] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.Equal(t, 1, len(*resp.Result)) +} + +func TestHandleGetPPDs(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().GetPPDs().Return(map[string]ipp.Attributes{ + "generic.ppd": { + "ppd-make-and-model": []ipp.Attribute{{Value: "Generic"}}, + }, + }, nil) + + m := &Manager{client: mockClient} + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ID: 1, Method: "cups.getPPDs"} + handleGetPPDs(conn, req, m) + + var resp models.Response[[]PPD] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.Equal(t, 1, len(*resp.Result)) +} + +func TestHandleGetClasses(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().GetClasses(mock.Anything).Return(map[string]ipp.Attributes{ + "office": { + ipp.AttributePrinterName: []ipp.Attribute{{Value: "office"}}, + ipp.AttributePrinterState: []ipp.Attribute{{Value: 3}}, + }, + }, nil) + + m := &Manager{client: mockClient} + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ID: 1, Method: "cups.getClasses"} + handleGetClasses(conn, req, m) + + var resp models.Response[[]PrinterClass] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.Equal(t, 1, len(*resp.Result)) +} + +func TestHandleCreatePrinter(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().CreatePrinter("newprinter", "usb://HP", "generic.ppd", false, "", "", "").Return(nil) + mockClient.EXPECT().ResumePrinter("newprinter").Return(nil) + mockClient.EXPECT().AcceptJobs("newprinter").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.createPrinter", + Params: map[string]interface{}{ + "name": "newprinter", + "deviceURI": "usb://HP", + "ppd": "generic.ppd", + }, + } + handleCreatePrinter(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleCreatePrinter_MissingParams(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + m := &Manager{client: mockClient} + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]interface{}{}} + handleCreatePrinter(conn, req, m) + + var resp models.Response[interface{}] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.Nil(t, resp.Result) + assert.NotNil(t, resp.Error) +} + +func TestHandleDeletePrinter(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeletePrinter("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.deletePrinter", + Params: map[string]interface{}{"printerName": "printer1"}, + } + handleDeletePrinter(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleAcceptJobs(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().AcceptJobs("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.acceptJobs", + Params: map[string]interface{}{"printerName": "printer1"}, + } + handleAcceptJobs(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleRejectJobs(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().RejectJobs("printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.rejectJobs", + Params: map[string]interface{}{"printerName": "printer1"}, + } + handleRejectJobs(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleSetPrinterShared(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterIsShared("printer1", true).Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.setPrinterShared", + Params: map[string]interface{}{"printerName": "printer1", "shared": true}, + } + handleSetPrinterShared(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleSetPrinterLocation(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterLocation("printer1", "Office").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.setPrinterLocation", + Params: map[string]interface{}{"printerName": "printer1", "location": "Office"}, + } + handleSetPrinterLocation(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleSetPrinterInfo(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().SetPrinterInformation("printer1", "Main Printer").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.setPrinterInfo", + Params: map[string]interface{}{"printerName": "printer1", "info": "Main Printer"}, + } + handleSetPrinterInfo(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleMoveJob(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().MoveJob(1, "printer2").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.moveJob", + Params: map[string]interface{}{"jobID": float64(1), "destPrinter": "printer2"}, + } + handleMoveJob(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandlePrintTestPage(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().PrintTestPage("printer1", mock.Anything, mock.Anything).Return(42, nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.printTestPage", + Params: map[string]interface{}{"printerName": "printer1"}, + } + handlePrintTestPage(conn, req, m) + + var resp models.Response[TestPageResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) + assert.Equal(t, 42, resp.Result.JobID) +} + +func TestHandleAddPrinterToClass(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().AddPrinterToClass("office", "printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.addPrinterToClass", + Params: map[string]interface{}{"className": "office", "printerName": "printer1"}, + } + handleAddPrinterToClass(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleRemovePrinterFromClass(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeletePrinterFromClass("office", "printer1").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.removePrinterFromClass", + Params: map[string]interface{}{"className": "office", "printerName": "printer1"}, + } + handleRemovePrinterFromClass(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleDeleteClass(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().DeleteClass("office").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.deleteClass", + Params: map[string]interface{}{"className": "office"}, + } + handleDeleteClass(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleRestartJob(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().RestartJob(1).Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.restartJob", + Params: map[string]interface{}{"jobID": float64(1)}, + } + handleRestartJob(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleHoldJob(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().HoldJobUntil(1, "indefinite").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.holdJob", + Params: map[string]interface{}{"jobID": float64(1)}, + } + handleHoldJob(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} + +func TestHandleHoldJob_WithHoldUntil(t *testing.T) { + mockClient := mocks_cups.NewMockCUPSClientInterface(t) + mockClient.EXPECT().HoldJobUntil(1, "no-hold").Return(nil) + mockClient.EXPECT().GetPrinters(mock.Anything).Return(map[string]ipp.Attributes{}, nil) + + m := NewTestManager(mockClient, nil) + buf := &bytes.Buffer{} + conn := &mockConn{Buffer: buf} + + req := Request{ + ID: 1, + Method: "cups.holdJob", + Params: map[string]interface{}{"jobID": float64(1), "holdUntil": "no-hold"}, + } + handleHoldJob(conn, req, m) + + var resp models.Response[SuccessResult] + err := json.NewDecoder(buf).Decode(&resp) + assert.NoError(t, err) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Success) +} diff --git a/core/internal/server/cups/manager.go b/core/internal/server/cups/manager.go index b02a3295..e5b7e8c3 100644 --- a/core/internal/server/cups/manager.go +++ b/core/internal/server/cups/manager.go @@ -1,6 +1,7 @@ package cups import ( + "errors" "fmt" "os" "strconv" @@ -31,11 +32,21 @@ func NewManager() (*Manager, error) { client := ipp.NewCUPSClient(host, port, username, password, false) baseURL := fmt.Sprintf("http://%s:%d", host, port) + var pkHelper PkHelper + if isLocalCUPS(host) { + var err error + pkHelper, err = NewPkHelper() + if err != nil { + log.Warnf("[CUPS] Failed to initialize pkhelper: %v", err) + } + } + m := &Manager{ state: &CUPSState{ Printers: make(map[string]*Printer), }, client: client, + pkHelper: pkHelper, baseURL: baseURL, stateMutex: sync.RWMutex{}, stopChan: make(chan struct{}), @@ -98,6 +109,12 @@ func (m *Manager) eventHandler() { func (m *Manager) updateState() error { printers, err := m.GetPrinters() if err != nil { + if isNoPrintersError(err) { + m.stateMutex.Lock() + m.state.Printers = make(map[string]*Printer) + m.stateMutex.Unlock() + return nil + } return err } @@ -119,6 +136,19 @@ func (m *Manager) updateState() error { return nil } +func isNoPrintersError(err error) bool { + if err == nil { + return false + } + + var ippErr ipp.IPPError + if errors.As(err, &ippErr) { + return ippErr.Status == 1030 + } + + return false +} + func (m *Manager) notifier() { defer m.notifierWg.Done() const minGap = 100 * time.Millisecond @@ -170,6 +200,14 @@ func (m *Manager) notifySubscribers() { } } +func (m *Manager) RefreshState() { + if err := m.updateState(); err != nil { + log.Warnf("[CUPS] Failed to refresh state: %v", err) + return + } + m.notifySubscribers() +} + func (m *Manager) GetState() CUPSState { return m.snapshotState() } @@ -256,6 +294,7 @@ func stateChanged(old, new *CUPSState) bool { } if oldPrinter.State != newPrinter.State || oldPrinter.StateReason != newPrinter.StateReason || + oldPrinter.Accepting != newPrinter.Accepting || len(oldPrinter.Jobs) != len(newPrinter.Jobs) { return true } @@ -334,3 +373,18 @@ func getBoolAttr(attrs ipp.Attributes, key string) bool { } return false } + +func getStringSliceAttr(attrs ipp.Attributes, key string) []string { + attr, ok := attrs[key] + if !ok { + return nil + } + + result := make([]string, 0, len(attr)) + for _, a := range attr { + if val, ok := a.Value.(string); ok { + result = append(result, val) + } + } + return result +} diff --git a/core/internal/server/cups/pkhelper.go b/core/internal/server/cups/pkhelper.go new file mode 100644 index 00000000..f4b713f0 --- /dev/null +++ b/core/internal/server/cups/pkhelper.go @@ -0,0 +1,184 @@ +package cups + +import ( + "fmt" + "strings" + + "github.com/godbus/dbus/v5" +) + +const ( + pkHelperDest = "org.opensuse.CupsPkHelper.Mechanism" + pkHelperPath = "/" + pkHelperInterface = "org.opensuse.CupsPkHelper.Mechanism" +) + +type PkHelper interface { + DevicesGet(timeout, limit int, includeSchemes, excludeSchemes []string) ([]Device, error) + PrinterAdd(name, uri, ppd, info, location string) error + PrinterDelete(name string) error + PrinterSetEnabled(name string, enabled bool) error + PrinterSetAcceptJobs(name string, enabled bool, reason string) error + PrinterSetInfo(name, info string) error + PrinterSetLocation(name, location string) error + PrinterSetShared(name string, shared bool) error + ClassAddPrinter(className, printerName string) error + ClassDeletePrinter(className, printerName string) error + ClassDelete(className string) error + JobCancelPurge(jobID int, purge bool) error + JobRestart(jobID int) error + JobSetHoldUntil(jobID int, holdUntil string) error +} + +type DBusPkHelper struct { + conn *dbus.Conn + obj dbus.BusObject +} + +func NewPkHelper() (*DBusPkHelper, error) { + conn, err := dbus.SystemBus() + if err != nil { + return nil, fmt.Errorf("failed to connect to system bus: %w", err) + } + + return &DBusPkHelper{ + conn: conn, + obj: conn.Object(pkHelperDest, pkHelperPath), + }, nil +} + +func (p *DBusPkHelper) DevicesGet(timeout, limit int, includeSchemes, excludeSchemes []string) ([]Device, error) { + if includeSchemes == nil { + includeSchemes = []string{} + } + if excludeSchemes == nil { + excludeSchemes = []string{} + } + + var errStr string + var devicesMap map[string]string + + call := p.obj.Call(pkHelperInterface+".DevicesGet", 0, int32(timeout), int32(limit), includeSchemes, excludeSchemes) + if call.Err != nil { + return nil, call.Err + } + if err := call.Store(&errStr, &devicesMap); err != nil { + return nil, err + } + if errStr != "" { + return nil, fmt.Errorf("%s", errStr) + } + + return parseDevicesMap(devicesMap), nil +} + +func parseDevicesMap(devicesMap map[string]string) []Device { + devicesByIndex := make(map[string]*Device) + + for key, value := range devicesMap { + idx := strings.LastIndex(key, ":") + if idx == -1 { + continue + } + + attr := key[:idx] + index := key[idx+1:] + + dev, ok := devicesByIndex[index] + if !ok { + dev = &Device{} + devicesByIndex[index] = dev + } + + switch attr { + case "device-uri": + dev.URI = value + case "device-class": + dev.Class = value + case "device-info": + dev.Info = value + case "device-make-and-model": + dev.MakeModel = value + case "device-id": + dev.ID = value + case "device-location": + dev.Location = value + } + } + + devices := make([]Device, 0, len(devicesByIndex)) + for _, dev := range devicesByIndex { + if dev.URI != "" { + devices = append(devices, *dev) + } + } + return devices +} + +func (p *DBusPkHelper) PrinterAdd(name, uri, ppd, info, location string) error { + return p.callSimple("PrinterAdd", name, uri, ppd, info, location) +} + +func (p *DBusPkHelper) PrinterDelete(name string) error { + return p.callSimple("PrinterDelete", name) +} + +func (p *DBusPkHelper) PrinterSetEnabled(name string, enabled bool) error { + return p.callSimple("PrinterSetEnabled", name, enabled) +} + +func (p *DBusPkHelper) PrinterSetAcceptJobs(name string, enabled bool, reason string) error { + return p.callSimple("PrinterSetAcceptJobs", name, enabled, reason) +} + +func (p *DBusPkHelper) PrinterSetInfo(name, info string) error { + return p.callSimple("PrinterSetInfo", name, info) +} + +func (p *DBusPkHelper) PrinterSetLocation(name, location string) error { + return p.callSimple("PrinterSetLocation", name, location) +} + +func (p *DBusPkHelper) PrinterSetShared(name string, shared bool) error { + return p.callSimple("PrinterSetShared", name, shared) +} + +func (p *DBusPkHelper) ClassAddPrinter(className, printerName string) error { + return p.callSimple("ClassAddPrinter", className, printerName) +} + +func (p *DBusPkHelper) ClassDeletePrinter(className, printerName string) error { + return p.callSimple("ClassDeletePrinter", className, printerName) +} + +func (p *DBusPkHelper) ClassDelete(className string) error { + return p.callSimple("ClassDelete", className) +} + +func (p *DBusPkHelper) JobCancelPurge(jobID int, purge bool) error { + return p.callSimple("JobCancelPurge", int32(jobID), purge) +} + +func (p *DBusPkHelper) JobRestart(jobID int) error { + return p.callSimple("JobRestart", int32(jobID)) +} + +func (p *DBusPkHelper) JobSetHoldUntil(jobID int, holdUntil string) error { + return p.callSimple("JobSetHoldUntil", int32(jobID), holdUntil) +} + +func (p *DBusPkHelper) callSimple(method string, args ...interface{}) error { + var errStr string + + call := p.obj.Call(pkHelperInterface+"."+method, 0, args...) + if call.Err != nil { + return call.Err + } + if err := call.Store(&errStr); err != nil { + return err + } + if errStr != "" { + return fmt.Errorf("%s", errStr) + } + return nil +} diff --git a/core/internal/server/cups/pkhelper_test.go b/core/internal/server/cups/pkhelper_test.go new file mode 100644 index 00000000..596f1d05 --- /dev/null +++ b/core/internal/server/cups/pkhelper_test.go @@ -0,0 +1,95 @@ +package cups + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseDevicesMap(t *testing.T) { + tests := []struct { + name string + input map[string]string + wantLen int + wantURIs []string + }{ + { + name: "empty", + input: map[string]string{}, + wantLen: 0, + wantURIs: nil, + }, + { + name: "single_device", + input: map[string]string{ + "device-uri:0": "usb://HP/LaserJet", + "device-class:0": "direct", + "device-info:0": "HP LaserJet", + "device-make-and-model:0": "HP LaserJet 1020", + "device-id:0": "MFG:HP;MDL:LaserJet", + }, + wantLen: 1, + wantURIs: []string{"usb://HP/LaserJet"}, + }, + { + name: "multiple_devices", + input: map[string]string{ + "device-uri:0": "usb://HP/LaserJet", + "device-class:0": "direct", + "device-info:0": "HP LaserJet", + "device-uri:1": "socket://192.168.1.100", + "device-class:1": "network", + "device-info:1": "Network Printer", + }, + wantLen: 2, + wantURIs: []string{"usb://HP/LaserJet", "socket://192.168.1.100"}, + }, + { + name: "malformed_key", + input: map[string]string{ + "no-colon-here": "value", + }, + wantLen: 0, + wantURIs: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseDevicesMap(tt.input) + assert.Len(t, got, tt.wantLen) + + if tt.wantURIs != nil { + gotURIs := make(map[string]bool) + for _, d := range got { + gotURIs[d.URI] = true + } + for _, uri := range tt.wantURIs { + assert.True(t, gotURIs[uri], "expected URI %s not found", uri) + } + } + }) + } +} + +func TestParseDevicesMap_Attributes(t *testing.T) { + input := map[string]string{ + "device-uri:0": "usb://HP/LaserJet", + "device-class:0": "direct", + "device-info:0": "HP LaserJet", + "device-make-and-model:0": "HP LaserJet 1020", + "device-id:0": "MFG:HP;MDL:LaserJet", + "device-location:0": "USB Port", + } + + got := parseDevicesMap(input) + assert.Len(t, got, 1) + + dev := got[0] + assert.Equal(t, "usb://HP/LaserJet", dev.URI) + assert.Equal(t, "direct", dev.Class) + assert.Equal(t, "HP LaserJet", dev.Info) + assert.Equal(t, "HP LaserJet 1020", dev.MakeModel) + assert.Equal(t, "MFG:HP;MDL:LaserJet", dev.ID) + assert.Equal(t, "USB Port", dev.Location) +} diff --git a/core/internal/server/cups/testing.go b/core/internal/server/cups/testing.go new file mode 100644 index 00000000..4820e347 --- /dev/null +++ b/core/internal/server/cups/testing.go @@ -0,0 +1,13 @@ +package cups + +func NewTestManager(client CUPSClientInterface, pkHelper PkHelper) *Manager { + return &Manager{ + client: client, + pkHelper: pkHelper, + state: &CUPSState{ + Printers: make(map[string]*Printer), + }, + stopChan: make(chan struct{}), + dirty: make(chan struct{}, 1), + } +} diff --git a/core/internal/server/cups/types.go b/core/internal/server/cups/types.go index 5d03bda7..824d8a50 100644 --- a/core/internal/server/cups/types.go +++ b/core/internal/server/cups/types.go @@ -35,9 +35,38 @@ type Job struct { TimeCreated time.Time `json:"timeCreated"` } +type Device struct { + URI string `json:"uri"` + Class string `json:"class"` + Info string `json:"info"` + MakeModel string `json:"makeModel"` + ID string `json:"id"` + Location string `json:"location"` +} + +type PPD struct { + Name string `json:"name"` + NaturalLanguage string `json:"naturalLanguage"` + MakeModel string `json:"makeModel"` + DeviceID string `json:"deviceId"` + Product string `json:"product"` + PSVersion string `json:"psVersion"` + Type string `json:"type"` +} + +type PrinterClass struct { + Name string `json:"name"` + URI string `json:"uri"` + State string `json:"state"` + Members []string `json:"members"` + Location string `json:"location"` + Info string `json:"info"` +} + type Manager struct { state *CUPSState client CUPSClientInterface + pkHelper PkHelper subscription SubscriptionManagerInterface stateMutex sync.RWMutex subscribers syncmap.Map[string, chan CUPSState] @@ -63,6 +92,24 @@ type CUPSClientInterface interface { ResumePrinter(printer string) error CancelAllJob(printer string, purge bool) error SendRequest(url string, req *ipp.Request, additionalResponseData io.Writer) (*ipp.Response, error) + + GetDevices() (map[string]ipp.Attributes, error) + GetPPDs() (map[string]ipp.Attributes, error) + GetClasses(attributes []string) (map[string]ipp.Attributes, error) + CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error + DeletePrinter(printer string) error + AcceptJobs(printer string) error + RejectJobs(printer string) error + SetPrinterIsShared(printer string, shared bool) error + SetPrinterLocation(printer, location string) error + SetPrinterInformation(printer, information string) error + MoveJob(jobID int, destPrinter string) error + PrintTestPage(printer string, testPageData io.Reader, size int) (int, error) + AddPrinterToClass(class, printer string) error + DeletePrinterFromClass(class, printer string) error + DeleteClass(class string) error + RestartJob(jobID int) error + HoldJobUntil(jobID int, holdUntil string) error } type SubscriptionEvent struct { diff --git a/core/internal/server/server.go b/core/internal/server/server.go index ce238d2b..5ec71643 100644 --- a/core/internal/server/server.go +++ b/core/internal/server/server.go @@ -31,7 +31,9 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" ) -const APIVersion = 21 +const APIVersion = 22 + +var CLIVersion = "dev" type Capabilities struct { Capabilities []string `json:"capabilities"` @@ -39,6 +41,7 @@ type Capabilities struct { type ServerInfo struct { APIVersion int `json:"apiVersion"` + CLIVersion string `json:"cliVersion,omitempty"` Capabilities []string `json:"capabilities"` } @@ -431,6 +434,7 @@ func getServerInfo() ServerInfo { return ServerInfo{ APIVersion: APIVersion, + CLIVersion: CLIVersion, Capabilities: caps, } } diff --git a/core/pkg/ipp/constants.go b/core/pkg/ipp/constants.go index 5c91983e..83bf41ba 100644 --- a/core/pkg/ipp/constants.go +++ b/core/pkg/ipp/constants.go @@ -288,6 +288,7 @@ const ( // useful mime types for ipp const ( MimeTypePostscript = "application/postscript" + MimeTypePDF = "application/pdf" MimeTypeOctetStream = "application/octet-stream" ) diff --git a/core/pkg/ipp/cups-client.go b/core/pkg/ipp/cups-client.go index 9e2efc33..27cbbf12 100644 --- a/core/pkg/ipp/cups-client.go +++ b/core/pkg/ipp/cups-client.go @@ -1,7 +1,7 @@ package ipp import ( - "bytes" + "io" "strings" ) @@ -300,22 +300,13 @@ func (c *CUPSClient) GetClasses(attributes []string) (map[string]Attributes, err return printerNameMap, nil } -// PrintTestPage prints a test page of type application/vnd.cups-pdf-banner -func (c *CUPSClient) PrintTestPage(printer string) (int, error) { - testPage := new(bytes.Buffer) - testPage.WriteString("#PDF-BANNER\n") - testPage.WriteString("Template default-testpage.pdf\n") - testPage.WriteString("Show printer-name printer-info printer-location printer-make-and-model printer-driver-name") - testPage.WriteString("printer-driver-version paper-size imageable-area job-id options time-at-creation") - testPage.WriteString("time-at-processing\n\n") - - return c.PrintDocuments([]Document{ - { - Document: testPage, - Name: "Test Page", - Size: testPage.Len(), - MimeType: MimeTypePostscript, - }, +// PrintTestPage prints a test page using the provided PDF data +func (c *CUPSClient) PrintTestPage(printer string, testPageData io.Reader, size int) (int, error) { + return c.PrintJob(Document{ + Document: testPageData, + Name: "Test Page", + Size: size, + MimeType: MimeTypePDF, }, printer, map[string]interface{}{ AttributeJobName: "Test Page", }) diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index d15e16c9..54329e39 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -146,13 +146,30 @@ FocusScope { } Loader { - id: launcherLoader + id: printerLoader anchors.fill: parent active: root.currentIndex === 7 visible: active focus: active + sourceComponent: PrinterTab {} + + onActiveChanged: { + if (active && item) { + Qt.callLater(() => item.forceActiveFocus()); + } + } + } + + Loader { + id: launcherLoader + + anchors.fill: parent + active: root.currentIndex === 8 + visible: active + focus: active + sourceComponent: LauncherTab {} onActiveChanged: { @@ -166,7 +183,7 @@ FocusScope { id: themeColorsLoader anchors.fill: parent - active: root.currentIndex === 8 + active: root.currentIndex === 9 visible: active focus: active @@ -183,7 +200,7 @@ FocusScope { id: powerLoader anchors.fill: parent - active: root.currentIndex === 9 + active: root.currentIndex === 10 visible: active focus: active @@ -200,7 +217,7 @@ FocusScope { id: pluginsLoader anchors.fill: parent - active: root.currentIndex === 10 + active: root.currentIndex === 11 visible: active focus: active @@ -219,7 +236,7 @@ FocusScope { id: aboutLoader anchors.fill: parent - active: root.currentIndex === 11 + active: root.currentIndex === 12 visible: active focus: active diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 9c1ef27e..ee260e4f 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -67,6 +67,15 @@ FloatingWindow { } } + Loader { + active: settingsModal.visible + sourceComponent: Component { + Ref { + service: CupsService + } + } + } + FileBrowserModal { id: profileBrowser @@ -112,7 +121,7 @@ FloatingWindow { focus: true Keys.onPressed: event => { - const tabCount = 12; + const tabCount = 13; if (event.key === Qt.Key_Escape) { hide(); event.accepted = true; diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index d5430a7f..6de863e3 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -48,33 +48,45 @@ Rectangle { "dmsOnly": true, "tabIndex": 6 }, + { + "text": I18n.tr("Printers"), + "icon": "print", + "cupsOnly": true, + "tabIndex": 7 + }, { "text": I18n.tr("Launcher"), "icon": "apps", - "tabIndex": 7 + "tabIndex": 8 }, { "text": I18n.tr("Theme & Colors"), "icon": "palette", - "tabIndex": 8 + "tabIndex": 9 }, { "text": I18n.tr("Power & Security"), "icon": "power", - "tabIndex": 9 + "tabIndex": 10 }, { "text": I18n.tr("Plugins"), "icon": "extension", - "tabIndex": 10 + "tabIndex": 11 }, { "text": I18n.tr("About"), "icon": "info", - "tabIndex": 11 + "tabIndex": 12 } ] - readonly property var sidebarItems: allSidebarItems.filter(item => !item.dmsOnly || !NetworkService.usingLegacy) + readonly property var sidebarItems: allSidebarItems.filter(item => { + if (item.dmsOnly && NetworkService.usingLegacy) + return false; + if (item.cupsOnly && !CupsService.cupsAvailable) + return false; + return true; + }) function navigateNext() { const currentItemIndex = sidebarItems.findIndex(item => item.tabIndex === currentIndex); diff --git a/quickshell/Modules/ControlCenter/Models/WidgetModel.qml b/quickshell/Modules/ControlCenter/Models/WidgetModel.qml index a56cdfbf..1f4e1e1a 100644 --- a/quickshell/Modules/ControlCenter/Models/WidgetModel.qml +++ b/quickshell/Modules/ControlCenter/Models/WidgetModel.qml @@ -41,16 +41,10 @@ QtObject { onItemChanged: { root.cupsBuiltinInstance = item; - if (item && !DMSService.activeSubscriptions.includes("cups") && !DMSService.activeSubscriptions.includes("all")) { - DMSService.addSubscription("cups"); - } } onActiveChanged: { if (!active) { - if (DMSService.activeSubscriptions.includes("cups")) { - DMSService.removeSubscription("cups"); - } root.cupsBuiltinInstance = null; } } diff --git a/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml b/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml index a95ca946..d35bfee5 100644 --- a/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml +++ b/quickshell/Modules/DankBar/Widgets/ControlCenterButton.qml @@ -22,6 +22,15 @@ BasePill { property bool showBatteryIcon: SettingsData.controlCenterShowBatteryIcon property bool showPrinterIcon: SettingsData.controlCenterShowPrinterIcon + Loader { + active: root.showPrinterIcon + sourceComponent: Component { + Ref { + service: CupsService + } + } + } + function getNetworkIconName() { if (NetworkService.wifiToggling) return "sync"; diff --git a/quickshell/Modules/Settings/AboutTab.qml b/quickshell/Modules/Settings/AboutTab.qml index 2ff702e4..bc11a913 100644 --- a/quickshell/Modules/Settings/AboutTab.qml +++ b/quickshell/Modules/Settings/AboutTab.qml @@ -595,6 +595,159 @@ Item { } } + StyledRect { + visible: DMSService.isConnected + width: parent.width + height: backendSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 0 + + Column { + id: backendSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "dns" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Backend") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + spacing: Theme.spacingL + + Column { + spacing: 2 + + StyledText { + text: I18n.tr("Version") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: DMSService.cliVersion || "—" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + Rectangle { + width: 1 + height: 32 + color: Theme.outlineVariant + } + + Column { + spacing: 2 + + StyledText { + text: I18n.tr("API") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + StyledText { + text: `v${DMSService.apiVersion}` + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + Rectangle { + width: 1 + height: 32 + color: Theme.outlineVariant + } + + Column { + spacing: 2 + + StyledText { + text: I18n.tr("Status") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + Row { + spacing: 4 + + Rectangle { + width: 8 + height: 8 + radius: 4 + color: Theme.success + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Connected") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: DMSService.capabilities.length > 0 + + StyledText { + text: I18n.tr("Capabilities") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + Flow { + width: parent.width + spacing: 6 + + Repeater { + model: DMSService.capabilities + + Rectangle { + width: capText.implicitWidth + 16 + height: 26 + radius: 13 + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + + StyledText { + id: capText + anchors.centerIn: parent + text: modelData + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + } + } + } + } + } + } + } + // Support Section StyledRect { width: parent.width diff --git a/quickshell/Modules/Settings/PrinterTab.qml b/quickshell/Modules/Settings/PrinterTab.qml new file mode 100644 index 00000000..16f16894 --- /dev/null +++ b/quickshell/Modules/Settings/PrinterTab.qml @@ -0,0 +1,1360 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Modals.Common +import qs.Services +import qs.Widgets + +Item { + id: printerTab + + property bool showAddPrinter: false + property string newPrinterName: "" + property string selectedDeviceUri: "" + property var selectedDevice: null + property string selectedPpd: "" + property string newPrinterLocation: "" + property string newPrinterInfo: "" + property var suggestedPPDs: [] + + function resetAddPrinterForm() { + newPrinterName = ""; + selectedDeviceUri = ""; + selectedDevice = null; + selectedPpd = ""; + newPrinterLocation = ""; + newPrinterInfo = ""; + suggestedPPDs = []; + } + + function selectDevice(device) { + if (!device) + return; + selectedDevice = device; + selectedDeviceUri = device.uri; + if (!newPrinterName) { + newPrinterName = CupsService.suggestPrinterName(device); + } + if (device.location && !newPrinterLocation) { + newPrinterLocation = CupsService.decodeUri(device.location); + } + suggestedPPDs = CupsService.getMatchingPPDs(device); + if (suggestedPPDs.length > 0 && !selectedPpd) { + selectedPpd = suggestedPPDs[0].name; + } + } + + Component.onCompleted: { + CupsService.getClasses(); + } + + ConfirmModal { + id: deletePrinterConfirm + } + + ConfirmModal { + id: purgeJobsConfirm + } + + ConfirmModal { + id: deleteClassConfirm + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: mainColumn + + width: Math.min(600, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingL + + StyledRect { + width: parent.width + height: overviewSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + + Column { + id: overviewSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "print" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("CUPS Print Server") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Grid { + columns: 2 + columnSpacing: Theme.spacingL + rowSpacing: Theme.spacingS + width: parent.width + + StyledText { + text: I18n.tr("Status") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + Row { + spacing: Theme.spacingS + + Rectangle { + width: 8 + height: 8 + radius: 4 + anchors.verticalCenter: parent.verticalCenter + color: CupsService.cupsAvailable ? Theme.success : Theme.error + } + + StyledText { + text: CupsService.cupsAvailable ? I18n.tr("Available") : I18n.tr("Unavailable") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + StyledText { + text: I18n.tr("Printers") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + StyledText { + text: CupsService.printerNames.length.toString() + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: I18n.tr("Total Jobs") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + } + StyledText { + text: CupsService.getTotalJobsNum().toString() + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + } + } + } + + StyledRect { + width: parent.width + height: addPrinterSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + visible: CupsService.cupsAvailable + + Column { + id: addPrinterSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "add_circle" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - addPrinterToggleBtn.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Add Printer") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Configure a new printer") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + Rectangle { + id: addPrinterToggleBtn + width: 28 + height: 28 + radius: 14 + color: addPrinterToggleArea.containsMouse ? Theme.surfacePressed : "transparent" + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: printerTab.showAddPrinter ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: addPrinterToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + printerTab.showAddPrinter = !printerTab.showAddPrinter; + if (printerTab.showAddPrinter) { + if (CupsService.devices.length === 0) { + CupsService.getDevices(); + CupsService.getPPDs(); + } + } else { + printerTab.resetAddPrinterForm(); + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: printerTab.showAddPrinter + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Device") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + id: deviceDropdown + dropdownWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2 + popupWidth: parent.width - 80 - scanDevicesBtn.width - Theme.spacingS * 2 + currentValue: { + if (CupsService.loadingDevices) + return I18n.tr("Scanning..."); + if (printerTab.selectedDevice) + return CupsService.getDeviceDisplayName(printerTab.selectedDevice); + return I18n.tr("Select device..."); + } + options: { + const filtered = CupsService.filteredDevices; + if (filtered.length === 0) + return [I18n.tr("No devices found")]; + return filtered.map(d => CupsService.getDeviceDisplayName(d)); + } + onValueChanged: value => { + if (value === I18n.tr("No devices found") || value === I18n.tr("Scanning...")) + return; + const filtered = CupsService.filteredDevices; + const device = filtered.find(d => CupsService.getDeviceDisplayName(d) === value); + if (device) { + printerTab.selectDevice(device); + } + } + } + + DankActionButton { + id: scanDevicesBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + enabled: !CupsService.loadingDevices + onClicked: CupsService.getDevices() + + RotationAnimation on rotation { + running: CupsService.loadingDevices + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1000 + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + visible: printerTab.selectedDevice !== null + + Item { + width: 80 + height: 1 + } + + StyledText { + text: CupsService.getDeviceSubtitle(printerTab.selectedDevice) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width - 80 - Theme.spacingS + elide: Text.ElideRight + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Driver") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankDropdown { + id: ppdDropdown + dropdownWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2 + popupWidth: parent.width - 80 - refreshPpdsBtn.width - Theme.spacingS * 2 + currentValue: { + if (CupsService.loadingPPDs) + return I18n.tr("Loading..."); + if (printerTab.selectedPpd) { + const ppd = CupsService.ppds.find(p => p.name === printerTab.selectedPpd); + if (ppd) { + const isSuggested = printerTab.suggestedPPDs.some(s => s.name === ppd.name); + return (isSuggested ? "★ " : "") + (ppd.makeModel || ppd.name); + } + return printerTab.selectedPpd; + } + return printerTab.suggestedPPDs.length > 0 ? I18n.tr("Recommended available") : I18n.tr("Select driver..."); + } + options: { + if (CupsService.ppds.length === 0) + return [I18n.tr("No drivers found")]; + const suggested = printerTab.suggestedPPDs.map(p => "★ " + (p.makeModel || p.name)); + const others = CupsService.ppds.filter(p => !printerTab.suggestedPPDs.some(s => s.name === p.name)).map(p => p.makeModel || p.name); + return suggested.concat(others); + } + onValueChanged: value => { + if (value === I18n.tr("No drivers found") || value === I18n.tr("Loading...")) + return; + const cleanValue = value.replace(/^★ /, ""); + const ppd = CupsService.ppds.find(p => (p.makeModel || p.name) === cleanValue); + if (ppd) { + printerTab.selectedPpd = ppd.name; + } + } + } + + DankActionButton { + id: refreshPpdsBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + enabled: !CupsService.loadingPPDs + onClicked: CupsService.getPPDs() + + RotationAnimation on rotation { + running: CupsService.loadingPPDs + loops: Animation.Infinite + from: 0 + to: 360 + duration: 1000 + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Name") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingS + height: 40 + placeholderText: I18n.tr("Printer name (no spaces)") + text: printerTab.newPrinterName + onTextEdited: printerTab.newPrinterName = text.replace(/\s/g, "-") + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Location") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingS + height: 40 + placeholderText: I18n.tr("Optional location") + text: printerTab.newPrinterLocation + onTextEdited: printerTab.newPrinterLocation = text + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Description") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + width: 80 + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + width: parent.width - 80 - Theme.spacingS + height: 40 + placeholderText: I18n.tr("Optional description") + text: printerTab.newPrinterInfo + onTextEdited: printerTab.newPrinterInfo = text + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + layoutDirection: Qt.RightToLeft + + DankButton { + text: CupsService.creatingPrinter ? I18n.tr("Creating...") : I18n.tr("Create Printer") + iconName: CupsService.creatingPrinter ? "sync" : "add" + buttonHeight: 36 + enabled: printerTab.newPrinterName.length > 0 && printerTab.selectedDeviceUri.length > 0 && printerTab.selectedPpd.length > 0 && !CupsService.creatingPrinter + onClicked: { + CupsService.createPrinter(printerTab.newPrinterName, printerTab.selectedDeviceUri, printerTab.selectedPpd, { + location: printerTab.newPrinterLocation, + information: printerTab.newPrinterInfo + }); + printerTab.resetAddPrinterForm(); + printerTab.showAddPrinter = false; + } + } + } + } + } + } + + StyledRect { + width: parent.width + height: printersSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + visible: CupsService.cupsAvailable + + Column { + id: printersSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "print" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - refreshBtn.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Printers") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: { + const count = CupsService.printerNames.length; + if (count === 0) + return I18n.tr("No printers configured"); + return I18n.tr("%1 printer(s)").arg(count); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + DankActionButton { + id: refreshBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + onClicked: CupsService.getState() + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Item { + width: parent.width + height: 80 + visible: CupsService.printerNames.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "print_disabled" + size: 32 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("No printers found") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + Column { + width: parent.width + spacing: 4 + visible: CupsService.printerNames.length > 0 + + Repeater { + model: CupsService.printerNames + + delegate: Rectangle { + id: printerDelegate + required property string modelData + required property int index + + readonly property var printerData: CupsService.getPrinterData(modelData) + readonly property bool isExpanded: CupsService.expandedPrinter === modelData || hasJobs + readonly property bool hasJobs: (printerData?.jobs?.length ?? 0) > 0 + readonly property bool isIdle: printerData?.state === "idle" + readonly property bool isStopped: printerData?.state === "stopped" + + width: parent.width + height: isExpanded ? 56 + expandedContent.height : 56 + radius: Theme.cornerRadius + color: printerMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + border.width: CupsService.selectedPrinter === modelData ? 2 : 0 + border.color: Theme.primary + clip: true + + Behavior on height { + NumberAnimation { + duration: 150 + easing.type: Easing.OutQuad + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 56 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + anchors.right: printerActions.left + anchors.rightMargin: Theme.spacingS + spacing: Theme.spacingS + + DankIcon { + name: isStopped ? "print_disabled" : "print" + size: 20 + color: isStopped ? Theme.error : (isIdle ? Theme.primary : Theme.warning) + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: parent.width - 20 - Theme.spacingS + + StyledText { + text: modelData + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: CupsService.selectedPrinter === modelData ? Font.Medium : Font.Normal + elide: Text.ElideRight + width: parent.width + } + + Row { + spacing: Theme.spacingXS + + StyledText { + text: CupsService.getPrinterStateTranslation(printerData?.state || "") + font.pixelSize: Theme.fontSizeSmall + color: { + switch (printerData?.state) { + case "idle": + return Theme.primary; + case "stopped": + return Theme.error; + case "processing": + return Theme.warning; + default: + return Theme.surfaceVariantText; + } + } + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (printerData?.jobs?.length ?? 0) > 0 + } + + StyledText { + text: I18n.tr("%1 job(s)").arg(printerData?.jobs?.length ?? 0) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + visible: (printerData?.jobs?.length ?? 0) > 0 + } + } + } + } + + Row { + id: printerActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent" + + DankIcon { + anchors.centerIn: parent + name: isExpanded ? "expand_less" : "expand_more" + size: 18 + color: Theme.surfaceText + } + + MouseArea { + id: expandBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + CupsService.expandedPrinter = isExpanded ? "" : modelData; + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: deleteBtn.containsMouse ? Theme.errorHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + deletePrinterConfirm.showWithOptions({ + title: I18n.tr("Delete Printer"), + message: I18n.tr("Delete \"%1\"?").arg(modelData), + confirmText: I18n.tr("Delete"), + confirmColor: Theme.error, + onConfirm: () => CupsService.deletePrinter(modelData) + }); + } + } + } + } + + MouseArea { + id: printerMouseArea + anchors.fill: parent + anchors.rightMargin: printerActions.width + Theme.spacingM + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + CupsService.setSelectedPrinter(modelData); + } + } + } + + Column { + id: expandedContent + width: parent.width + visible: isExpanded + + Rectangle { + width: parent.width - Theme.spacingM * 2 + height: 1 + x: Theme.spacingM + color: Theme.outlineLight + } + + Item { + width: parent.width + height: detailsColumn.implicitHeight + Theme.spacingM * 2 + + Column { + id: detailsColumn + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + Flow { + width: parent.width + spacing: Theme.spacingXS + + Repeater { + model: { + const fields = []; + const p = printerData; + if (!p) + return fields; + + fields.push({ + label: I18n.tr("State"), + value: CupsService.getPrinterStateTranslation(p.state) + }); + if (p.stateReason && p.stateReason !== "none") + fields.push({ + label: I18n.tr("Reason"), + value: CupsService.getPrinterStateReasonTranslation(p.stateReason) + }); + if (p.makeModel) + fields.push({ + label: I18n.tr("Model"), + value: p.makeModel + }); + if (p.location) + fields.push({ + label: I18n.tr("Location"), + value: p.location + }); + fields.push({ + label: I18n.tr("Accepting"), + value: p.accepting ? I18n.tr("Yes") : I18n.tr("No") + }); + + return fields; + } + + delegate: Rectangle { + required property var modelData + required property int index + + width: fieldContent.width + Theme.spacingM * 2 + height: 32 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHigh + border.width: 1 + border.color: Theme.outlineLight + + Row { + id: fieldContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: modelData.label + ":" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.value + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Rectangle { + height: 28 + width: pauseResumeRow.width + Theme.spacingM * 2 + radius: 14 + color: pauseResumeArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + id: pauseResumeRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: isStopped ? "play_arrow" : "pause" + size: 16 + color: Theme.surfaceText + } + + StyledText { + text: isStopped ? I18n.tr("Resume") : I18n.tr("Pause") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: pauseResumeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (isStopped) { + CupsService.resumePrinter(modelData); + } else { + CupsService.pausePrinter(modelData); + } + } + } + } + + Rectangle { + height: 28 + width: testPageRow.width + Theme.spacingM * 2 + radius: 14 + color: testPageArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + id: testPageRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "description" + size: 16 + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Test Page") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: testPageArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.printTestPage(modelData) + } + } + + Rectangle { + height: 28 + width: acceptRejectRow.width + Theme.spacingM * 2 + radius: 14 + color: acceptRejectArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + id: acceptRejectRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: printerData?.accepting ? "block" : "check_circle" + size: 16 + color: Theme.surfaceText + } + + StyledText { + text: printerData?.accepting ? I18n.tr("Reject Jobs") : I18n.tr("Accept Jobs") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: acceptRejectArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (printerData?.accepting) { + CupsService.rejectJobs(modelData); + } else { + CupsService.acceptJobs(modelData); + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingXS + visible: (printerData?.jobs?.length ?? 0) > 0 + + Row { + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Jobs") + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: 1 + height: 1 + Layout.fillWidth: true + } + + Rectangle { + height: 24 + width: purgeRow.width + Theme.spacingM * 2 + radius: 12 + color: purgeArea.containsMouse ? Theme.errorHover : Theme.surfaceLight + + Row { + id: purgeRow + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "delete_sweep" + size: 14 + color: purgeArea.containsMouse ? Theme.error : Theme.surfaceText + } + + StyledText { + text: I18n.tr("Clear All") + font.pixelSize: Theme.fontSizeSmall - 1 + color: purgeArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Medium + } + } + + MouseArea { + id: purgeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + purgeJobsConfirm.showWithOptions({ + title: I18n.tr("Clear All Jobs"), + message: I18n.tr("Cancel all jobs for \"%1\"?").arg(modelData), + confirmText: I18n.tr("Clear"), + confirmColor: Theme.error, + onConfirm: () => CupsService.purgeJobs(modelData) + }); + } + } + } + } + + Repeater { + model: printerData?.jobs ?? [] + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 44 + radius: Theme.cornerRadius - 2 + color: Theme.surfaceContainerHighest + border.width: 1 + border.color: Theme.outlineLight + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: jobActions.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "description" + size: 18 + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 1 + width: parent.width - 18 - Theme.spacingS + + StyledText { + text: "[" + modelData.id + "] " + CupsService.getJobStateTranslation(modelData.state) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + StyledText { + text: { + const size = Math.round((modelData.size || 0) / 1024); + const date = new Date(modelData.timeCreated); + return size + " KB • " + date.toLocaleString(Qt.locale(), Locale.ShortFormat); + } + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + } + } + + Row { + id: jobActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: holdJobBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: modelData.state === "pending" + + DankIcon { + anchors.centerIn: parent + name: "pause" + size: 14 + color: Theme.surfaceVariantText + } + + MouseArea { + id: holdJobBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.holdJob(modelData.id) + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: restartJobBtn.containsMouse ? Theme.surfacePressed : "transparent" + visible: modelData.state === "pending-held" || modelData.state === "completed" || modelData.state === "aborted" + + DankIcon { + anchors.centerIn: parent + name: "replay" + size: 14 + color: Theme.surfaceVariantText + } + + MouseArea { + id: restartJobBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.restartJob(modelData.id) + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: cancelJobBtn.containsMouse ? Theme.errorHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 14 + color: cancelJobBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: cancelJobBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: CupsService.cancelJob(printerDelegate.modelData, modelData.id) + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + StyledRect { + width: parent.width + height: classesSection.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + visible: CupsService.cupsAvailable && CupsService.printerClasses.length > 0 + + Column { + id: classesSection + + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "workspaces" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - Theme.spacingM - refreshClassesBtn.width - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Printer Classes") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("%1 class(es)").arg(CupsService.printerClasses.length) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + DankActionButton { + id: refreshClassesBtn + iconName: "refresh" + buttonSize: 32 + anchors.verticalCenter: parent.verticalCenter + onClicked: CupsService.getClasses() + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + + Column { + width: parent.width + spacing: 4 + + Repeater { + model: CupsService.printerClasses + + delegate: Rectangle { + required property var modelData + required property int index + + width: parent.width + height: 48 + radius: Theme.cornerRadius + color: classMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.right: classActions.left + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "workspaces" + size: 20 + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + StyledText { + text: modelData.name || I18n.tr("Unknown") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("%1 printer(s)").arg(modelData.members?.length ?? 0) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + + Row { + id: classActions + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Rectangle { + width: 28 + height: 28 + radius: 14 + color: deleteClassBtn.containsMouse ? Theme.errorHover : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "delete" + size: 18 + color: deleteClassBtn.containsMouse ? Theme.error : Theme.surfaceVariantText + } + + MouseArea { + id: deleteClassBtn + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + deleteClassConfirm.showWithOptions({ + title: I18n.tr("Delete Class"), + message: I18n.tr("Delete class \"%1\"?").arg(modelData.name), + confirmText: I18n.tr("Delete"), + confirmColor: Theme.error, + onConfirm: () => CupsService.deleteClass(modelData.name) + }); + } + } + } + } + + MouseArea { + id: classMouseArea + anchors.fill: parent + anchors.rightMargin: classActions.width + Theme.spacingM + hoverEnabled: true + } + } + } + } + } + } + } + } +} diff --git a/quickshell/Services/CupsService.qml b/quickshell/Services/CupsService.qml index 5e358d3c..21b4cfc1 100644 --- a/quickshell/Services/CupsService.qml +++ b/quickshell/Services/CupsService.qml @@ -1,10 +1,8 @@ pragma Singleton - pragma ComponentBehavior: Bound import QtQuick import Quickshell -import Quickshell.Io import qs.Common Singleton { @@ -12,20 +10,179 @@ Singleton { property int refCount: 0 + onRefCountChanged: { + if (refCount > 0) { + ensureSubscription(); + } else if (refCount === 0 && DMSService.activeSubscriptions.includes("cups")) { + DMSService.removeSubscription("cups"); + } + } + + function ensureSubscription() { + if (refCount <= 0) + return; + if (!DMSService.isConnected) + return; + if (DMSService.activeSubscriptions.includes("cups")) + return; + if (DMSService.activeSubscriptions.includes("all")) + return; + DMSService.addSubscription("cups"); + if (cupsAvailable) { + getState(); + } + } + property var printerNames: [] property var printers: [] property string selectedPrinter: "" + property string expandedPrinter: "" property bool cupsAvailable: false property bool stateInitialized: false + property var devices: [] + property var ppds: [] + property var printerClasses: [] + + readonly property var filteredDevices: { + if (!devices || devices.length === 0) + return []; + const bareProtocols = ["ipp", "ipps", "http", "https", "lpd", "socket", "beh", "dnssd", "mdns", "smb", "file", "cups-brf"]; + + // First pass: filter out invalid/bare protocol entries + const validDevices = devices.filter(d => { + if (!d.uri) + return false; + const uriLower = d.uri.toLowerCase(); + for (let proto of bareProtocols) { + if (uriLower === proto || uriLower === proto + ":") + return false; + } + if (d.class === "network" && d.info === "Backend Error Handler") + return false; + return true; + }); + + // Second pass: prefer IPP over LPD for the same printer + // _printer._tcp (LPD) doesn't work well with driverless printing + // _ipp._tcp or _ipps._tcp (IPP) should be preferred + const ippDeviceHosts = new Set(); + for (const d of validDevices) { + if (!d.uri) + continue; + // Extract hostname from dnssd URIs like dnssd://Name%20[mac]._ipp._tcp.local + const ippMatch = d.uri.match(/dnssd:\/\/[^/]*\._ipps?\._tcp/); + if (ippMatch) { + // Extract the unique identifier (usually MAC address in brackets) + const macMatch = d.uri.match(/\[([a-f0-9]+)\]/i); + if (macMatch) + ippDeviceHosts.add(macMatch[1].toLowerCase()); + } + } + + // Filter out _printer._tcp devices when we have _ipp._tcp for the same printer + return validDevices.filter(d => { + if (!d.uri) + return true; + // If this is an LPD device, check if we have an IPP alternative + if (d.uri.includes("._printer._tcp")) { + const macMatch = d.uri.match(/\[([a-f0-9]+)\]/i); + if (macMatch && ippDeviceHosts.has(macMatch[1].toLowerCase())) { + return false; // Skip LPD device, we have IPP + } + } + return true; + }); + } + + function decodeUri(str) { + if (!str) + return ""; + try { + return decodeURIComponent(str.replace(/\+/g, " ")); + } catch (e) { + return str; + } + } + + function getDeviceDisplayName(device) { + if (!device) + return ""; + if (device.info && device.info.length > 0) { + return decodeUri(device.info); + } + if (device.makeModel && device.makeModel.length > 0) { + return decodeUri(device.makeModel); + } + return decodeUri(device.uri); + } + + function getDeviceSubtitle(device) { + if (!device) + return ""; + const parts = []; + if (device.class) { + switch (device.class) { + case "direct": + parts.push(I18n.tr("Local")); + break; + case "network": + parts.push(I18n.tr("Network")); + break; + case "file": + parts.push(I18n.tr("File")); + break; + default: + parts.push(device.class); + } + } + if (device.location) + parts.push(decodeUri(device.location)); + return parts.join(" • "); + } + + function suggestPrinterName(device) { + if (!device) + return ""; + let name = device.info || device.makeModel || ""; + name = name.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); + return name.substring(0, 32) || "Printer"; + } + + function getMatchingPPDs(device) { + if (!device || !ppds || ppds.length === 0) + return []; + const isDnssd = device.uri && (device.uri.startsWith("dnssd://") || device.uri.startsWith("ipp://") || device.uri.startsWith("ipps://")); + if (isDnssd) { + const driverless = ppds.filter(p => p.name === "driverless" || p.name === "everywhere" || (p.makeModel && p.makeModel.toLowerCase().includes("driverless"))); + if (driverless.length > 0) + return driverless; + } + if (!device.makeModel) + return []; + const makeModelLower = device.makeModel.toLowerCase(); + const words = makeModelLower.split(/[\s_-]+/).filter(w => w.length > 2); + return ppds.filter(p => { + if (!p.makeModel) + return false; + const ppdLower = p.makeModel.toLowerCase(); + return words.some(w => ppdLower.includes(w)); + }).slice(0, 10); + } + + property bool loadingDevices: false + property bool loadingPPDs: false + property bool loadingClasses: false + property bool creatingPrinter: false + signal cupsStateUpdate readonly property string socketPath: Quickshell.env("DMS_SOCKET") Component.onCompleted: { if (socketPath && socketPath.length > 0) { - checkDMSCapabilities() + checkDMSCapabilities(); } } @@ -34,7 +191,8 @@ Singleton { function onConnectionStateChanged() { if (DMSService.isConnected) { - checkDMSCapabilities() + checkDMSCapabilities(); + ensureSubscription(); } } } @@ -44,328 +202,641 @@ Singleton { enabled: DMSService.isConnected function onCupsStateUpdate(data) { - console.log("CupsService: Subscription update received") - getState() + console.log("CupsService: Subscription update received"); + getState(); } function onCapabilitiesChanged() { - checkDMSCapabilities() + checkDMSCapabilities(); } } function checkDMSCapabilities() { - if (!DMSService.isConnected) { - return - } - - if (DMSService.capabilities.length === 0) { - return - } - - cupsAvailable = DMSService.capabilities.includes("cups") + if (!DMSService.isConnected) + return; + if (DMSService.capabilities.length === 0) + return; + cupsAvailable = DMSService.capabilities.includes("cups"); if (cupsAvailable && !stateInitialized) { - stateInitialized = true - getState() + stateInitialized = true; + getState(); } } function getState() { if (!cupsAvailable) - return - + return; DMSService.sendRequest("cups.getPrinters", null, response => { - if (response.result) { - updatePrinters(response.result) - fetchAllJobs() - } - }) + if (response.result) { + updatePrinters(response.result); + fetchAllJobs(); + } + }); } function updatePrinters(printersData) { - printerNames = printersData.map(p => p.name) + printerNames = printersData.map(p => p.name); - let printersObj = {} + let printersObj = {}; for (var i = 0; i < printersData.length; i++) { - let printer = printersData[i] + let printer = printersData[i]; printersObj[printer.name] = { + "name": printer.name, + "uri": printer.uri || "", "state": printer.state, "stateReason": printer.stateReason, + "location": printer.location || "", + "info": printer.info || "", + "makeModel": printer.makeModel || "", + "accepting": printer.accepting !== false, "jobs": [] - } + }; } - printers = printersObj + printers = printersObj; if (printerNames.length > 0) { if (selectedPrinter.length > 0) { if (!printerNames.includes(selectedPrinter)) { - selectedPrinter = printerNames[0] + selectedPrinter = printerNames[0]; } } else { - selectedPrinter = printerNames[0] + selectedPrinter = printerNames[0]; } } } function fetchAllJobs() { for (var i = 0; i < printerNames.length; i++) { - fetchJobsForPrinter(printerNames[i]) + fetchJobsForPrinter(printerNames[i]); } } function fetchJobsForPrinter(printerName) { const params = { "printerName": printerName - } + }; DMSService.sendRequest("cups.getJobs", params, response => { - if (response.result && printers[printerName]) { - let updatedPrinters = Object.assign({}, printers) - updatedPrinters[printerName].jobs = response.result - printers = updatedPrinters - } - }) + if (response.result && printers[printerName]) { + let updatedPrinters = Object.assign({}, printers); + updatedPrinters[printerName].jobs = response.result; + printers = updatedPrinters; + } + }); } function getSelectedPrinter() { - return selectedPrinter + return selectedPrinter; } function setSelectedPrinter(printerName) { if (printerNames.length > 0) { if (printerNames.includes(printerName)) { - selectedPrinter = printerName + selectedPrinter = printerName; } else { - selectedPrinter = printerNames[0] + selectedPrinter = printerNames[0]; } } } function getPrintersNum() { if (!cupsAvailable) - return 0 + return 0; - return printerNames.length + return printerNames.length; } function getPrintersNames() { if (!cupsAvailable) - return [] + return []; - return printerNames + return printerNames; } function getTotalJobsNum() { if (!cupsAvailable) - return 0 + return 0; - var result = 0 + var result = 0; for (var i = 0; i < printerNames.length; i++) { - var printerName = printerNames[i] + var printerName = printerNames[i]; if (printers[printerName] && printers[printerName].jobs) { - result += printers[printerName].jobs.length + result += printers[printerName].jobs.length; } } - return result + return result; } function getCurrentPrinterState() { if (!cupsAvailable || !selectedPrinter) - return "" + return ""; - var printer = printers[selectedPrinter] - return printer.state + var printer = printers[selectedPrinter]; + return printer.state; } function getCurrentPrinterStatePrettyShort() { if (!cupsAvailable || !selectedPrinter) - return "" + return ""; - var printer = printers[selectedPrinter] - return getPrinterStateTranslation(printer.state) + " (" + getPrinterStateReasonTranslation(printer.stateReason) + ")" + var printer = printers[selectedPrinter]; + return getPrinterStateTranslation(printer.state) + " (" + getPrinterStateReasonTranslation(printer.stateReason) + ")"; } function getCurrentPrinterStatePretty() { if (!cupsAvailable || !selectedPrinter) - return "" + return ""; - var printer = printers[selectedPrinter] - return getPrinterStateTranslation(printer.state) + " (" + I18n.tr("Reason") + ": " + getPrinterStateReasonTranslation(printer.stateReason) + ")" + var printer = printers[selectedPrinter]; + return getPrinterStateTranslation(printer.state) + " (" + I18n.tr("Reason") + ": " + getPrinterStateReasonTranslation(printer.stateReason) + ")"; } function getCurrentPrinterJobs() { if (!cupsAvailable || !selectedPrinter) - return [] + return []; - return getJobs(selectedPrinter) + return getJobs(selectedPrinter); } function getJobs(printerName) { if (!cupsAvailable) - return "" + return ""; - var printer = printers[printerName] - return printer.jobs + var printer = printers[printerName]; + return printer.jobs; } function getJobsNum(printerName) { if (!cupsAvailable) - return 0 + return 0; - var printer = printers[printerName] - return printer.jobs.length + var printer = printers[printerName]; + return printer.jobs.length; } function pausePrinter(printerName) { if (!cupsAvailable) - return - + return; const params = { "printerName": printerName - } + }; DMSService.sendRequest("cups.pausePrinter", params, response => { - if (response.error) { - ToastService.showError(I18n.tr("Failed to pause printer") + " - " + response.error) - } else { - getState() - } - }) + if (response.error) { + ToastService.showError(I18n.tr("Failed to pause printer") + " - " + response.error); + } else { + getState(); + } + }); } function resumePrinter(printerName) { if (!cupsAvailable) - return - + return; const params = { "printerName": printerName - } + }; DMSService.sendRequest("cups.resumePrinter", params, response => { - if (response.error) { - ToastService.showError(I18n.tr("Failed to resume printer") + " - " + response.error) - } else { - getState() - } - }) + if (response.error) { + ToastService.showError(I18n.tr("Failed to resume printer") + " - " + response.error); + } else { + getState(); + } + }); } function cancelJob(printerName, jobID) { if (!cupsAvailable) - return - + return; const params = { "printerName": printerName, "jobID": jobID - } + }; DMSService.sendRequest("cups.cancelJob", params, response => { - if (response.error) { - ToastService.showError(I18n.tr("Failed to cancel selected job") + " - " + response.error) - } else { - fetchJobsForPrinter(printerName) - } - }) + if (response.error) { + ToastService.showError(I18n.tr("Failed to cancel selected job") + " - " + response.error); + } else { + fetchJobsForPrinter(printerName); + } + }); } function purgeJobs(printerName) { if (!cupsAvailable) - return - + return; const params = { "printerName": printerName - } + }; DMSService.sendRequest("cups.purgeJobs", params, response => { - if (response.error) { - ToastService.showError(I18n.tr("Failed to cancel all jobs") + " - " + response.error) - } else { - fetchJobsForPrinter(printerName) - } - }) + if (response.error) { + ToastService.showError(I18n.tr("Failed to cancel all jobs") + " - " + response.error); + } else { + fetchJobsForPrinter(printerName); + } + }); + } + + function getDevices() { + if (!cupsAvailable) + return; + loadingDevices = true; + DMSService.sendRequest("cups.getDevices", null, response => { + loadingDevices = false; + if (response.result) { + devices = response.result; + } + }); + } + + function getPPDs() { + if (!cupsAvailable) + return; + loadingPPDs = true; + DMSService.sendRequest("cups.getPPDs", null, response => { + loadingPPDs = false; + if (response.result) { + ppds = response.result; + } + }); + } + + function getClasses() { + if (!cupsAvailable) + return; + loadingClasses = true; + DMSService.sendRequest("cups.getClasses", null, response => { + loadingClasses = false; + if (response.result) { + printerClasses = response.result; + } + }); + } + + function createPrinter(name, deviceURI, ppd, options) { + if (!cupsAvailable) + return; + creatingPrinter = true; + const params = { + "name": name, + "deviceURI": deviceURI, + "ppd": ppd + }; + if (options) { + if (options.shared !== undefined) + params.shared = options.shared; + if (options.location) + params.location = options.location; + if (options.information) + params.information = options.information; + if (options.errorPolicy) + params.errorPolicy = options.errorPolicy; + } + + DMSService.sendRequest("cups.createPrinter", params, response => { + creatingPrinter = false; + if (response.error) { + ToastService.showError(I18n.tr("Failed to create printer") + " - " + response.error); + } else { + ToastService.showInfo(I18n.tr("Printer created successfully")); + getState(); + } + }); + } + + function deletePrinter(printerName) { + if (!cupsAvailable) + return; + const params = { + "printerName": printerName + }; + + DMSService.sendRequest("cups.deletePrinter", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to delete printer") + " - " + response.error); + } else { + ToastService.showInfo(I18n.tr("Printer deleted")); + if (selectedPrinter === printerName) { + selectedPrinter = ""; + } + getState(); + } + }); + } + + function acceptJobs(printerName) { + if (!cupsAvailable) + return; + const params = { + "printerName": printerName + }; + + DMSService.sendRequest("cups.acceptJobs", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to enable job acceptance") + " - " + response.error); + } else { + getState(); + } + }); + } + + function rejectJobs(printerName) { + if (!cupsAvailable) + return; + const params = { + "printerName": printerName + }; + + DMSService.sendRequest("cups.rejectJobs", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to disable job acceptance") + " - " + response.error); + } else { + getState(); + } + }); + } + + function setPrinterShared(printerName, shared) { + if (!cupsAvailable) + return; + const params = { + "printerName": printerName, + "shared": shared + }; + + DMSService.sendRequest("cups.setPrinterShared", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to update sharing") + " - " + response.error); + } else { + getState(); + } + }); + } + + function setPrinterLocation(printerName, location) { + if (!cupsAvailable) + return; + const params = { + "printerName": printerName, + "location": location + }; + + DMSService.sendRequest("cups.setPrinterLocation", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to update location") + " - " + response.error); + } else { + getState(); + } + }); + } + + function setPrinterInfo(printerName, info) { + if (!cupsAvailable) + return; + const params = { + "printerName": printerName, + "info": info + }; + + DMSService.sendRequest("cups.setPrinterInfo", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to update description") + " - " + response.error); + } else { + getState(); + } + }); + } + + function printTestPage(printerName) { + if (!cupsAvailable) + return; + const params = { + "printerName": printerName + }; + + DMSService.sendRequest("cups.printTestPage", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to print test page") + " - " + response.error); + } else { + ToastService.showInfo(I18n.tr("Test page sent to printer")); + fetchJobsForPrinter(printerName); + } + }); + } + + function moveJob(jobID, destPrinter) { + if (!cupsAvailable) + return; + const params = { + "jobID": jobID, + "destPrinter": destPrinter + }; + + DMSService.sendRequest("cups.moveJob", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to move job") + " - " + response.error); + } else { + fetchAllJobs(); + } + }); + } + + function restartJob(jobID) { + if (!cupsAvailable) + return; + const params = { + "jobID": jobID + }; + + DMSService.sendRequest("cups.restartJob", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to restart job") + " - " + response.error); + } else { + fetchAllJobs(); + } + }); + } + + function holdJob(jobID, holdUntil) { + if (!cupsAvailable) + return; + const params = { + "jobID": jobID + }; + if (holdUntil) { + params.holdUntil = holdUntil; + } + + DMSService.sendRequest("cups.holdJob", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to hold job") + " - " + response.error); + } else { + fetchAllJobs(); + } + }); + } + + function addPrinterToClass(className, printerName) { + if (!cupsAvailable) + return; + const params = { + "className": className, + "printerName": printerName + }; + + DMSService.sendRequest("cups.addPrinterToClass", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to add printer to class") + " - " + response.error); + } else { + getClasses(); + } + }); + } + + function removePrinterFromClass(className, printerName) { + if (!cupsAvailable) + return; + const params = { + "className": className, + "printerName": printerName + }; + + DMSService.sendRequest("cups.removePrinterFromClass", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to remove printer from class") + " - " + response.error); + } else { + getClasses(); + } + }); + } + + function deleteClass(className) { + if (!cupsAvailable) + return; + const params = { + "className": className + }; + + DMSService.sendRequest("cups.deleteClass", params, response => { + if (response.error) { + ToastService.showError(I18n.tr("Failed to delete class") + " - " + response.error); + } else { + getClasses(); + } + }); + } + + function getPrinterData(printerName) { + if (!printers || !printers[printerName]) + return null; + return printers[printerName]; + } + + function getJobStateTranslation(state) { + switch (state) { + case "pending": + return I18n.tr("Pending"); + case "pending-held": + return I18n.tr("Held"); + case "processing": + return I18n.tr("Processing"); + case "processing-stopped": + return I18n.tr("Stopped"); + case "canceled": + return I18n.tr("Canceled"); + case "aborted": + return I18n.tr("Aborted"); + case "completed": + return I18n.tr("Completed"); + default: + return state; + } } readonly property var states: ({ - "idle": I18n.tr("Idle"), - "processing": I18n.tr("Processing"), - "stopped": I18n.tr("Stopped") - }) + "idle": I18n.tr("Idle"), + "processing": I18n.tr("Processing"), + "stopped": I18n.tr("Stopped") + }) readonly property var reasonsGeneral: ({ - "none": I18n.tr("None"), - "other": I18n.tr("Other") - }) + "none": I18n.tr("None"), + "other": I18n.tr("Other") + }) readonly property var reasonsSupplies: ({ - "toner-low": I18n.tr("Toner Low"), - "toner-empty": I18n.tr("Toner Empty"), - "marker-supply-low": I18n.tr("Marker Supply Low"), - "marker-supply-empty": I18n.tr("Marker Supply Empty"), - "marker-waste-almost-full": I18n.tr("Marker Waste Almost Full"), - "marker-waste-full": I18n.tr("Marker Waste Full") - }) + "toner-low": I18n.tr("Toner Low"), + "toner-empty": I18n.tr("Toner Empty"), + "marker-supply-low": I18n.tr("Marker Supply Low"), + "marker-supply-empty": I18n.tr("Marker Supply Empty"), + "marker-waste-almost-full": I18n.tr("Marker Waste Almost Full"), + "marker-waste-full": I18n.tr("Marker Waste Full") + }) readonly property var reasonsMedia: ({ - "media-low": I18n.tr("Media Low"), - "media-empty": I18n.tr("Media Empty"), - "media-needed": I18n.tr("Media Needed"), - "media-jam": I18n.tr("Media Jam") - }) + "media-low": I18n.tr("Media Low"), + "media-empty": I18n.tr("Media Empty"), + "media-needed": I18n.tr("Media Needed"), + "media-jam": I18n.tr("Media Jam") + }) readonly property var reasonsParts: ({ - "cover-open": I18n.tr("Cover Open"), - "door-open": I18n.tr("Door Open"), - "interlock-open": I18n.tr("Interlock Open"), - "output-tray-missing": I18n.tr("Output Tray Missing"), - "output-area-almost-full": I18n.tr("Output Area Almost Full"), - "output-area-full": I18n.tr("Output Area Full") - }) + "cover-open": I18n.tr("Cover Open"), + "door-open": I18n.tr("Door Open"), + "interlock-open": I18n.tr("Interlock Open"), + "output-tray-missing": I18n.tr("Output Tray Missing"), + "output-area-almost-full": I18n.tr("Output Area Almost Full"), + "output-area-full": I18n.tr("Output Area Full") + }) readonly property var reasonsErrors: ({ - "paused": I18n.tr("Paused"), - "shutdown": I18n.tr("Shutdown"), - "connecting-to-device": I18n.tr("Connecting to Device"), - "timed-out": I18n.tr("Timed Out"), - "stopping": I18n.tr("Stopping"), - "stopped-partly": I18n.tr("Stopped Partly") - }) + "paused": I18n.tr("Paused"), + "shutdown": I18n.tr("Shutdown"), + "connecting-to-device": I18n.tr("Connecting to Device"), + "timed-out": I18n.tr("Timed Out"), + "stopping": I18n.tr("Stopping"), + "stopped-partly": I18n.tr("Stopped Partly") + }) readonly property var reasonsService: ({ - "spool-area-full": I18n.tr("Spool Area Full"), - "cups-missing-filter-warning": I18n.tr("CUPS Missing Filter Warning"), - "cups-insecure-filter-warning": I18n.tr("CUPS Insecure Filter Warning") - }) + "spool-area-full": I18n.tr("Spool Area Full"), + "cups-missing-filter-warning": I18n.tr("CUPS Missing Filter Warning"), + "cups-insecure-filter-warning": I18n.tr("CUPS Insecure Filter Warning") + }) readonly property var reasonsConnectivity: ({ - "offline-report": I18n.tr("Offline Report"), - "moving-to-paused": I18n.tr("Moving to Paused") - }) + "offline-report": I18n.tr("Offline Report"), + "moving-to-paused": I18n.tr("Moving to Paused") + }) readonly property var severitySuffixes: ({ - "-error": I18n.tr("Error"), - "-warning": I18n.tr("Warning"), - "-report": I18n.tr("Report") - }) + "-error": I18n.tr("Error"), + "-warning": I18n.tr("Warning"), + "-report": I18n.tr("Report") + }) function getPrinterStateTranslation(state) { - return states[state] || state + return states[state] || state; } function getPrinterStateReasonTranslation(reason) { - let allReasons = Object.assign({}, reasonsGeneral, reasonsSupplies, reasonsMedia, reasonsParts, reasonsErrors, reasonsService, reasonsConnectivity) + let allReasons = Object.assign({}, reasonsGeneral, reasonsSupplies, reasonsMedia, reasonsParts, reasonsErrors, reasonsService, reasonsConnectivity); - let basReason = reason - let suffix = "" + let basReason = reason; + let suffix = ""; for (let s in severitySuffixes) { if (reason.endsWith(s)) { - basReason = reason.slice(0, -s.length) - suffix = severitySuffixes[s] - break + basReason = reason.slice(0, -s.length); + suffix = severitySuffixes[s]; + break; } } - let translation = allReasons[basReason] || basReason - return suffix ? translation + " (" + suffix + ")" : translation + let translation = allReasons[basReason] || basReason; + return suffix ? translation + " (" + suffix + ")" : translation; } } diff --git a/quickshell/Services/DMSService.qml b/quickshell/Services/DMSService.qml index 180b88e9..44c64f9a 100644 --- a/quickshell/Services/DMSService.qml +++ b/quickshell/Services/DMSService.qml @@ -1,8 +1,6 @@ pragma Singleton - pragma ComponentBehavior: Bound -import QtCore import QtQuick import Quickshell import Quickshell.Io @@ -14,6 +12,7 @@ Singleton { property bool dmsAvailable: false property var capabilities: [] property int apiVersion: 0 + property string cliVersion: "" readonly property int expectedApiVersion: 1 property var availablePlugins: [] property var installedPlugins: [] @@ -57,18 +56,18 @@ Singleton { Component.onCompleted: { if (socketPath && socketPath.length > 0) { - detectUpdateCommand() + detectUpdateCommand(); } } function detectUpdateCommand() { - checkingUpdateCommand = true - checkAurHelper.running = true + checkingUpdateCommand = true; + checkAurHelper.running = true; } function startSocketConnection() { if (socketPath && socketPath.length > 0) { - testProcess.running = true + testProcess.running = true; } } @@ -79,26 +78,26 @@ Singleton { stdout: StdioCollector { onStreamFinished: { - const helper = text.trim() + const helper = text.trim(); if (helper.includes("paru")) { - checkDmsPackage.helper = "paru" - checkDmsPackage.running = true + checkDmsPackage.helper = "paru"; + checkDmsPackage.running = true; } else if (helper.includes("yay")) { - checkDmsPackage.helper = "yay" - checkDmsPackage.running = true + checkDmsPackage.helper = "yay"; + checkDmsPackage.running = true; } else { - updateCommand = "dms update" - checkingUpdateCommand = false - startSocketConnection() + updateCommand = "dms update"; + checkingUpdateCommand = false; + startSocketConnection(); } } } onExited: exitCode => { if (exitCode !== 0) { - updateCommand = "dms update" - checkingUpdateCommand = false - startSocketConnection() + updateCommand = "dms update"; + checkingUpdateCommand = false; + startSocketConnection(); } } } @@ -112,22 +111,22 @@ Singleton { stdout: StdioCollector { onStreamFinished: { if (text.includes("dms-shell-git")) { - updateCommand = checkDmsPackage.helper + " -S dms-shell-git" + updateCommand = checkDmsPackage.helper + " -S dms-shell-git"; } else if (text.includes("dms-shell-bin")) { - updateCommand = checkDmsPackage.helper + " -S dms-shell-bin" + updateCommand = checkDmsPackage.helper + " -S dms-shell-bin"; } else { - updateCommand = "dms update" + updateCommand = "dms update"; } - checkingUpdateCommand = false - startSocketConnection() + checkingUpdateCommand = false; + startSocketConnection(); } } onExited: exitCode => { if (exitCode !== 0) { - updateCommand = "dms update" - checkingUpdateCommand = false - startSocketConnection() + updateCommand = "dms update"; + checkingUpdateCommand = false; + startSocketConnection(); } } } @@ -138,21 +137,21 @@ Singleton { onExited: exitCode => { if (exitCode === 0) { - root.dmsAvailable = true - connectSocket() + root.dmsAvailable = true; + connectSocket(); } else { - root.dmsAvailable = false + root.dmsAvailable = false; } } } function connectSocket() { if (!dmsAvailable || isConnected || isConnecting) { - return + return; } - isConnecting = true - requestSocket.connected = true + isConnecting = true; + requestSocket.connected = true; } DankSocket { @@ -162,32 +161,32 @@ Singleton { onConnectionStateChanged: { if (connected) { - root.isConnected = true - root.isConnecting = false - root.connectionStateChanged() - subscribeSocket.connected = true + root.isConnected = true; + root.isConnecting = false; + root.connectionStateChanged(); + subscribeSocket.connected = true; } else { - root.isConnected = false - root.isConnecting = false - root.apiVersion = 0 - root.capabilities = [] - root.connectionStateChanged() + root.isConnected = false; + root.isConnecting = false; + root.apiVersion = 0; + root.capabilities = []; + root.connectionStateChanged(); } } parser: SplitParser { onRead: line => { if (!line || line.length === 0) { - return + return; } - console.log("DMSService: Request socket <<", line) + console.log("DMSService: Request socket <<", line); try { - const response = JSON.parse(line) - handleResponse(response) + const response = JSON.parse(line); + handleResponse(response); } catch (e) { - console.warn("DMSService: Failed to parse request response:", line, e) + console.warn("DMSService: Failed to parse request response:", line, e); } } } @@ -199,25 +198,25 @@ Singleton { connected: false onConnectionStateChanged: { - root.subscribeConnected = connected + root.subscribeConnected = connected; if (connected) { - sendSubscribeRequest() + sendSubscribeRequest(); } } parser: SplitParser { onRead: line => { if (!line || line.length === 0) { - return + return; } - console.log("DMSService: Subscribe socket <<", line) + console.log("DMSService: Subscribe socket <<", line); try { - const response = JSON.parse(line) - handleSubscriptionEvent(response) + const response = JSON.parse(line); + handleSubscriptionEvent(response); } catch (e) { - console.warn("DMSService: Failed to parse subscription event:", line, e) + console.warn("DMSService: Failed to parse subscription event:", line, e); } } } @@ -226,319 +225,317 @@ Singleton { function sendSubscribeRequest() { const request = { "method": "subscribe" - } + }; if (activeSubscriptions.length > 0) { request.params = { "services": activeSubscriptions - } - console.log("DMSService: Subscribing to services:", JSON.stringify(activeSubscriptions)) + }; + console.log("DMSService: Subscribing to services:", JSON.stringify(activeSubscriptions)); } else { - console.log("DMSService: Subscribing to all services") + console.log("DMSService: Subscribing to all services"); } - subscribeSocket.send(request) + subscribeSocket.send(request); } function subscribe(services) { if (!Array.isArray(services)) { - services = [services] + services = [services]; } - activeSubscriptions = services + activeSubscriptions = services; if (subscribeConnected) { - subscribeSocket.connected = false + subscribeSocket.connected = false; Qt.callLater(() => { - subscribeSocket.connected = true - }) + subscribeSocket.connected = true; + }); } } function addSubscription(service) { - if (activeSubscriptions.includes("all")) { - console.warn("DMSService: Cannot add specific subscription when subscribed to 'all'") - return - } - + if (activeSubscriptions.includes("all")) + return; if (!activeSubscriptions.includes(service)) { - const newSubs = [...activeSubscriptions, service] - subscribe(newSubs) + const newSubs = [...activeSubscriptions, service]; + subscribe(newSubs); } } function removeSubscription(service) { if (activeSubscriptions.includes("all")) { - const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace"] - const filtered = allServices.filter(s => s !== service) - subscribe(filtered) + const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace"]; + const filtered = allServices.filter(s => s !== service); + subscribe(filtered); } else { - const filtered = activeSubscriptions.filter(s => s !== service) + const filtered = activeSubscriptions.filter(s => s !== service); if (filtered.length === 0) { - console.warn("DMSService: Cannot remove last subscription") - return + console.warn("DMSService: Cannot remove last subscription"); + return; } - subscribe(filtered) + subscribe(filtered); } } function subscribeAll() { - subscribe(["all"]) + subscribe(["all"]); } function subscribeAllExcept(excludeServices) { if (!Array.isArray(excludeServices)) { - excludeServices = [excludeServices] + excludeServices = [excludeServices]; } - const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace"] - const filtered = allServices.filter(s => !excludeServices.includes(s)) - subscribe(filtered) + const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace"]; + const filtered = allServices.filter(s => !excludeServices.includes(s)); + subscribe(filtered); } function handleSubscriptionEvent(response) { if (response.error) { if (response.error.includes("unknown method") && response.error.includes("subscribe")) { if (!shownOutdatedError) { - console.error("DMSService: Server does not support subscribe method") - ToastService.showError(I18n.tr("DMS out of date"), I18n.tr("To update, run the following command:"), updateCommand) - shownOutdatedError = true + console.error("DMSService: Server does not support subscribe method"); + ToastService.showError(I18n.tr("DMS out of date"), I18n.tr("To update, run the following command:"), updateCommand); + shownOutdatedError = true; } } - return + return; } if (!response.result) { - return + return; } - const service = response.result.service - const data = response.result.data + const service = response.result.service; + const data = response.result.data; if (service === "server") { - apiVersion = data.apiVersion || 0 - capabilities = data.capabilities || [] + apiVersion = data.apiVersion || 0; + cliVersion = data.cliVersion || ""; + capabilities = data.capabilities || []; - console.info("DMSService: Connected (API v" + apiVersion + ") -", JSON.stringify(capabilities)) + console.info("DMSService: Connected (API v" + apiVersion + ", CLI " + cliVersion + ") -", JSON.stringify(capabilities)); if (apiVersion < expectedApiVersion) { - ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")") + ToastService.showError("DMS server is outdated (API v" + apiVersion + ", expected v" + expectedApiVersion + ")"); } - capabilitiesReceived() + capabilitiesReceived(); } else if (service === "network") { - networkStateUpdate(data) + networkStateUpdate(data); } else if (service === "network.credentials") { - credentialsRequest(data) + credentialsRequest(data); } else if (service === "loginctl") { if (data.event) { - loginctlEvent(data) + loginctlEvent(data); } else { - loginctlStateUpdate(data) + loginctlStateUpdate(data); } } else if (service === "bluetooth.pairing") { - bluetoothPairingRequest(data) + bluetoothPairingRequest(data); } else if (service === "cups") { - cupsStateUpdate(data) + cupsStateUpdate(data); } else if (service === "dwl") { - dwlStateUpdate(data) + dwlStateUpdate(data); } else if (service === "brightness") { - brightnessStateUpdate(data) + brightnessStateUpdate(data); } else if (service === "brightness.update") { if (data.device) { - brightnessDeviceUpdate(data.device) + brightnessDeviceUpdate(data.device); } } else if (service === "extworkspace") { - extWorkspaceStateUpdate(data) + extWorkspaceStateUpdate(data); } else if (service === "wlroutput") { - wlrOutputStateUpdate(data) + wlrOutputStateUpdate(data); } else if (service === "evdev") { if (data.capsLock !== undefined) { - capsLockState = data.capsLock + capsLockState = data.capsLock; } - evdevStateUpdate(data) + evdevStateUpdate(data); } } function sendRequest(method, params, callback) { if (!isConnected) { - console.warn("DMSService.sendRequest: Not connected, method:", method) + console.warn("DMSService.sendRequest: Not connected, method:", method); if (callback) { callback({ - "error": "not connected to DMS socket" - }) + "error": "not connected to DMS socket" + }); } - return + return; } - requestIdCounter++ - const id = Date.now() + requestIdCounter + requestIdCounter++; + const id = Date.now() + requestIdCounter; const request = { "id": id, "method": method - } + }; if (params) { - request.params = params + request.params = params; } if (callback) { - pendingRequests[id] = callback + pendingRequests[id] = callback; } - console.log("DMSService.sendRequest: Sending request id=" + id + " method=" + method) - requestSocket.send(request) + console.log("DMSService.sendRequest: Sending request id=" + id + " method=" + method); + requestSocket.send(request); } function handleResponse(response) { - const callback = pendingRequests[response.id] + const callback = pendingRequests[response.id]; if (callback) { - delete pendingRequests[response.id] - callback(response) + delete pendingRequests[response.id]; + callback(response); } } function ping(callback) { - sendRequest("ping", null, callback) + sendRequest("ping", null, callback); } function listPlugins(callback) { sendRequest("plugins.list", null, response => { - if (response.result) { - availablePlugins = response.result - pluginsListReceived(response.result) - } - if (callback) { - callback(response) - } - }) + if (response.result) { + availablePlugins = response.result; + pluginsListReceived(response.result); + } + if (callback) { + callback(response); + } + }); } function listInstalled(callback) { sendRequest("plugins.listInstalled", null, response => { - if (response.result) { - installedPlugins = response.result - installedPluginsReceived(response.result) - } - if (callback) { - callback(response) - } - }) + if (response.result) { + installedPlugins = response.result; + installedPluginsReceived(response.result); + } + if (callback) { + callback(response); + } + }); } function search(query, category, compositor, capability, callback) { const params = { "query": query - } + }; if (category) { - params.category = category + params.category = category; } if (compositor) { - params.compositor = compositor + params.compositor = compositor; } if (capability) { - params.capability = capability + params.capability = capability; } sendRequest("plugins.search", params, response => { - if (response.result) { - searchResultsReceived(response.result) - } - if (callback) { - callback(response) - } - }) + if (response.result) { + searchResultsReceived(response.result); + } + if (callback) { + callback(response); + } + }); } function install(pluginName, callback) { sendRequest("plugins.install", { - "name": pluginName - }, response => { - if (callback) { - callback(response) - } - if (!response.error) { - listInstalled() - } - }) + "name": pluginName + }, response => { + if (callback) { + callback(response); + } + if (!response.error) { + listInstalled(); + } + }); } function uninstall(pluginName, callback) { sendRequest("plugins.uninstall", { - "name": pluginName - }, response => { - if (callback) { - callback(response) - } - if (!response.error) { - listInstalled() - } - }) + "name": pluginName + }, response => { + if (callback) { + callback(response); + } + if (!response.error) { + listInstalled(); + } + }); } function update(pluginName, callback) { sendRequest("plugins.update", { - "name": pluginName - }, response => { - if (callback) { - callback(response) - } - if (!response.error) { - listInstalled() - } - }) + "name": pluginName + }, response => { + if (callback) { + callback(response); + } + if (!response.error) { + listInstalled(); + } + }); } function lockSession(callback) { - sendRequest("loginctl.lock", null, callback) + sendRequest("loginctl.lock", null, callback); } function unlockSession(callback) { - sendRequest("loginctl.unlock", null, callback) + sendRequest("loginctl.unlock", null, callback); } function bluetoothPair(devicePath, callback) { sendRequest("bluetooth.pair", { - "device": devicePath - }, callback) + "device": devicePath + }, callback); } function bluetoothConnect(devicePath, callback) { sendRequest("bluetooth.connect", { - "device": devicePath - }, callback) + "device": devicePath + }, callback); } function bluetoothDisconnect(devicePath, callback) { sendRequest("bluetooth.disconnect", { - "device": devicePath - }, callback) + "device": devicePath + }, callback); } function bluetoothRemove(devicePath, callback) { sendRequest("bluetooth.remove", { - "device": devicePath - }, callback) + "device": devicePath + }, callback); } function bluetoothTrust(devicePath, callback) { sendRequest("bluetooth.trust", { - "device": devicePath - }, callback) + "device": devicePath + }, callback); } function bluetoothSubmitPairing(token, secrets, accept, callback) { sendRequest("bluetooth.pairing.submit", { - "token": token, - "secrets": secrets, - "accept": accept - }, callback) + "token": token, + "secrets": secrets, + "accept": accept + }, callback); } function bluetoothCancelPairing(token, callback) { sendRequest("bluetooth.pairing.cancel", { - "token": token - }, callback) + "token": token + }, callback); } } diff --git a/quickshell/Widgets/DankDropdown.qml b/quickshell/Widgets/DankDropdown.qml index 2e163976..fe1a1cb9 100644 --- a/quickshell/Widgets/DankDropdown.qml +++ b/quickshell/Widgets/DankDropdown.qml @@ -15,6 +15,17 @@ Item { property var options: [] property var optionIcons: [] property bool enableFuzzySearch: false + + onOptionsChanged: { + if (dropdownMenu.visible) { + dropdownMenu.fzfFinder = new Fzf.Finder(options, { + "selector": option => option, + "limit": 50, + "casing": "case-insensitive" + }); + dropdownMenu.updateFilteredOptions(); + } + } property int popupWidthOffset: 0 property int maxPopupHeight: 400 property bool openUpwards: false @@ -30,9 +41,9 @@ Item { implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM) Component.onDestruction: { - const popup = dropdownMenu + const popup = dropdownMenu; if (popup && popup.visible) { - popup.close() + popup.close(); } } @@ -85,38 +96,38 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { if (dropdownMenu.visible) { - dropdownMenu.close() - return + dropdownMenu.close(); + return; } - dropdownMenu.searchQuery = "" - dropdownMenu.updateFilteredOptions() + dropdownMenu.searchQuery = ""; + dropdownMenu.updateFilteredOptions(); - dropdownMenu.open() + dropdownMenu.open(); - const pos = dropdown.mapToItem(Overlay.overlay, 0, 0) - const popupWidth = dropdownMenu.width - const popupHeight = dropdownMenu.height - const overlayHeight = Overlay.overlay.height + const pos = dropdown.mapToItem(Overlay.overlay, 0, 0); + const popupWidth = dropdownMenu.width; + const popupHeight = dropdownMenu.height; + const overlayHeight = Overlay.overlay.height; if (root.openUpwards || pos.y + dropdown.height + popupHeight + 4 > overlayHeight) { if (root.alignPopupRight) { - dropdownMenu.x = pos.x + dropdown.width - popupWidth + dropdownMenu.x = pos.x + dropdown.width - popupWidth; } else { - dropdownMenu.x = pos.x - (root.popupWidthOffset / 2) + dropdownMenu.x = pos.x - (root.popupWidthOffset / 2); } - dropdownMenu.y = pos.y - popupHeight - 4 + dropdownMenu.y = pos.y - popupHeight - 4; } else { if (root.alignPopupRight) { - dropdownMenu.x = pos.x + dropdown.width - popupWidth + dropdownMenu.x = pos.x + dropdown.width - popupWidth; } else { - dropdownMenu.x = pos.x - (root.popupWidthOffset / 2) + dropdownMenu.x = pos.x - (root.popupWidthOffset / 2); } - dropdownMenu.y = pos.y + dropdown.height + 4 + dropdownMenu.y = pos.y + dropdown.height + 4; } if (root.enableFuzzySearch && searchField.visible) { - searchField.forceActiveFocus() + searchField.forceActiveFocus(); } } } @@ -133,8 +144,8 @@ Item { DankIcon { name: { - const currentIndex = root.options.indexOf(root.currentValue) - return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : "" + const currentIndex = root.options.indexOf(root.currentValue); + return currentIndex >= 0 && root.optionIcons.length > currentIndex ? root.optionIcons[currentIndex] : ""; } size: 18 color: Theme.surfaceText @@ -186,39 +197,39 @@ Item { function updateFilteredOptions() { if (!root.enableFuzzySearch || searchQuery.length === 0) { - filteredOptions = root.options - selectedIndex = -1 - return + filteredOptions = root.options; + selectedIndex = -1; + return; } - const results = fzfFinder.find(searchQuery) - filteredOptions = results.map(result => result.item) - selectedIndex = -1 + const results = fzfFinder.find(searchQuery); + filteredOptions = results.map(result => result.item); + selectedIndex = -1; } function selectNext() { if (filteredOptions.length === 0) { - return + return; } - selectedIndex = (selectedIndex + 1) % filteredOptions.length - listView.positionViewAtIndex(selectedIndex, ListView.Contain) + selectedIndex = (selectedIndex + 1) % filteredOptions.length; + listView.positionViewAtIndex(selectedIndex, ListView.Contain); } function selectPrevious() { if (filteredOptions.length === 0) { - return + return; } - selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1 - listView.positionViewAtIndex(selectedIndex, ListView.Contain) + selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1; + listView.positionViewAtIndex(selectedIndex, ListView.Contain); } function selectCurrent() { if (selectedIndex < 0 || selectedIndex >= filteredOptions.length) { - return + return; } - root.currentValue = filteredOptions[selectedIndex] - root.valueChanged(filteredOptions[selectedIndex]) - close() + root.currentValue = filteredOptions[selectedIndex]; + root.valueChanged(filteredOptions[selectedIndex]); + close(); } parent: Overlay.overlay @@ -270,8 +281,8 @@ Item { topPadding: Theme.spacingS bottomPadding: Theme.spacingS onTextChanged: { - dropdownMenu.searchQuery = text - dropdownMenu.updateFilteredOptions() + dropdownMenu.searchQuery = text; + dropdownMenu.updateFilteredOptions(); } Keys.onDownPressed: dropdownMenu.selectNext() Keys.onUpPressed: dropdownMenu.selectPrevious() @@ -279,17 +290,17 @@ Item { Keys.onEnterPressed: dropdownMenu.selectCurrent() Keys.onPressed: event => { if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) { - dropdownMenu.selectNext() - event.accepted = true + dropdownMenu.selectNext(); + event.accepted = true; } else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) { - dropdownMenu.selectPrevious() - event.accepted = true + dropdownMenu.selectPrevious(); + event.accepted = true; } else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) { - dropdownMenu.selectNext() - event.accepted = true + dropdownMenu.selectNext(); + event.accepted = true; } else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) { - dropdownMenu.selectPrevious() - event.accepted = true + dropdownMenu.selectPrevious(); + event.accepted = true; } } } @@ -362,9 +373,9 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - root.currentValue = modelData - root.valueChanged(modelData) - dropdownMenu.close() + root.currentValue = modelData; + root.valueChanged(modelData); + dropdownMenu.close(); } } }