├── .gitignore ├── .pylintrc ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── __pycache__ └── find_entries.cpython-310.pyc ├── docs └── preferences.json.md ├── grub-editor.desktop ├── grub-editor.png ├── grub-editor.py ├── grubEditor ├── __init__.py ├── __main__.py ├── core.py ├── libs │ ├── __init__.py │ ├── find_entries.py │ ├── qt_functools.py │ └── worker.py ├── locations.py ├── main.py ├── ui │ ├── chroot.ui │ ├── chroot_after.ui │ ├── chroot_loading.ui │ ├── create_snapshot_dialog.ui │ ├── dialog.ui │ ├── issues.ui │ ├── main.ui │ ├── main1.ui │ ├── progress.ui │ ├── set_recommendations.ui │ ├── snapshots_.ui │ └── treeWidgetTest.ui └── widgets │ ├── __init__.py │ ├── dialog.py │ ├── elided_label.py │ ├── error_dialog.py │ ├── loading_bar.py │ ├── progress.py │ ├── ui │ ├── error_dialog.ui │ └── view_snapshot.ui │ └── view_mode_popup.py ├── screenshots ├── grub-editor0.png ├── grub-editor1.png ├── grub-editor2.png ├── light-screenshot0.png └── light-screenshot1.png ├── tests ├── pytest.ini ├── test_edit_configurations.py ├── test_fix_kernel_version.py ├── test_get_set_value.py ├── test_grubcfg_error.py ├── test_invalid_default_entry.py ├── test_output_widget.py ├── test_progress.py ├── test_quotes_values.py ├── test_reinstall_grub_package.py ├── test_remove_value.py ├── test_snapshots.py ├── test_widget_dialog.py └── tools.py └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | temp* 2 | temp1.py 3 | temp2.py 4 | temp3.py 5 | temp4.py 6 | temp5.py 7 | tempe.py 8 | temp7.py 9 | test_json.py 10 | temp8.py 11 | PKGBUILD 12 | *.pyc 13 | __pycache__ 14 | .qt_for_python/ 15 | pytest.ini 16 | .vscode/ -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist= 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold to be exceeded before program exits with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=CVS 46 | 47 | # Add files or directories matching the regex patterns to the ignore-list. The 48 | # regex matches against paths and can be in Posix or Windows format. 49 | ignore-paths= 50 | 51 | # Files or directories matching the regex patterns are skipped. The regex 52 | # matches against base names, not paths. The default value ignores Emacs file 53 | # locks 54 | ignore-patterns=^\.# 55 | 56 | # List of module names for which member attributes should not be checked 57 | # (useful for modules/projects where namespaces are manipulated during runtime 58 | # and thus existing member attributes cannot be deduced by static analysis). It 59 | # supports qualified module names, as well as Unix pattern matching. 60 | ignored-modules= 61 | 62 | # Python code to execute, usually for sys.path manipulation such as 63 | # pygtk.require(). 64 | #init-hook= 65 | 66 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 67 | # number of processors available to use. 68 | jobs=1 69 | 70 | # Control the amount of potential inferred values when inferring a single 71 | # object. This can help the performance when dealing with large functions or 72 | # complex, nested conditions. 73 | limit-inference-results=100 74 | 75 | # List of plugins (as comma separated values of python module names) to load, 76 | # usually to register additional checkers. 77 | load-plugins= 78 | 79 | # Pickle collected data for later comparisons. 80 | persistent=yes 81 | 82 | # Minimum Python version to use for version dependent checks. Will default to 83 | # the version used to run pylint. 84 | py-version=3.10 85 | 86 | # Discover python modules and packages in the file system subtree. 87 | recursive=no 88 | 89 | # When enabled, pylint would attempt to guess common misconfiguration and emit 90 | # user-friendly hints instead of false-positive error messages. 91 | suggestion-mode=yes 92 | 93 | # Allow loading of arbitrary C extensions. Extensions are imported into the 94 | # active Python interpreter and may run arbitrary code. 95 | unsafe-load-any-extension=no 96 | 97 | # In verbose mode, extra non-checker-related info will be displayed. 98 | #verbose= 99 | 100 | 101 | [REPORTS] 102 | 103 | # Python expression which should return a score less than or equal to 10. You 104 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 105 | # 'convention', and 'info' which contain the number of messages in each 106 | # category, as well as 'statement' which is the total number of statements 107 | # analyzed. This score is used by the global evaluation report (RP0004). 108 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 109 | 110 | # Template used to display messages. This is a python new-style format string 111 | # used to format the message information. See doc for all details. 112 | msg-template= 113 | 114 | # Set the output format. Available formats are text, parseable, colorized, json 115 | # and msvs (visual studio). You can also give a reporter class, e.g. 116 | # mypackage.mymodule.MyReporterClass. 117 | #output-format= 118 | 119 | # Tells whether to display a full report or only the messages. 120 | reports=no 121 | 122 | # Activate the evaluation score. 123 | score=yes 124 | 125 | 126 | [MESSAGES CONTROL] 127 | 128 | # Only show warnings with the listed confidence levels. Leave empty to show 129 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 130 | # UNDEFINED. 131 | confidence=HIGH, 132 | CONTROL_FLOW, 133 | INFERENCE, 134 | INFERENCE_FAILURE, 135 | UNDEFINED 136 | 137 | # Disable the message, report, category or checker with the given id(s). You 138 | # can either give multiple identifiers separated by comma (,) or put this 139 | # option multiple times (only on the command line, not in the configuration 140 | # file where it should appear only once). You can also use "--disable=all" to 141 | # disable everything first and then re-enable specific checks. For example, if 142 | # you want to run only the similarities checker, you can use "--disable=all 143 | # --enable=similarities". If you want to run only the classes checker, but have 144 | # no Warning level messages displayed, use "--disable=all --enable=classes 145 | # --disable=W". 146 | disable=raw-checker-failed, 147 | bad-inline-option, 148 | locally-disabled, 149 | file-ignored, 150 | suppressed-message, 151 | useless-suppression, 152 | deprecated-pragma, 153 | use-symbolic-message-instead, 154 | no-name-in-module, 155 | unused-import, 156 | subprocess-run-check, 157 | unspecified-encoding, 158 | trailing-whitespace, 159 | c-extension-no-member, 160 | invalid-name, 161 | wrong-import-position, 162 | import-error, 163 | attribute-defined-outside-init, 164 | unused-wildcard-import, 165 | wildcard-import, 166 | missing-function-docstring, 167 | missing-class-docstring, 168 | global-at-module-level 169 | 170 | ; disable=all 171 | 172 | # Enable the message, report, category or checker with the given id(s). You can 173 | # either give multiple identifier separated by comma (,) or put this option 174 | # multiple time (only on the command line, not in the configuration file where 175 | # it should appear only once). See also the "--disable" option for examples. 176 | #enable= 177 | 178 | 179 | [BASIC] 180 | 181 | # Naming style matching correct argument names. 182 | argument-naming-style=snake_case 183 | 184 | # Regular expression matching correct argument names. Overrides argument- 185 | # naming-style. If left empty, argument names will be checked with the set 186 | # naming style. 187 | #argument-rgx= 188 | 189 | # Naming style matching correct attribute names. 190 | attr-naming-style=snake_case 191 | 192 | # Regular expression matching correct attribute names. Overrides attr-naming- 193 | # style. If left empty, attribute names will be checked with the set naming 194 | # style. 195 | #attr-rgx= 196 | 197 | # Bad variable names which should always be refused, separated by a comma. 198 | bad-names=foo, 199 | bar, 200 | baz, 201 | toto, 202 | tutu, 203 | tata 204 | 205 | # Bad variable names regexes, separated by a comma. If names match any regex, 206 | # they will always be refused 207 | bad-names-rgxs= 208 | 209 | # Naming style matching correct class attribute names. 210 | class-attribute-naming-style=any 211 | 212 | # Regular expression matching correct class attribute names. Overrides class- 213 | # attribute-naming-style. If left empty, class attribute names will be checked 214 | # with the set naming style. 215 | #class-attribute-rgx= 216 | 217 | # Naming style matching correct class constant names. 218 | class-const-naming-style=UPPER_CASE 219 | 220 | # Regular expression matching correct class constant names. Overrides class- 221 | # const-naming-style. If left empty, class constant names will be checked with 222 | # the set naming style. 223 | #class-const-rgx= 224 | 225 | # Naming style matching correct class names. 226 | class-naming-style=PascalCase 227 | 228 | # Regular expression matching correct class names. Overrides class-naming- 229 | # style. If left empty, class names will be checked with the set naming style. 230 | #class-rgx= 231 | 232 | # Naming style matching correct constant names. 233 | const-naming-style=UPPER_CASE 234 | 235 | # Regular expression matching correct constant names. Overrides const-naming- 236 | # style. If left empty, constant names will be checked with the set naming 237 | # style. 238 | #const-rgx= 239 | 240 | # Minimum line length for functions/classes that require docstrings, shorter 241 | # ones are exempt. 242 | docstring-min-length=-1 243 | 244 | # Naming style matching correct function names. 245 | function-naming-style=snake_case 246 | 247 | # Regular expression matching correct function names. Overrides function- 248 | # naming-style. If left empty, function names will be checked with the set 249 | # naming style. 250 | #function-rgx= 251 | 252 | # Good variable names which should always be accepted, separated by a comma. 253 | good-names=i, 254 | j, 255 | k, 256 | ex, 257 | Run, 258 | _ 259 | 260 | # Good variable names regexes, separated by a comma. If names match any regex, 261 | # they will always be accepted 262 | good-names-rgxs= 263 | 264 | # Include a hint for the correct naming format with invalid-name. 265 | include-naming-hint=no 266 | 267 | # Naming style matching correct inline iteration names. 268 | inlinevar-naming-style=any 269 | 270 | # Regular expression matching correct inline iteration names. Overrides 271 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 272 | # with the set naming style. 273 | #inlinevar-rgx= 274 | 275 | # Naming style matching correct method names. 276 | method-naming-style=snake_case 277 | 278 | # Regular expression matching correct method names. Overrides method-naming- 279 | # style. If left empty, method names will be checked with the set naming style. 280 | #method-rgx= 281 | 282 | # Naming style matching correct module names. 283 | module-naming-style=snake_case 284 | 285 | # Regular expression matching correct module names. Overrides module-naming- 286 | # style. If left empty, module names will be checked with the set naming style. 287 | #module-rgx= 288 | 289 | # Colon-delimited sets of names that determine each other's naming style when 290 | # the name regexes allow several styles. 291 | name-group= 292 | 293 | # Regular expression which should only match function or class names that do 294 | # not require a docstring. 295 | no-docstring-rgx=^_ 296 | 297 | # List of decorators that produce properties, such as abc.abstractproperty. Add 298 | # to this list to register other decorators that produce valid properties. 299 | # These decorators are taken in consideration only for invalid-name. 300 | property-classes=abc.abstractproperty 301 | 302 | # Regular expression matching correct type variable names. If left empty, type 303 | # variable names will be checked with the set naming style. 304 | #typevar-rgx= 305 | 306 | # Naming style matching correct variable names. 307 | variable-naming-style=snake_case 308 | 309 | # Regular expression matching correct variable names. Overrides variable- 310 | # naming-style. If left empty, variable names will be checked with the set 311 | # naming style. 312 | #variable-rgx= 313 | 314 | 315 | [VARIABLES] 316 | 317 | # List of additional names supposed to be defined in builtins. Remember that 318 | # you should avoid defining new builtins when possible. 319 | additional-builtins= 320 | 321 | # Tells whether unused global variables should be treated as a violation. 322 | allow-global-unused-variables=yes 323 | 324 | # List of names allowed to shadow builtins 325 | allowed-redefined-builtins= 326 | 327 | # List of strings which can identify a callback function by name. A callback 328 | # name must start or end with one of those strings. 329 | callbacks=cb_, 330 | _cb 331 | 332 | # A regular expression matching the name of dummy variables (i.e. expected to 333 | # not be used). 334 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 335 | 336 | # Argument names that match this expression will be ignored. Default to name 337 | # with leading underscore. 338 | ignored-argument-names=_.*|^ignored_|^unused_ 339 | 340 | # Tells whether we should check for unused import in __init__ files. 341 | init-import=no 342 | 343 | # List of qualified module names which can have objects that can redefine 344 | # builtins. 345 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 346 | 347 | 348 | [MISCELLANEOUS] 349 | 350 | # List of note tags to take in consideration, separated by a comma. 351 | notes=FIXME, 352 | XXX, 353 | TODO 354 | 355 | # Regular expression of note tags to take in consideration. 356 | notes-rgx= 357 | 358 | 359 | [CLASSES] 360 | 361 | # Warn about protected attribute access inside special methods 362 | check-protected-access-in-special-methods=no 363 | 364 | # List of method names used to declare (i.e. assign) instance attributes. 365 | defining-attr-methods=__init__, 366 | __new__, 367 | setUp, 368 | __post_init__ 369 | 370 | # List of member names, which should be excluded from the protected access 371 | # warning. 372 | exclude-protected=_asdict, 373 | _fields, 374 | _replace, 375 | _source, 376 | _make 377 | 378 | # List of valid names for the first argument in a class method. 379 | valid-classmethod-first-arg=cls 380 | 381 | # List of valid names for the first argument in a metaclass class method. 382 | valid-metaclass-classmethod-first-arg=cls 383 | 384 | 385 | [EXCEPTIONS] 386 | 387 | # Exceptions that will emit a warning when caught. 388 | overgeneral-exceptions=BaseException, 389 | Exception 390 | 391 | 392 | [FORMAT] 393 | 394 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 395 | expected-line-ending-format= 396 | 397 | # Regexp for a line that is allowed to be longer than the limit. 398 | ignore-long-lines=^\s*(# )??$ 399 | 400 | # Number of spaces of indent required inside a hanging or continued line. 401 | indent-after-paren=4 402 | 403 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 404 | # tab). 405 | indent-string=' ' 406 | 407 | # Maximum number of characters on a single line. 408 | max-line-length=100 409 | 410 | # Maximum number of lines in a module. 411 | max-module-lines=1000 412 | 413 | # Allow the body of a class to be on the same line as the declaration if body 414 | # contains single statement. 415 | single-line-class-stmt=no 416 | 417 | # Allow the body of an if to be on the same line as the test if there is no 418 | # else. 419 | single-line-if-stmt=no 420 | 421 | 422 | [SPELLING] 423 | 424 | # Limits count of emitted suggestions for spelling mistakes. 425 | max-spelling-suggestions=4 426 | 427 | # Spelling dictionary name. Available dictionaries: none. To make it work, 428 | # install the 'python-enchant' package. 429 | spelling-dict= 430 | 431 | # List of comma separated words that should be considered directives if they 432 | # appear at the beginning of a comment and should not be checked. 433 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 434 | 435 | # List of comma separated words that should not be checked. 436 | spelling-ignore-words= 437 | 438 | # A path to a file that contains the private dictionary; one word per line. 439 | spelling-private-dict-file= 440 | 441 | # Tells whether to store unknown words to the private dictionary (see the 442 | # --spelling-private-dict-file option) instead of raising a message. 443 | spelling-store-unknown-words=no 444 | 445 | 446 | [LOGGING] 447 | 448 | # The type of string formatting that logging methods do. `old` means using % 449 | # formatting, `new` is for `{}` formatting. 450 | logging-format-style=old 451 | 452 | # Logging modules to check that the string format arguments are in logging 453 | # function parameter format. 454 | logging-modules=logging 455 | 456 | 457 | [STRING] 458 | 459 | # This flag controls whether inconsistent-quotes generates a warning when the 460 | # character used as a quote delimiter is used inconsistently within a module. 461 | check-quote-consistency=no 462 | 463 | # This flag controls whether the implicit-str-concat should generate a warning 464 | # on implicit string concatenation in sequences defined over several lines. 465 | check-str-concat-over-line-jumps=no 466 | 467 | 468 | [SIMILARITIES] 469 | 470 | # Comments are removed from the similarity computation 471 | ignore-comments=yes 472 | 473 | # Docstrings are removed from the similarity computation 474 | ignore-docstrings=yes 475 | 476 | # Imports are removed from the similarity computation 477 | ignore-imports=yes 478 | 479 | # Signatures are removed from the similarity computation 480 | ignore-signatures=yes 481 | 482 | # Minimum lines number of a similarity. 483 | min-similarity-lines=4 484 | 485 | 486 | [REFACTORING] 487 | 488 | # Maximum number of nested blocks for function / method body 489 | max-nested-blocks=5 490 | 491 | # Complete name of functions that never returns. When checking for 492 | # inconsistent-return-statements if a never returning function is called then 493 | # it will be considered as an explicit return statement and no message will be 494 | # printed. 495 | never-returning-functions=sys.exit,argparse.parse_error 496 | 497 | 498 | [IMPORTS] 499 | 500 | # List of modules that can be imported at any level, not just the top level 501 | # one. 502 | allow-any-import-level= 503 | 504 | # Allow wildcard imports from modules that define __all__. 505 | allow-wildcard-with-all=no 506 | 507 | # Deprecated modules which should not be used, separated by a comma. 508 | deprecated-modules= 509 | 510 | # Output a graph (.gv or any supported image format) of external dependencies 511 | # to the given file (report RP0402 must not be disabled). 512 | ext-import-graph= 513 | 514 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 515 | # external) dependencies to the given file (report RP0402 must not be 516 | # disabled). 517 | import-graph= 518 | 519 | # Output a graph (.gv or any supported image format) of internal dependencies 520 | # to the given file (report RP0402 must not be disabled). 521 | int-import-graph= 522 | 523 | # Force import order to recognize a module as part of the standard 524 | # compatibility libraries. 525 | known-standard-library= 526 | 527 | # Force import order to recognize a module as part of a third party library. 528 | known-third-party=enchant 529 | 530 | # Couples of modules and preferred modules, separated by a comma. 531 | preferred-modules= 532 | 533 | 534 | [DESIGN] 535 | 536 | # List of regular expressions of class ancestor names to ignore when counting 537 | # public methods (see R0903) 538 | exclude-too-few-public-methods= 539 | 540 | # List of qualified class names to ignore when counting class parents (see 541 | # R0901) 542 | ignored-parents= 543 | 544 | # Maximum number of arguments for function / method. 545 | max-args=5 546 | 547 | # Maximum number of attributes for a class (see R0902). 548 | max-attributes=7 549 | 550 | # Maximum number of boolean expressions in an if statement (see R0916). 551 | max-bool-expr=5 552 | 553 | # Maximum number of branch for function / method body. 554 | max-branches=12 555 | 556 | # Maximum number of locals for function / method body. 557 | max-locals=15 558 | 559 | # Maximum number of parents for a class (see R0901). 560 | max-parents=7 561 | 562 | # Maximum number of public methods for a class (see R0904). 563 | max-public-methods=20 564 | 565 | # Maximum number of return / yield for function / method body. 566 | max-returns=6 567 | 568 | # Maximum number of statements in function / method body. 569 | max-statements=50 570 | 571 | # Minimum number of public methods for a class (see R0903). 572 | min-public-methods=2 573 | 574 | 575 | [TYPECHECK] 576 | 577 | # List of decorators that produce context managers, such as 578 | # contextlib.contextmanager. Add to this list to register other decorators that 579 | # produce valid context managers. 580 | contextmanager-decorators=contextlib.contextmanager 581 | 582 | # List of members which are set dynamically and missed by pylint inference 583 | # system, and so shouldn't trigger E1101 when accessed. Python regular 584 | # expressions are accepted. 585 | generated-members= 586 | 587 | # Tells whether to warn about missing members when the owner of the attribute 588 | # is inferred to be None. 589 | ignore-none=yes 590 | 591 | # This flag controls whether pylint should warn about no-member and similar 592 | # checks whenever an opaque object is returned when inferring. The inference 593 | # can return multiple potential results while evaluating a Python object, but 594 | # some branches might not be evaluated, which results in partial inference. In 595 | # that case, it might be useful to still emit no-member and other checks for 596 | # the rest of the inferred objects. 597 | ignore-on-opaque-inference=yes 598 | 599 | # List of symbolic message names to ignore for Mixin members. 600 | ignored-checks-for-mixins=no-member, 601 | not-async-context-manager, 602 | not-context-manager, 603 | attribute-defined-outside-init 604 | 605 | # List of class names for which member attributes should not be checked (useful 606 | # for classes with dynamically set attributes). This supports the use of 607 | # qualified names. 608 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 609 | 610 | # Show a hint with possible names when a member name was not found. The aspect 611 | # of finding the hint is based on edit distance. 612 | missing-member-hint=yes 613 | 614 | # The minimum edit distance a name should have in order to be considered a 615 | # similar match for a missing member name. 616 | missing-member-hint-distance=1 617 | 618 | # The total number of similar names that should be taken in consideration when 619 | # showing a hint for a missing member. 620 | missing-member-max-choices=1 621 | 622 | # Regex pattern to define which classes are considered mixins. 623 | mixin-class-rgx=.*[Mm]ixin 624 | 625 | # List of decorators that change the signature of a decorated function. 626 | signature-mutators= 627 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022 Scott Chacon and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESKTOP_FILE=grub-editor.desktop 2 | INSTALL_PATH=$(PKG_DIR)/opt/grub-editor 3 | DESKTOP_PATH=$(PKG_DIR)/usr/share/applications 4 | LICENSE_PATH=$(PKG_DIR)/usr/share/licenses/grub-editor 5 | ICON_PATH=$(PKG_DIR)/usr/share/pixmaps 6 | install: 7 | find . -type f -exec install -Dm 755 "{}" "$(INSTALL_PATH)/{}" \; 8 | install $(DESKTOP_FILE) -D $(DESKTOP_PATH)/$(DESKTOP_FILE) 9 | # install README -D $(DOCPATH)/README 10 | # install $(DOC)/CHANGES -D $(DOCPATH)/CHANGES 11 | install LICENSE -D $(LICENSE_PATH)/LICENSE 12 | install -D grub-editor.png $(ICON_PATH)/grub-editor.png 13 | 14 | uninstall: 15 | rm -f $(DESKTOP_PATH)/$(DESKTOP_FILE) 16 | rm -rf $(INSTALL_PATH) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # grub-editor 4 | 5 | GUI application to manage grub configuration 6 | 7 | 8 | It workes by editing the /etc/default/grub 9 | 10 | 11 | [Website](https://thenujan-0.github.io/grub-editor-web) 12 | 13 | [![License: MIT](https://img.shields.io/github/license/Thenujan-0/grub-editor)](https://opensource.org/licenses/MIT) 14 | 15 | 16 | ![Screenshots](screenshots/light-screenshot0.png) 17 | 18 | ![Screenshots](screenshots/light-screenshot1.png) 19 | 20 | # snapshots storage 21 | 22 | Snapshots of the configs are stored in ~/.local/share/grub-editor/snapshots/ -------------------------------------------------------------------------------- /__pycache__/find_entries.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/__pycache__/find_entries.cpython-310.pyc -------------------------------------------------------------------------------- /docs/preferences.json.md: -------------------------------------------------------------------------------- 1 | #This file contains keys and all possible values of main.json file that stores the user preferences 2 | 3 | default way to view snapshots 4 | view_default:"on_the_application_itself","default_text_editor","None" 5 | take note that the None above is actually a string 6 | 7 | default way to create snapshots when loaded /etc/grub/default was modified in UI 8 | create_snapshot:"add_changes_to_snapshot","None","ignore_changes" 9 | 10 | show_invalid_default_entry:"True","False","None" 11 | invalid_kernel_version:"fix","cancel",:None -------------------------------------------------------------------------------- /grub-editor.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Categories=Utility;System; 3 | Comment[en_US]=GUI application to edit grub configurations 4 | Comment=GUI application to edit grub configurations 5 | Exec=/opt/grub-editor/grub-editor.py 6 | GenericName[en_US]= 7 | GenericName= 8 | Icon=grub-editor 9 | MimeType= 10 | Name[en_US]=Grub Editor 11 | Name=Grub Editor 12 | Path= 13 | StartupNotify=true 14 | StartupWMClass=Grub Editor 15 | Terminal=false 16 | TerminalOptions= 17 | Type=Application 18 | X-DBUS-ServiceName= 19 | X-DBUS-StartupType= 20 | X-KDE-SubstituteUID=false 21 | X-KDE-Username= 22 | -------------------------------------------------------------------------------- /grub-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/grub-editor.png -------------------------------------------------------------------------------- /grub-editor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import subprocess 3 | import sys 4 | import os 5 | import traceback 6 | import logging 7 | from math import floor 8 | 9 | from grubEditor.main import main 10 | 11 | 12 | PATH=os.path.dirname(os.path.realpath(__file__)) 13 | 14 | HOME =os.getenv('HOME') 15 | if os.getenv("XDG_DATA_HOME") is None: 16 | DATA_LOC=HOME+"/.local/share/grub-editor" 17 | else: 18 | DATA_LOC=os.getenv("XDG_DATA_HOME")+"/grub-editor" 19 | 20 | LOG_PATH=f'{DATA_LOC}/logs/main.log' 21 | 22 | logging.root.handlers = [] 23 | 24 | logging.basicConfig( 25 | level=logging.ERROR, 26 | format="%(asctime)s [%(levelname)s] %(message)s", 27 | handlers=[ 28 | logging.FileHandler(LOG_PATH), 29 | logging.StreamHandler() 30 | ] 31 | ) 32 | 33 | 34 | size= os.path.getsize(LOG_PATH) 35 | 36 | if size>5*10**6: 37 | with open(LOG_PATH) as f: 38 | data = f.read() 39 | ind=floor(len(data)/2) 40 | new_data =data[ind:] 41 | with open(LOG_PATH,"w") as f: 42 | f.write(new_data) 43 | 44 | def except_hook(_,exception,__): 45 | # sys.__excepthook__(cls, exception, traceback) 46 | # logging.error(traceback.format_exc()) 47 | error_text = "".join(traceback.format_exception(exception)) 48 | logging.error("Unhandled exception: %s", error_text) 49 | 50 | #escape single quotes 51 | error_text = error_text.replace("'","''") 52 | 53 | print(error_text) #Incase error_dialog fails 54 | cmd=f"python3 {PATH}/grubEditor/widgets/error_dialog.py 'An Exception occured' '{error_text}'" 55 | subprocess.Popen([cmd],shell=True) 56 | 57 | 58 | 59 | 60 | sys.excepthook = except_hook 61 | 62 | 63 | 64 | if __name__ == '__main__': 65 | print('starting main') 66 | try: 67 | print(PATH) 68 | main() 69 | except Exception as e: 70 | print(traceback.format_exc()) #Incase error_dialog fails 71 | error_text = traceback.format_exc() 72 | error_text=error_text.replace("'","''") 73 | cmd=f"python3 {PATH}/grubEditor/widgets/error_dialog.py 'An Exception occured' '{error_text}'" 74 | subprocess.Popen(cmd,shell=True) 75 | exit(1) -------------------------------------------------------------------------------- /grubEditor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/grubEditor/__init__.py -------------------------------------------------------------------------------- /grubEditor/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /grubEditor/core.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import os 3 | import sys 4 | import math 5 | from datetime import datetime as dt 6 | from grubEditor.locations import CACHE_LOC, DATA_LOC 7 | 8 | 9 | 10 | class GRUB_CONF(str, Enum): 11 | GRUB_TIMEOUT = "GRUB_TIMEOUT=" 12 | GRUB_DISABLE_OS_PROBER = "GRUB_DISABLE_OS_PROBER=" 13 | GRUB_DEFAULT = "GRUB_DEFAULT=" 14 | GRUB_TIMEOUT_STYLE = "GRUB_TIMEOUT_STYLE=" 15 | GRUB_RECORDFAIL_TIMEOUT = "GRUB_RECORDFAIL_TIMEOUT=" 16 | GRUB_CMDLINE_LINUX = "GRUB_CMDLINE_LINUX=" 17 | 18 | def remove_quotes(value:str)->str: 19 | """ Removes double quotes or single quotes from the begining and the end 20 | Only if the exist in both places 21 | """ 22 | if value[0]=='"' and value[-1]=='"': 23 | value=value[1:-1] 24 | elif value[0]=="'" and value[-1]=="'": 25 | value=value[1:-1] 26 | 27 | return value 28 | 29 | def printer(*args): 30 | """ writes to log and writes to console """ 31 | time_now = dt.now() 32 | printer_temp='' 33 | for arg in args: 34 | printer_temp= printer_temp +' '+str(arg) 35 | 36 | if sys.platform == 'linux': 37 | if os.stat(f'{DATA_LOC}/logs/main.log').st_size > 5000000:#number is in bytes 38 | 39 | #only keep last half of the file 40 | with open(f'{DATA_LOC}/logs/main.log','r') as f: 41 | data =f.read() 42 | lendata = len(data)/2 43 | lendata=math.floor(lendata) 44 | new_data = data[lendata:]+'\n' 45 | 46 | with open(f'{DATA_LOC}/logs/main.log','w') as f: 47 | f.write(str(time_now)+new_data+'\n') 48 | 49 | 50 | with open(f'{DATA_LOC}/logs/main.log','a') as f: 51 | f.write(str(time_now)+printer_temp+'\n') 52 | print(printer_temp) 53 | 54 | class CONF_HANDLER(): 55 | current_file :str = "/etc/default/grub" 56 | 57 | 58 | 59 | def get(self, name:GRUB_CONF,issues,read_file=None, remove_quotes_=False): 60 | """arguments are the string to look for 61 | and the list to append issues to 62 | Note: It does some minor edits to the value read from the file before 63 | returning it if name==GRUB_DEFAULT 64 | 1.if the value is not saved then check for double quotes, if it has 65 | double quotes then it will be removed if not then (Missing ") will be added 66 | 2.replace " >" with ">" to make sure it is found as invalid 67 | 68 | """ 69 | 70 | if read_file is None: 71 | read_file=self.current_file 72 | 73 | #check if last character is = to avoid possible bugs 74 | if name[-1] != '=': 75 | raise ValueError("name passed for get_value doesnt contain = as last character") 76 | 77 | with open(read_file) as file: 78 | data =file.read() 79 | lines=data.splitlines() 80 | 81 | # found the name that is being looked for in a commented line 82 | found_commented=False 83 | 84 | val= None 85 | for line in lines: 86 | sline=line.strip() 87 | if sline.find(f"#{name}")==0: 88 | found_commented=True 89 | 90 | if sline.find("#")==0: 91 | continue 92 | elif sline.find(name)==0: 93 | start_index= line.find(name)+len(name) 94 | 95 | val=sline[start_index:] 96 | 97 | #remove the double quotes in the end and the begining 98 | if name==GRUB_CONF.GRUB_DEFAULT and val is not None: 99 | if val.find(">")>0 and val.find(" >")==-1: 100 | val =val.replace(">"," >") 101 | elif val.find(" >")>0: 102 | #GRUB default is obviously invalid to make sure that other functions detect that its invalid lets just 103 | val.replace(" >",">") 104 | 105 | if val !="saved" : 106 | if (val[0]=="\"" and val[-1]=='"') or (val[0]=="'" and val[0] == "'"): 107 | val=val[1:-1] 108 | elif not val.replace(" >","").isdigit(): 109 | val+=" (Missing \")" 110 | 111 | if val is None: 112 | comment_issue_string =f"{name} is commented out in {read_file}" 113 | 114 | if found_commented and comment_issue_string not in issues: 115 | issues.append(comment_issue_string) 116 | else: 117 | issues.append(f"{name} was not found in {read_file}") 118 | elif remove_quotes_: 119 | val = remove_quotes(val) 120 | if name=="GRUB_DISABLE_OS_PROBER=" and val is None: 121 | val="true" 122 | return val 123 | 124 | def remove(self, name:GRUB_CONF,target_file=f"{CACHE_LOC}/temp.txt"): 125 | """ Removes the value from the value """ 126 | if name[-1] != '=': 127 | raise ValueError("name passed for set_value doesn't contain = as last character") 128 | 129 | with open(target_file) as f: 130 | data=f.read() 131 | 132 | lines=data.splitlines() 133 | 134 | for ind ,line in enumerate(lines): 135 | sline=line.strip() 136 | 137 | #no need to read the line if it starts with # 138 | if sline.find("#")==0: 139 | continue 140 | 141 | elif sline.find(name)==0: 142 | lines[ind]="" 143 | 144 | to_write_data="" 145 | 146 | for line in lines: 147 | to_write_data+=line+"\n" 148 | 149 | with open(target_file,'w') as file: 150 | file.write(to_write_data) 151 | 152 | def set(self, name,val,target_file=f'{CACHE_LOC}/temp.txt'): 153 | """ writes the changes to target_file(default:~/.cache/grub-editor/temp.txt). call initialize_temp_file before start writing to temp.txt 154 | call self.saveConfs or cp the file from cache to original to finalize the changes 155 | 156 | Note: It does some minor edits to the value passed if name==GRUB_DEFAULT 157 | 1.add double quotes if necessary 158 | 2.replace " >" with ">" 159 | """ 160 | 161 | if name[-1] != '=': 162 | raise ValueError("name passed for set_value doesn't contain = as last character") 163 | 164 | # if name not in available_conf_keys: 165 | # raise ValueError("name not in available_conf_keys :"+name) 166 | 167 | if name ==GRUB_CONF.GRUB_DEFAULT and val!="saved": 168 | if val[0]!='"' and val[-1]!='"': 169 | val='"'+val+'"' 170 | if " >" in val: 171 | val= val.replace(" >",">") 172 | 173 | 174 | with open(target_file) as f: 175 | data=f.read() 176 | 177 | lines=data.splitlines() 178 | old_val=None 179 | for ind ,line in enumerate(lines): 180 | sline=line.strip() 181 | 182 | #no need to read the line if it starts with # 183 | if sline.find("#")==0: 184 | continue 185 | 186 | elif sline.find(name)==0: 187 | start_index= line.find(name)+len(name) 188 | old_val=line[start_index:] 189 | if old_val!="": 190 | new_line =line.replace(old_val,val) 191 | lines[ind]=new_line 192 | else: 193 | print("empty string") 194 | lines[ind]=name+val 195 | 196 | to_write_data="" 197 | 198 | for line in lines: 199 | to_write_data+=line+"\n" 200 | 201 | #if line wasn't found 202 | if old_val is None: 203 | to_write_data+=name+val 204 | 205 | with open(target_file,'w') as file: 206 | file.write(to_write_data) 207 | 208 | conf_handler = CONF_HANDLER() -------------------------------------------------------------------------------- /grubEditor/libs/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys; 2 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) -------------------------------------------------------------------------------- /grubEditor/libs/find_entries.py: -------------------------------------------------------------------------------- 1 | """This module is used to find the entries in the grub config file""" 2 | 3 | import traceback 4 | import subprocess 5 | import os 6 | GRUB_CONF_NONEDITABLE="/boot/grub/grub.cfg" 7 | 8 | class GrubConfigNotFound(Exception): 9 | """ Raised when /boot/grub/grub.cfg is not found """ 10 | def __init__(self): 11 | super(Exception,self).__init__(f"Grub config file was not found at {GRUB_CONF_NONEDITABLE}") 12 | 13 | 14 | class MainEntry(): 15 | parent=None 16 | title:str 17 | sub_entries=[] 18 | def __init__(self,title,sub_entries_): 19 | self.title = title 20 | self.sub_entries=sub_entries_ 21 | 22 | def __repr__(self) -> str: 23 | to_return="" 24 | if len(self.sub_entries)==0: 25 | return "\nMainEntry(title:'"+self.title+"')" 26 | 27 | for sub_entry in self.sub_entries: 28 | to_return+="\n"+sub_entry.title 29 | 30 | 31 | to_return="\nMainEntry(title:'"+self.title+"', sub_entries :["+to_return+"])" 32 | 33 | return to_return 34 | 35 | def set_parents_for_children(self): 36 | for child in self.sub_entries: 37 | child.parent=self 38 | 39 | 40 | def echo(self): 41 | print('----------------------------------------------------------------') 42 | print('') 43 | print(self.title) 44 | 45 | print('printing sub_entries _____________') 46 | for sub_entry in self.sub_entries: 47 | print(sub_entry.title,'title of sub entry') 48 | print(sub_entry.parent,'parent of sub entry') 49 | print('finished printing for one big main entry') 50 | print('----------------') 51 | print('----------------') 52 | print('') 53 | 54 | def find_entries(): 55 | 56 | cmd_find_entries=["awk -F\\' '$1==\"menuentry \" || $1==\"submenu \" "+ 57 | "{print i++ \" : \" $2}; /\\tmenuentry / {print \"\\t\" i-1\">\"j++ \" : "+ 58 | "\" $2};' "+GRUB_CONF_NONEDITABLE] 59 | 60 | out =subprocess.getoutput(cmd_find_entries) 61 | NO_SUCH_FILE_ERR_MSG=f"awk: fatal: cannot open file `{GRUB_CONF_NONEDITABLE}' for reading: No such file or directory" 62 | PERMISSION_ERR_MSG=f"awk: fatal: cannot open file `{GRUB_CONF_NONEDITABLE}' for reading: Permission denied" 63 | 64 | if NO_SUCH_FILE_ERR_MSG in out: 65 | raise GrubConfigNotFound 66 | elif PERMISSION_ERR_MSG in out: 67 | raise PermissionError(f"Permission denied to read {GRUB_CONF_NONEDITABLE}") 68 | 69 | 70 | main_entries=[] 71 | lines =out.splitlines() 72 | for line in lines: 73 | 74 | if line[0].isdigit(): 75 | to_append=MainEntry(line[4:],[]) 76 | main_entries.append(to_append) 77 | 78 | else: 79 | try: 80 | to_append=MainEntry(line[7:],[]) 81 | main_entries[-1].sub_entries.append(to_append) 82 | except IndexError as e: 83 | print(traceback.format_exc()) 84 | print(e) 85 | print('error occured as an entry that was thought to be a sub entry couldnt be added to last main entry on the list .\ 86 | Error might have occured because the main_entries list is empty') 87 | print('--------------------------Printing the output of the command to find entries--------------------------------------') 88 | print(out) 89 | print("printing main entries",main_entries) 90 | print("printing to_append",to_append) 91 | print("line being parsed",line) 92 | 93 | 94 | 95 | for entry in main_entries: 96 | entry.set_parents_for_children() 97 | 98 | return main_entries 99 | 100 | 101 | if __name__=="__main__": 102 | print(find_entries()) 103 | -------------------------------------------------------------------------------- /grubEditor/libs/qt_functools.py: -------------------------------------------------------------------------------- 1 | 2 | ''' Some functions that are usefull in qt gui applications ''' 3 | from PyQt5 import QtWidgets 4 | 5 | def reconnect(signal ,new_handler=None,old_handler=None): 6 | try: 7 | if old_handler is not None: 8 | while True: 9 | signal.disconnect(old_handler) 10 | else: 11 | signal.disconnect() 12 | except TypeError: 13 | pass 14 | if new_handler is not None: 15 | # printer(signal) 16 | signal.connect(new_handler) 17 | 18 | def insert_into(layout,index,widget): 19 | items=[] 20 | for i in reversed(range(index,layout.count())): 21 | item = layout.takeAt(i) 22 | if isinstance(item,QtWidgets.QWidgetItem): 23 | widget_ =item.widget() 24 | widget_.setParent(None) 25 | items.append(item) 26 | elif isinstance(item,QtWidgets.QSpacerItem): 27 | if item.sizePolicy().horizontalPolicy()==7: 28 | horizontalSpacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Expanding,\ 29 | QtWidgets.QSizePolicy.Minimum) 30 | items.append(horizontalSpacer) 31 | else: 32 | raise Exception("Error in insert_into") 33 | else: 34 | raise Exception("Non handled case in insert_into") 35 | layout.addWidget(widget) 36 | for i in reversed(range(len(items))): 37 | item = items[i] 38 | if isinstance(item,QtWidgets.QWidgetItem): 39 | layout.addWidget(item.widget()) 40 | elif isinstance(item,QtWidgets.QSpacerItem): 41 | layout.addItem(item) 42 | else: 43 | raise Exception("Non handled case in insert_into") -------------------------------------------------------------------------------- /grubEditor/libs/worker.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore,QtWidgets 2 | 3 | import traceback 4 | import sys 5 | 6 | 7 | class WorkerSignals(QtCore.QObject): 8 | ''' 9 | Defines the signals available from a running worker thread. 10 | 11 | Supported signals are: 12 | 13 | finished 14 | No data 15 | 16 | error 17 | tuple (exctype, value, traceback.format_exc() ) 18 | 19 | result 20 | object data returned from processing, anything 21 | 22 | started 23 | used when there is a first step to pass in the process has to be set to emit manually in the runner function 24 | 25 | output 26 | emits everyline in stdout 27 | 28 | 29 | 30 | ''' 31 | finished = QtCore.pyqtSignal() 32 | error = QtCore.pyqtSignal(tuple) 33 | result = QtCore.pyqtSignal(object) 34 | output =QtCore.pyqtSignal(str) 35 | exception=QtCore.pyqtSignal(object) 36 | 37 | started=QtCore.pyqtSignal() 38 | 39 | 40 | 41 | 42 | class Worker(QtCore.QRunnable): 43 | ''' 44 | Worker thread 45 | 46 | Inherits from QRunnable to handler worker thread setup, signals and wrap-up. 47 | 48 | :param callback: The function callback to run on this worker thread. Supplied args and 49 | kwargs will be passed through to the runner. 50 | :type callback: function 51 | :param args: Arguments to pass to the callback function 52 | :param kwargs: Keywords to pass to the callback function 53 | 54 | ''' 55 | 56 | def __init__(self, fn, *args, **kwargs): 57 | super(Worker, self).__init__() 58 | # Store constructor arguments (re-used for processing) 59 | self.fn = fn 60 | self.args = args 61 | self.kwargs = kwargs 62 | self.signals = WorkerSignals() 63 | 64 | 65 | # this is a decorator 66 | # now when this run function is executed it actually calls the QtCore.pyqtSlot function wuth run function as argument 67 | @QtCore.pyqtSlot() 68 | def run(self): 69 | ''' 70 | Initialise the runner function with passed args, kwargs. 71 | ''' 72 | 73 | # Retrieve args/kwargs here; and fire processing using them 74 | try: 75 | result = self.fn( 76 | *self.args, **self.kwargs 77 | ) 78 | except Exception as e: 79 | traceback.print_exc() 80 | exctype, value = sys.exc_info()[:2] 81 | self.signals.error.emit((exctype, value, traceback.format_exc())) 82 | self.signals.exception.emit(e) 83 | else: 84 | self.signals.result.emit(result) # Return the result of the processing 85 | finally: 86 | self.signals.finished.emit() # Done -------------------------------------------------------------------------------- /grubEditor/locations.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | GRUB_CONF_LOC='/etc/default/grub' 4 | file_loc=GRUB_CONF_LOC 5 | HOME =os.getenv('HOME') 6 | 7 | if os.getenv("XDG_CONFIG_HOME") is None: 8 | CONFIG_LOC=HOME+"/.config/grub-editor" 9 | else: 10 | CONFIG_LOC=os.getenv("XDG_CONFIG_HOME")+"/grub-editor" 11 | 12 | if os.getenv("XDG_CACHE_HOME") is None: 13 | CACHE_LOC=HOME+"/.cache/grub-editor" 14 | else: 15 | CACHE_LOC=os.getenv("XDG_CACHE_HOME")+"/grub-editor" 16 | 17 | if os.getenv("XDG_DATA_HOME") is None: 18 | DATA_LOC=HOME+"/.local/share/grub-editor" 19 | else: 20 | DATA_LOC=os.getenv("XDG_DATA_HOME")+"/grub-editor" 21 | 22 | -------------------------------------------------------------------------------- /grubEditor/ui/chroot.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 939 10 | 554 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | 40 21 | 22 | 23 | 0 24 | 25 | 26 | 27 | 28 | 29 | 30 | Chroot is accessing another operating system from this operating system. Grub-editor uses manjaro-chroot to chroot 31 | 32 | 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Select an operating system to chroot into 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /grubEditor/ui/chroot_after.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 584 10 | 459 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | 30 21 | 22 | 23 | 50 24 | 25 | 26 | 27 | 28 | 29 | 30 | Chrooted successfully into kde neon 31 | 32 | 33 | Qt::AlignCenter 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Qt::Vertical 43 | 44 | 45 | 46 | 20 47 | 40 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 20 56 | 57 | 58 | 59 | 60 | reinstall grub package 61 | 62 | 63 | 64 | 65 | 66 | 67 | 64 68 | 64 69 | 70 | 71 | 72 | Qt::ToolButtonTextUnderIcon 73 | 74 | 75 | 76 | 77 | 78 | 79 | reinstall grub to device 80 | 81 | 82 | 83 | 84 | 85 | 86 | 64 87 | 64 88 | 89 | 90 | 91 | Qt::ToolButtonTextUnderIcon 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | Qt::Vertical 101 | 102 | 103 | 104 | 20 105 | 40 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 0 114 | 115 | 116 | 0 117 | 118 | 119 | 120 | 121 | Qt::Horizontal 122 | 123 | 124 | 125 | 40 126 | 20 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | update grub configuration 135 | 136 | 137 | 138 | 139 | 140 | 141 | 64 142 | 64 143 | 144 | 145 | 146 | Qt::ToolButtonTextUnderIcon 147 | 148 | 149 | 150 | 151 | 152 | 153 | Qt::Horizontal 154 | 155 | 156 | 157 | 40 158 | 20 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 180 168 | 0 169 | 170 | 171 | 172 | exit chroot 173 | 174 | 175 | 176 | 177 | 178 | 179 | 64 180 | 64 181 | 182 | 183 | 184 | Qt::ToolButtonTextUnderIcon 185 | 186 | 187 | 188 | 189 | 190 | 191 | Qt::Horizontal 192 | 193 | 194 | 195 | 40 196 | 20 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /grubEditor/ui/chroot_loading.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 903 10 | 623 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 23 | 24 | 25 | 26 | 27 | 28 | 12 29 | 75 30 | true 31 | 32 | 33 | 34 | Please wait .Looking for other operating systems using os-prober. This might take a While 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Output of os-prober 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | true 53 | 54 | 55 | 56 | 57 | 0 58 | 0 59 | 883 60 | 516 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /grubEditor/ui/create_snapshot_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 649 10 | 222 11 | 12 | 13 | 14 | Create snapshot 15 | 16 | 17 | 18 | 19 | 20 | 21 | 10 22 | 23 | 24 | 25 | 26 | you have made changes to the loaded configurations. Do you want to take snap shot of the current /etc/default/grub file which would mean that the changes you have done would be ignored . Or do you want to add these changes to the snapshot 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | Do this everytime 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Ignore the changes 46 | 47 | 48 | 49 | 50 | 51 | 52 | add changes to snapshot 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 0 67 | 649 68 | 32 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /grubEditor/ui/dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 604 10 | 297 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 20 21 | 22 | 23 | 24 | 25 | The partition you have selected is not empty and is too big for your system specs.It is recommended that you create another partition using gparted or kparted and then try again. 26 | 27 | 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | Never show this to me again 36 | 37 | 38 | 39 | 40 | 41 | 42 | 6 43 | 44 | 45 | 46 | 47 | Cancel 48 | 49 | 50 | 51 | 52 | 53 | 54 | Qt::Horizontal 55 | 56 | 57 | 58 | 40 59 | 20 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | OK 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /grubEditor/ui/issues.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 655 10 | 443 11 | 12 | 13 | 14 | Issues 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 15 25 | 26 | 27 | 28 | Some issues were found on the /etc/default/grub 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Grub-editor might not be able to function properly because of these issues.Please fix these issues manually before proceeding 39 | 40 | 41 | true 42 | 43 | 44 | 45 | 46 | 47 | 48 | 10 49 | 50 | 51 | 52 | 53 | Quit 54 | 55 | 56 | 57 | 58 | 59 | 60 | Open /etc/default/grub 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 0 74 | 0 75 | 655 76 | 32 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /grubEditor/ui/main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1007 10 | 556 11 | 12 | 13 | 14 | 15 | 700 16 | 0 17 | 18 | 19 | 20 | Grub Editor 21 | 22 | 23 | 24 | 25 | 0 26 | 27 | 28 | 29 | 30 | 31 | 1000 32 | 16777215 33 | 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | 1000 42 | 16777215 43 | 44 | 45 | 46 | Edit configurations 47 | 48 | 49 | 50 | 51 | 52 | 0 53 | 54 | 55 | 56 | 57 | 30 58 | 59 | 60 | 61 | 62 | Loaded configuration from 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | /etc/default/grub 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 0 81 | 82 | 83 | 0 84 | 85 | 86 | 87 | 88 | Look for other operating systems 89 | 90 | 91 | 92 | 93 | 94 | 95 | Visiblity 96 | 97 | 98 | 99 | 20 100 | 101 | 102 | 103 | 104 | Show menu 105 | 106 | 107 | 108 | 109 | 110 | 111 | 0 112 | 113 | 114 | 0 115 | 116 | 117 | 118 | 119 | Boot default entry after 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 40 131 | 0 132 | 133 | 134 | 135 | 136 | 40 137 | 40 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | .. 146 | 147 | 148 | 149 | 20 150 | 20 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 0 160 | 0 161 | 162 | 163 | 164 | 165 | 40 166 | 40 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | .. 175 | 176 | 177 | 178 | 179 | 180 | 181 | seconds 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | Force zero timeout 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 1000 202 | 16777215 203 | 204 | 205 | 206 | Default entry 207 | 208 | 209 | 210 | 211 | 212 | 0 213 | 214 | 215 | 10 216 | 217 | 218 | 219 | 220 | predefined: 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 1 229 | 0 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | previously booted entry 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 15 250 | 251 | 252 | 253 | 254 | Reset 255 | 256 | 257 | 258 | 259 | 260 | 261 | set 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | conf snapshots 276 | 277 | 278 | 279 | 280 | 281 | create a snapshot now 282 | 283 | 284 | 285 | 286 | 287 | 288 | Looks like you dont have any snapshots .Snapshots are backups of /etc/default/grub .Snapshots can help you when you mess up some configuration in /etc/default/grub . These snapshots are stored inside ~/.grub-editor/snapshots/ 289 | 290 | 291 | true 292 | 293 | 294 | 295 | 296 | 297 | 298 | Qt::ScrollBarAlwaysOff 299 | 300 | 301 | QAbstractScrollArea::AdjustToContents 302 | 303 | 304 | true 305 | 306 | 307 | 308 | 309 | 0 310 | 0 311 | 971 312 | 347 313 | 314 | 315 | 316 | 317 | 318 | 319 | 40 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 0 340 | 0 341 | 1007 342 | 32 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | -------------------------------------------------------------------------------- /grubEditor/ui/main1.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1007 10 | 595 11 | 12 | 13 | 14 | 15 | 700 16 | 0 17 | 18 | 19 | 20 | Grub Editor 21 | 22 | 23 | 24 | 25 | 0 26 | 27 | 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | Edit configurations 35 | 36 | 37 | 38 | 39 | 40 | 0 41 | 42 | 43 | 44 | 45 | 30 46 | 47 | 48 | 49 | 50 | Loaded configuration from 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | /etc/default/grub 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 0 69 | 70 | 71 | 0 72 | 73 | 74 | 75 | 76 | 15 77 | 78 | 79 | 80 | 81 | Reset 82 | 83 | 84 | 85 | 86 | 87 | 88 | set 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Default entry 98 | 99 | 100 | 101 | 102 | 103 | previously booted entry 104 | 105 | 106 | 107 | 108 | 109 | 110 | 0 111 | 112 | 113 | 10 114 | 115 | 116 | 117 | 118 | predefined: 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 1 127 | 0 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | Look for other operating systems 141 | 142 | 143 | 144 | 145 | 146 | 147 | Visiblity 148 | 149 | 150 | 151 | 152 | 153 | 6 154 | 155 | 156 | 0 157 | 158 | 159 | 160 | 161 | 0 162 | 163 | 164 | 0 165 | 166 | 167 | 168 | 169 | Boot default entry after 170 | 171 | 172 | 173 | 174 | 175 | 176 | true 177 | 178 | 179 | 180 | 200 181 | 0 182 | 183 | 184 | 185 | 186 | 187 | 188 | QFrame::StyledPanel 189 | 190 | 191 | QFrame::Raised 192 | 193 | 194 | 195 | 0 196 | 197 | 198 | 0 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 40 210 | 0 211 | 212 | 213 | 214 | 215 | 40 216 | 40 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | .. 225 | 226 | 227 | 228 | 20 229 | 20 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 0 239 | 0 240 | 241 | 242 | 243 | 244 | 40 245 | 40 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | .. 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | seconds 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 0 275 | 276 | 277 | 278 | 279 | Show menu 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | conf snapshots 299 | 300 | 301 | 302 | 303 | 304 | create a snapshot now 305 | 306 | 307 | 308 | 309 | 310 | 311 | Looks like you dont have any snapshots .Snapshots are backups of /etc/default/grub .Snapshots can help you when you mess up some configuration in /etc/default/grub . These snapshots are stored inside ~/.grub-editor/snapshots/ 312 | 313 | 314 | true 315 | 316 | 317 | 318 | 319 | 320 | 321 | Qt::ScrollBarAlwaysOff 322 | 323 | 324 | QAbstractScrollArea::AdjustToContents 325 | 326 | 327 | true 328 | 329 | 330 | 331 | 332 | 0 333 | 0 334 | 971 335 | 386 336 | 337 | 338 | 339 | 340 | 341 | 342 | 40 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 0 363 | 0 364 | 1007 365 | 32 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | -------------------------------------------------------------------------------- /grubEditor/ui/progress.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 584 10 | 217 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 22 | 23 | 24 | 25 | 26 | 27 | 28 | Chrooting please wait 29 | 30 | 31 | Qt::AlignCenter 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | true 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 545 48 | 110 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | this is a big 58 | paragraph of text 59 | or should i say 60 | very vey 61 | very big paragraph of text 62 | 63 | 64 | true 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 0 82 | 0 83 | 584 84 | 32 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /grubEditor/ui/set_recommendations.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 449 10 | 272 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 23 | 24 | 25 | 26 | Ignore all 27 | 28 | 29 | 30 | 31 | 32 | 33 | Fix all 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | true 45 | 46 | 47 | 48 | 49 | 0 50 | 0 51 | 427 52 | 123 53 | 54 | 55 | 56 | 57 | 58 | 59 | 0 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 12 74 | 75 | 76 | 77 | Recommendations regarding your configurations 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 0 87 | 0 88 | 449 89 | 30 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /grubEditor/ui/snapshots_.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 641 10 | 320 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Looks like you made some changes to the file that was loaded. 23 | Note that the changes you have done through the gui will not be added to the snap shot. 24 | Inorder to add those changes to the snapshot you can take a snapshot of your currently loaded configuration and then save the changes and then create a snapshot and then revert back 25 | 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Qt::Horizontal 37 | 38 | 39 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | buttonBox 49 | accepted() 50 | Dialog 51 | accept() 52 | 53 | 54 | 248 55 | 254 56 | 57 | 58 | 157 59 | 274 60 | 61 | 62 | 63 | 64 | buttonBox 65 | rejected() 66 | Dialog 67 | reject() 68 | 69 | 70 | 316 71 | 260 72 | 73 | 74 | 286 75 | 274 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /grubEditor/ui/treeWidgetTest.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Operating system 23 | 24 | 25 | 26 | 27 | manjaro 28 | 29 | 30 | 31 | 32 | advanced options for manjaro 33 | 34 | 35 | 36 | manjaro 5.14 37 | 38 | 39 | 40 | 41 | manjaro 5.15 42 | 43 | 44 | 45 | 46 | manjaro 5.15 fallback 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 0 58 | 0 59 | 800 60 | 30 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /grubEditor/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/grubEditor/widgets/__init__.py -------------------------------------------------------------------------------- /grubEditor/widgets/dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets ,uic 2 | from PyQt5.QtWidgets import QDesktopWidget,QApplication 3 | import os 4 | import sys 5 | 6 | 7 | #remove /widgets part from path 8 | PATH = os.path.dirname(os.path.realpath(__file__)) 9 | PATH = PATH[0:-8] 10 | 11 | sys.path.append(PATH) 12 | 13 | 14 | class DialogUi(QtWidgets.QDialog): 15 | """ Create a dialog 16 | Available functions 17 | 1.setText 18 | 2.setBtnOkText 19 | 3.exitOnAny (if you want to exit the app on closing the dialog or clicking ok or cancel) 20 | 4.exitOnClose 21 | 5.exitOnCancel 22 | 23 | """ 24 | _exitOnclose=False 25 | 26 | def __init__(self,btn_cancel=True): 27 | super(DialogUi,self).__init__() 28 | uic.loadUi(f'{PATH}/ui/dialog.ui',self) 29 | if not btn_cancel: 30 | self.horizontalLayout.takeAt(0).widget().deleteLater() 31 | else: 32 | self.btn_cancel.clicked.connect(self._btn_cancel_callback) 33 | self.btn_ok.clicked.connect(self._btn_ok_callback) 34 | 35 | 36 | #make sure window is in center of the screen 37 | qtRectangle = self.frameGeometry() 38 | centerPoint = QDesktopWidget().availableGeometry().center() 39 | qtRectangle.moveCenter(centerPoint) 40 | self.move(qtRectangle.topLeft()) 41 | 42 | def _btn_ok_callback(self): 43 | self.close() 44 | 45 | def _btn_cancel_callback(self): 46 | self.close() 47 | 48 | def show_dialog(self): 49 | """ Decides on showing the dialog using the preferences file and the value of label that is set """ 50 | 51 | self.show() 52 | 53 | def remove_btn_cancel(self): 54 | self.horizontalLayout.takeAt(0).widget().deleteLater() 55 | self.btn_cancel.setParent(None) 56 | 57 | def add_btn_cancel(self): 58 | self.btn_cancel=QtWidgets.QPushButton() 59 | self.btn_cancel.setText("Cancel") 60 | self.horizontalLayout.insertWidget(0,self.btn_cancel) 61 | self.btn_cancel.clicked.connect(self._btn_cancel_callback) 62 | 63 | def setText(self,text): 64 | self.label.setText(text) 65 | 66 | def setBtnOkText(self,text): 67 | self.btn_ok.setText(text) 68 | 69 | def removeCheckBox(self): 70 | self.checkBox.setParent(None) 71 | self.checkBox.deleteLater() 72 | 73 | def _exitApp(self): 74 | QApplication.quit() 75 | 76 | def exitOnAny(self): 77 | self.btn_ok.clicked.connect(self._exitApp) 78 | self.exitOnclose=True 79 | self.btn_cancel.clicked.connect(self._exitApp) 80 | 81 | def exitOnCancel(self): 82 | self.btn_cancel.clicked.connect(self._exitApp) 83 | 84 | def exitOnClose(self): 85 | self._exitOnClose=True 86 | 87 | def closeEvent(self,event): 88 | if self._exitOnclose: 89 | self._exitApp() 90 | else: 91 | event.accept() 92 | 93 | def main(): 94 | global PATH 95 | 96 | print(PATH) 97 | app = QtWidgets.QApplication([]) 98 | window = DialogUi() 99 | window.show() 100 | app.exec_() 101 | 102 | if __name__ == '__main__': 103 | main() -------------------------------------------------------------------------------- /grubEditor/widgets/elided_label.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt 2 | from PyQt5.QtGui import QPainter,QFontMetrics 3 | from PyQt5.QtWidgets import QLabel 4 | 5 | class ElidedLabel(QLabel): 6 | def paintEvent(self, event): 7 | painter = QPainter(self) 8 | metrics = QFontMetrics(self.font()) 9 | elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) 10 | painter.drawText(self.rect(), self.alignment(), elided) -------------------------------------------------------------------------------- /grubEditor/widgets/error_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets ,uic,QtGui 2 | from PyQt5.QtWidgets import QDesktopWidget,QApplication 3 | import os 4 | import sys 5 | 6 | PATH = os.path.dirname(os.path.realpath(__file__)) 7 | 8 | 9 | 10 | class ErrorDialogUi(QtWidgets.QDialog): 11 | """ Avaiable functions are 12 | set_error_title 13 | set_error_body 14 | 15 | """ 16 | 17 | exitOnclose=False 18 | 19 | def __init__(self,): 20 | super(ErrorDialogUi,self).__init__() 21 | print(PATH) 22 | uic.loadUi(f'{PATH}/ui/error_dialog.ui',self) 23 | 24 | self.btn_ok.clicked.connect(self.selfClose) 25 | self.btn_copy.clicked.connect(self.onCopy) 26 | 27 | 28 | #make sure window is in center of the screen 29 | qtRectangle = self.frameGeometry() 30 | centerPoint = QDesktopWidget().availableGeometry().center() 31 | qtRectangle.moveCenter(centerPoint) 32 | self.move(qtRectangle.topLeft()) 33 | 34 | 35 | def selfClose(self): 36 | self.close() 37 | 38 | def set_error_title(self,error_title): 39 | ''' Sets the title for error message dont use dot in the end as dot will be placed automatically 40 | ''' 41 | 42 | #

An error has occured.Please consider reporting this to github page

43 | #is the main error message 44 | 45 | #only part thats going to be replaced is An error has occured 46 | 47 | text = self.lbl_error_title.text() 48 | self.lbl_error_title.setText(text.replace('An error has occured',error_title)) 49 | 50 | def set_error_body(self,error_body): 51 | self.lbl_error_body.setText(error_body) 52 | 53 | def _exitApp(self): 54 | QApplication.exit() 55 | def exitOnAny(self): 56 | self.btn_ok.clicked.connect(self._exitApp) 57 | self.exitOnclose=True 58 | 59 | 60 | def closeEvent(self,event): 61 | if self.exitOnclose: 62 | self._exitApp() 63 | else: 64 | event.accept() 65 | 66 | def onCopy(self): 67 | QApplication.clipboard().setText(self.lbl_error_body.text()) 68 | 69 | def main(): 70 | app= QtWidgets.QApplication([]) 71 | window=ErrorDialogUi() 72 | 73 | try: 74 | window.set_error_title(sys.argv[1]) 75 | window.set_error_body(sys.argv[2]) 76 | 77 | except IndexError: 78 | pass 79 | window.show() 80 | app.setWindowIcon(QtGui.QIcon('/usr/share/pixmaps/grub-editor.png')) 81 | 82 | sys.exit(app.exec_()) 83 | 84 | 85 | if __name__ =="__main__": 86 | main() -------------------------------------------------------------------------------- /grubEditor/widgets/loading_bar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | 4 | import sys 5 | from PyQt5.QtWidgets import QWidget, QApplication 6 | from PyQt5.QtGui import QPainter, QColor, QFont,QPalette,QPolygonF,QBrush 7 | from PyQt5.QtCore import Qt , QPointF,QRectF,QObject,pyqtSignal,QRunnable,pyqtSlot,QThreadPool,QTimer 8 | import traceback 9 | from time import sleep 10 | from functools import partial 11 | 12 | 13 | class WorkerSignals(QObject): 14 | """ defines the signals available from a running worker thread 15 | 16 | supported signals are: 17 | finished 18 | No data 19 | 20 | error 21 | tuple( exctype ,value ,traceback.format_exc() ) 22 | 23 | result 24 | object data returned from processing , anything 25 | 26 | 27 | """ 28 | 29 | finished = pyqtSignal() 30 | error= pyqtSignal(tuple) 31 | result = pyqtSignal(object) 32 | 33 | class Worker(QRunnable): 34 | """ 35 | Worker thread 36 | inherits from QRunnable to handler worker thread setup , signals, wrap-up. 37 | :param callback: The function to run on this worker thread . Supllied args and 38 | kwargs will be passed through the runner 39 | :type callback: function 40 | :param args : Arguments to pass the callback function 41 | :param kwargs : keyword to pass to the callback function 42 | 43 | """ 44 | 45 | def __init__(self,fn,*args,**kwargs): 46 | super(Worker, self).__init__() 47 | self.fn =fn 48 | self.args= args 49 | self.kwargs=kwargs 50 | self.signals=WorkerSignals() 51 | 52 | 53 | @pyqtSlot() 54 | def run(self): 55 | """ 56 | initialise the runner function with passed args and kwargs 57 | """ 58 | 59 | try: 60 | result =self.fn(*self.args,**self.kwargs) 61 | 62 | except: 63 | traceback.print_exc() 64 | exctype,value = sys.exc_info()[:2] 65 | self.signals.error.emit((exctype, value, traceback.format_exc() )) 66 | else: 67 | self.signals.result.emit(result) 68 | finally: 69 | self.signals.finished.emit() 70 | 71 | 72 | 73 | class LoadingBar(QWidget): 74 | 75 | def __init__(self): 76 | super().__init__() 77 | self.threadpool=QThreadPool() 78 | 79 | self.initUI() 80 | 81 | #position of colored part in loading bar 0 -100 82 | self.position=20 83 | self.startWorker(self.move_loading_bar) 84 | self.loading_increasing=True 85 | self.interruptRequested = False 86 | 87 | 88 | def onDestroyed(self): 89 | self.interruptRequested = True 90 | 91 | #For some reason it doesn't work if i directly make onDestroyed a method of this class 92 | self.destroyed.connect(partial(onDestroyed,self)) 93 | 94 | def move_loading_bar(self): 95 | """ move the loading bar back and forth by changing the value of self.position """ 96 | while not self.interruptRequested: 97 | # print('moving loading bar',self.position) 98 | sleep(0.015) 99 | if self.position ==100: 100 | self.loading_increasing=False 101 | elif self.position==0: 102 | self.loading_increasing=True 103 | 104 | if self.loading_increasing: 105 | self.position+=1 106 | else: 107 | self.position-=1 108 | 109 | 110 | #Error might occur if the LoadingBar widget is deleted so to catch that 111 | try: 112 | self.update() 113 | except RuntimeError: 114 | pass 115 | 116 | 117 | def startWorker(self,fn,*args,**kwargs): 118 | worker= Worker(fn) 119 | 120 | self.threadpool.start(worker) 121 | 122 | 123 | def initUI(self): 124 | 125 | 126 | self.setGeometry(300, 300, 350, 300) 127 | self.setWindowTitle('loading please wait') 128 | self.show() 129 | 130 | def paintEvent(self, event): 131 | qp = QPainter() 132 | qp.begin(self) 133 | self.drawText(event, qp) 134 | qp.end() 135 | 136 | def drawText(self, event, qp): 137 | 138 | width = self.width() 139 | height = self.height() 140 | 141 | self.widget_height= 6 142 | 143 | 144 | #the part of the loading bar that is not going to have the progressed part 145 | reduce_amount = width*0.6 146 | 147 | top_left =QPointF(int(width*0.1),int(height/2-self.widget_height/2)) 148 | bottom_right =QPointF(int(width*0.9)-reduce_amount ,int(height/2 +self.widget_height/2)) 149 | 150 | bigger_bottom_right =QPointF(int(width*0.9) ,int(height/2+self.widget_height/2) ) 151 | 152 | recty =QRectF(QPointF(top_left.x()+self.position/100*reduce_amount,top_left.y()), 153 | QPointF(bottom_right.x()+self.position/100*reduce_amount,bottom_right.y())) 154 | bigger_recty=QRectF(top_left,bigger_bottom_right) 155 | 156 | 157 | #non progressed part (bigger rounded rect) 158 | qp.setPen(QPalette().color(QPalette.Disabled,QPalette.Text)) 159 | qp.setBrush(QBrush(QPalette().color(QPalette.Active,QPalette.Button))) 160 | qp.drawRoundedRect(bigger_recty,3,3) 161 | 162 | 163 | #progressed part 164 | qp.setBrush(QBrush(QPalette().color(QPalette().Inactive,QPalette().Highlight))) 165 | qp.setPen(QPalette().color(QPalette().Active,QPalette().Highlight)) 166 | qp.drawRoundedRect(recty,2,2) 167 | 168 | 169 | def main(): 170 | 171 | app = QApplication(sys.argv) 172 | ex = LoadingBar() 173 | sys.exit(app.exec_()) 174 | 175 | 176 | if __name__ == '__main__': 177 | main() -------------------------------------------------------------------------------- /grubEditor/widgets/progress.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | from PyQt5 import QtWidgets,QtCore 5 | 6 | 7 | PATH= os.path.dirname(os.path.realpath(__file__)) 8 | 9 | 10 | if 'widgets' == PATH[-7:]: 11 | print(PATH[0:-8]) 12 | sys.path.append(PATH[0:-8]) 13 | 14 | import widgets.loading_bar as loading_bar 15 | from grubEditor.libs.qt_functools import insert_into 16 | 17 | 18 | class ProgressUi(QtWidgets.QMainWindow): 19 | def __init__(self): 20 | super().__init__() 21 | 22 | 23 | self.centralwidget=QtWidgets.QWidget(self) 24 | self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) 25 | self.verticalLayout=QtWidgets.QVBoxLayout() 26 | self.verticalLayout.setContentsMargins(-1, -1, -1, 30) 27 | self.lbl_status=QtWidgets.QLabel(self.centralwidget) 28 | self.lbl_status.setAlignment(QtCore.Qt.AlignCenter) 29 | self.lbl_status.setText("please wait") 30 | self.verticalLayout.addWidget(self.lbl_status) 31 | self.gridLayout.addLayout(self.verticalLayout,0,0,1,1) 32 | self.setCentralWidget(self.centralwidget) 33 | self.loading_bar=loading_bar.LoadingBar() 34 | self.loading_bar.setMinimumHeight(20) 35 | self.verticalLayout.addWidget(self.loading_bar) 36 | 37 | self.btn_show_details=QtWidgets.QPushButton(self.centralwidget) 38 | self.btn_show_details.setText("Show details") 39 | self.verticalLayout.addWidget(self.btn_show_details) 40 | 41 | 42 | self.resize(400,200) 43 | 44 | 45 | self.lbl_details_text='' 46 | self.lbl_details=None 47 | 48 | self.btn_show_details.clicked.connect(self.btn_show_details_callback) 49 | 50 | 51 | 52 | 53 | def btn_show_details_callback(self): 54 | btn=self.sender() 55 | text =btn.text() 56 | if self.verticalLayout.itemAt(3) is not None: 57 | print(self.verticalLayout.itemAt(3).widget()) 58 | else: 59 | print(None) 60 | if text =='Show details': 61 | self.scrollArea=QtWidgets.QScrollArea(self.centralwidget) 62 | self.scrollArea.setWidgetResizable(True) 63 | self.scrollAreaWidgetContents=QtWidgets.QWidget() 64 | self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 545, 110)) 65 | self.gridLayout_2 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents) 66 | self.verticalLayout_2 = QtWidgets.QVBoxLayout() 67 | self.lbl_details = QtWidgets.QLabel(self.scrollAreaWidgetContents) 68 | self.lbl_details.setWordWrap(True) 69 | 70 | self.lbl_details.setText(self.lbl_details_text) 71 | 72 | #find the index of the button 73 | for index in range(self.verticalLayout.count()): 74 | item = self.verticalLayout.itemAt(index).widget() 75 | if 'QPushButton' in item.__str__() and item.text()=='Show details': 76 | 77 | self.verticalLayout_2.addWidget(self.lbl_details) 78 | self.gridLayout_2.addLayout(self.verticalLayout_2, 0, 0, 1, 1) 79 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 80 | 81 | # if it is the last widget of the veritical layout 82 | if self.verticalLayout.count()-1 == index: 83 | 84 | self.verticalLayout.addWidget(self.scrollArea) 85 | 86 | #if it isnt the last widget of the verticalLayout 87 | else: 88 | insert_into(self.verticalLayout,index+1,self.scrollArea) 89 | break 90 | 91 | 92 | # lbl =QtWidgets.QLabel(self.centralwidget) 93 | 94 | # self.verticalLayout.addWidget(lbl) 95 | 96 | btn.setText("Hide details") 97 | 98 | elif text=='Hide details': 99 | self.lbl_details=None 100 | btn.setText("Show details") 101 | for index in range(self.verticalLayout.count()): 102 | item = self.verticalLayout.itemAt(index).widget() 103 | if 'QScrollArea' in item.__str__(): 104 | item.deleteLater() 105 | item.setParent(None) 106 | #delete the child widget of the QScrollArea 107 | item.widget().deleteLater() 108 | break 109 | else: 110 | print("unknown text in btn_show_details") 111 | 112 | def update_lbl_details(self,text:str): 113 | ''' updates the lbl_details adds the the string to the pre existing string in lbl_details 114 | This doesnt add new lines 115 | ''' 116 | 117 | pre_text=self.lbl_details_text 118 | self.lbl_details_text=pre_text+text 119 | 120 | if self.lbl_details is not None: 121 | self.lbl_details.setText(self.lbl_details_text) 122 | 123 | 124 | if __name__=="__main__": 125 | global APP 126 | APP = QtWidgets.QApplication([]) 127 | 128 | window = ProgressUi() 129 | window.show() 130 | APP.exec_() 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /grubEditor/widgets/ui/error_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 646 10 | 397 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <p>An error has occured.Please consider reporting this to <span><a href="https://github.com/Thenujan-0/grub-editor/issues">github page</a></span></p> 23 | 24 | 25 | true 26 | 27 | 28 | true 29 | 30 | 31 | Qt::LinksAccessibleByMouse 32 | 33 | 34 | 35 | 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 0 44 | 0 45 | 624 46 | 302 47 | 48 | 49 | 50 | 51 | 52 | 53 | Another error occured while loading the error message :) 54 | 55 | 56 | Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Qt::Horizontal 70 | 71 | 72 | 73 | 40 74 | 20 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | Copy error message 83 | 84 | 85 | 86 | 87 | 88 | 89 | OK 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /grubEditor/widgets/ui/view_snapshot.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 377 10 | 169 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | 10 21 | 22 | 23 | 24 | 25 | How do you want to view the file? 26 | 27 | 28 | 29 | 30 | 31 | 32 | Do this everytime 33 | 34 | 35 | 36 | 37 | 38 | 39 | On the appliction itself 40 | 41 | 42 | 43 | 44 | 45 | 46 | Default text editor 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /grubEditor/widgets/view_mode_popup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | from PyQt5 import uic 5 | from PyQt5.QtWidgets import QDesktopWidget, QDialog 6 | 7 | 8 | PATH = os.path.dirname(os.path.realpath(__file__)) 9 | 10 | class ViewModePopup(QDialog): 11 | def __init__(self,file_location,conf_handler,parent): 12 | self.file_location = file_location 13 | self.conf_handler = conf_handler 14 | self.parent = parent 15 | super(ViewModePopup, self).__init__(parent) 16 | uic.loadUi(f'{PATH}/ui/view_snapshot.ui',self) 17 | self.btn_on_the_application_itself.clicked.connect(self.btn_on_the_application_itself_callback) 18 | self.btn_default_text_editor.clicked.connect(self.btn_default_text_editor_callback) 19 | 20 | #create window in the center of the screen 21 | qtRectangle = self.frameGeometry() 22 | centerPoint = QDesktopWidget().availableGeometry().center() 23 | qtRectangle.moveCenter(centerPoint) 24 | self.move(qtRectangle.topLeft()) 25 | 26 | def safe_close(self,arg): 27 | """makes sure the properties of checkbox is saved""" 28 | if self.checkBox_do_this_everytime.isChecked(): 29 | set_preference('view_default',arg) 30 | self.close() 31 | 32 | def btn_default_text_editor_callback(self): 33 | self.safe_close('default_text_editor') 34 | subprocess.Popen([f'xdg-open \'{self.file_location}\''],shell=True) 35 | 36 | def btn_on_the_application_itself_callback(self): 37 | self.conf_handler.current_file= self.file_location 38 | self.parent.setUiElements(show_issues=False) 39 | self.parent.tabWidget.setCurrentIndex(0) 40 | self.safe_close('on_the_application_itself') -------------------------------------------------------------------------------- /screenshots/grub-editor0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/screenshots/grub-editor0.png -------------------------------------------------------------------------------- /screenshots/grub-editor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/screenshots/grub-editor1.png -------------------------------------------------------------------------------- /screenshots/grub-editor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/screenshots/grub-editor2.png -------------------------------------------------------------------------------- /screenshots/light-screenshot0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/screenshots/light-screenshot0.png -------------------------------------------------------------------------------- /screenshots/light-screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thenujan-0/grub-editor/2cfa9a263dcb5522f2cccb29a910de1b29858040/screenshots/light-screenshot1.png -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | qt_api=pyqt5 -------------------------------------------------------------------------------- /tests/test_edit_configurations.py: -------------------------------------------------------------------------------- 1 | from calendar import c 2 | import os; 3 | import sys; 4 | from time import sleep 5 | from PyQt5 import QtWidgets,QtCore 6 | import subprocess 7 | from tools import change_comboBox_current_index ,create_tmp_file,create_snapshot 8 | 9 | HOME =os.getenv('HOME') 10 | PATH=os.path.dirname(os.path.realpath(__file__)) 11 | 12 | #parent dir 13 | PATH=PATH[0:-5] + "/grubEditor" 14 | sys.path.append(PATH) 15 | 16 | import main 17 | 18 | 19 | def test_grub_timeout_add_substract(qtbot): 20 | 21 | MainWindow = main.Ui() 22 | mw=MainWindow 23 | qtbot.addWidget(mw) 24 | 25 | time_out_val =mw.ledit_grub_timeout.text() 26 | print(time_out_val,'default grub_timeout_val') 27 | 28 | # click in the Greet button and make sure it updates the appropriate label 29 | qtbot.mouseClick(mw.btn_add, QtCore.Qt.LeftButton) 30 | assert mw.ledit_grub_timeout.text() == str(float(time_out_val)+1) 31 | qtbot.mouseClick(mw.btn_substract, QtCore.Qt.LeftButton) 32 | assert mw.ledit_grub_timeout.text()==str(float(time_out_val)) 33 | 34 | #set the value of ledit_grub_timeout to 1 and then check if it goes below zero 35 | mw.ledit_grub_timeout.setText("1.0") 36 | qtbot.mouseClick(mw.btn_substract, QtCore.Qt.LeftButton) 37 | assert mw.ledit_grub_timeout.text() == "0.0" 38 | qtbot.mouseClick(mw.btn_substract, QtCore.Qt.LeftButton) 39 | assert mw.ledit_grub_timeout.text() == "0.0" 40 | 41 | def test_look_for_other_os(qtbot): 42 | MainWindow = main.Ui() 43 | mw=MainWindow 44 | qtbot.addWidget(mw) 45 | 46 | cb =mw.checkBox_look_for_other_os 47 | 48 | 49 | if not cb.isChecked(): 50 | main.initialize_temp_file() 51 | main.set_value("GRUB_DISABLE_OS_PROBER=","false") 52 | mw.saveConfs() 53 | 54 | 55 | #h 56 | with qtbot.waitSignal(mw.saveConfs_worker.signals.finished,timeout=30*1000): 57 | pass 58 | 59 | 60 | 61 | 62 | 63 | mw.setUiElements() 64 | 65 | 66 | assert cb.isChecked() ==True 67 | assert not mw.btn_set.isEnabled() 68 | 69 | # mw.checkBox_look_for_other_os.setChecked(False) 70 | cb.setChecked(False) 71 | cb.clicked.emit() 72 | 73 | assert cb.isChecked() ==False 74 | 75 | assert mw.btn_set.isEnabled() 76 | 77 | qtbot.mouseClick(mw.btn_set, QtCore.Qt.LeftButton) 78 | assert mw.lbl_status.text() =="Waiting for authentication" 79 | 80 | while mw.lbl_status.text() =="Waiting for authentication": 81 | sleep(1) 82 | 83 | assert mw.lbl_status.text() =="Saving configurations" 84 | while mw.lbl_status.text()=="Saving configurations": 85 | sleep(1) 86 | 87 | 88 | 89 | issues=[] 90 | assert main.get_value("GRUB_DISABLE_OS_PROBER=",issues)=="true" 91 | assert issues ==[] 92 | 93 | assert mw.lbl_status.text() =="Saved successfully" 94 | 95 | 96 | 97 | #repeat the same test but now the opposite case 98 | 99 | cb=mw.checkBox_look_for_other_os 100 | cb.setChecked(True) 101 | 102 | assert cb.isChecked() ==True 103 | 104 | main.set_value("GRUB_DISABLE_OS_PROBER=","true") 105 | 106 | qtbot.mouseClick(mw.btn_set, QtCore.Qt.LeftButton) 107 | assert mw.lbl_status.text() =="Waiting for authentication" 108 | while mw.lbl_status.text()=='Waiting for authentication': 109 | sleep(1) 110 | assert mw.lbl_status.text() =="Saving configurations" 111 | 112 | while mw.lbl_status.text()=="Saving configurations": 113 | sleep(1) 114 | 115 | 116 | 117 | issues=[] 118 | assert main.get_value("GRUB_DISABLE_OS_PROBER=",issues)=="false" 119 | assert issues ==[] 120 | 121 | 122 | assert mw.lbl_status.text() =="Saved successfully" 123 | 124 | def test_comboBox_configurations(qtbot): 125 | mw = main.Ui() 126 | main.MainWindow=mw 127 | qtbot.addWidget(mw) 128 | for i in range(len(mw.all_entries)): 129 | if i != mw.comboBox_grub_default.currentIndex(): 130 | temp_entry = mw.all_entries[i] 131 | break 132 | 133 | 134 | snapshot_test="""GRUB_DEFAULT=""" 135 | print(f' echo "{snapshot_test}" > {main.DATA_LOC}/snapshots/test_snapshot') 136 | subprocess.run([f' echo "{snapshot_test}" > {main.DATA_LOC}/snapshots/test_snapshot'],shell=True) 137 | main.set_value("GRUB_DEFAULT=",temp_entry,target_file=f"{main.DATA_LOC}/snapshots/test_snapshot") 138 | mw.setUiElements(only_snapshots=True) 139 | mw.comboBox_configurations.setCurrentIndex(mw.configurations.index("test_snapshot")) 140 | 141 | 142 | #check if the correct value for grub default was shown 143 | assert mw.all_entries[mw.comboBox_grub_default.currentIndex()] ==temp_entry 144 | 145 | 146 | 147 | def test_btn_set(qtbot): 148 | mw = main.Ui() 149 | main.MainWindow=mw 150 | qtbot.addWidget(mw) 151 | assert "(modified)" not in mw.configurations[mw.comboBox_configurations.currentIndex()] 152 | old_ind=mw.comboBox_grub_default.currentIndex() 153 | 154 | curr_ind =change_comboBox_current_index(mw) 155 | print(curr_ind,"currentIndex") 156 | new_ind=mw.comboBox_grub_default.currentIndex() 157 | 158 | assert new_ind!=old_ind 159 | 160 | assert "(modified)" in mw.configurations[mw.comboBox_configurations.currentIndex()] 161 | qtbot.mouseClick(mw.btn_set, QtCore.Qt.LeftButton) 162 | 163 | with qtbot.waitSignal(mw.saveConfs_worker.signals.finished,raising=True,timeout=30*1000): 164 | pass 165 | 166 | assert mw.original_modifiers ==[] 167 | print(mw.configurations[mw.comboBox_configurations.currentIndex()]) 168 | assert "(modified)" not in mw.configurations[mw.comboBox_configurations.currentIndex()] 169 | 170 | def test_checkBox_look_for_other_os(qtbot): 171 | ''' Test if this comboBox defaults to not checked if GRUB_DISABLE_OS_PROBER wasn't found or commented ''' 172 | mw=main.Ui() 173 | main.MainWindow=mw 174 | qtbot.addWidget(mw) 175 | 176 | test_config1="""#GRUB_DISABLE_OS_PROBER=false""" 177 | 178 | 179 | tmp_file=create_tmp_file(test_config1) 180 | issues=[] 181 | val =main.get_value("GRUB_DISABLE_OS_PROBER=",issues,tmp_file) 182 | assert val =="true" 183 | assert issues ==[f"GRUB_DISABLE_OS_PROBER= is commented out in {tmp_file}"] 184 | 185 | def test_comboBox_grub_default_numbers(qtbot): 186 | mw = main.Ui() 187 | main.MainWindow=mw 188 | qtbot.addWidget(mw) 189 | 190 | #close that dialog if it exists because it poped up because /etc/default/grub has an invalid entry 191 | if mw.dialog_invalid_default_entry: 192 | mw.dialog_invalid_default_entry.close() 193 | 194 | 195 | test_config1="""GRUB_DEFAULT=\"1\"""" 196 | 197 | sfile=create_snapshot(test_config1) 198 | mw.setUiElements(only_snapshots=True) 199 | mw.comboBox_configurations.setCurrentIndex(mw.configurations.index(sfile)) 200 | 201 | assert mw.all_entries[mw.comboBox_grub_default.currentIndex()]==mw.all_entries[1] 202 | 203 | 204 | #todo test 0 >2 205 | #1 >2 206 | test_config2="GRUB_DEFAULT=0>1" 207 | sfile=create_snapshot(test_config2) 208 | mw.setUiElements() 209 | 210 | mw.comboBox_configurations.setCurrentIndex(mw.configurations.index(sfile)) 211 | 212 | assert mw.dialog_invalid_default_entry.isVisible() 213 | assert mw.comboBox_grub_default.styleSheet()==mw.comboBox_grub_default_invalid_style 214 | 215 | mw.dialog_invalid_default_entry.close() 216 | assert not mw.dialog_invalid_default_entry.isVisible() 217 | 218 | test_config3="GRUB_DEFAULT=\"1>1\"" 219 | sfile=create_snapshot(test_config3) 220 | mw.setUiElements() 221 | mw.dialog_invalid_default_entry.close() 222 | 223 | 224 | mw.comboBox_configurations.setCurrentIndex(mw.configurations.index(sfile)) 225 | assert not mw.dialog_invalid_default_entry.isVisible() 226 | assert mw.comboBox_grub_default.currentIndex()==2 227 | 228 | 229 | 230 | test_config4="GRUB_DEFAULT=0" 231 | sfile=create_snapshot(test_config4) 232 | mw.setUiElements() 233 | mw.comboBox_configurations.setCurrentIndex(mw.configurations.index(sfile)) 234 | assert not mw.dialog_invalid_default_entry.isVisible() 235 | assert mw.comboBox_grub_default.currentIndex()==0 236 | 237 | 238 | def test_missing_double_quotes_default(qtbot): 239 | mw = main.Ui() 240 | main.MainWindow=mw 241 | qtbot.addWidget(mw) 242 | 243 | test_config="""GRUB_DEFAULT=Manjaro Linux 244 | GRUB_TIMEOUT=20 245 | GRUB_TIMEOUT_STYLE=menu 246 | GRUB_DISTRIBUTOR=\"Manjaro\"""" 247 | 248 | sfile=create_snapshot(test_config) 249 | mw.setUiElements() 250 | if mw.dialog_invalid_default_entry: 251 | mw.dialog_invalid_default_entry.close() 252 | 253 | mw.comboBox_configurations.setCurrentIndex(mw.configurations.index(sfile)) 254 | 255 | assert mw.comboBox_grub_default.styleSheet()==mw.comboBox_grub_default_invalid_style 256 | assert mw.dialog_invalid_default_entry.isVisible() 257 | -------------------------------------------------------------------------------- /tests/test_fix_kernel_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import re 4 | import subprocess 5 | from time import sleep 6 | from pathlib import Path 7 | from PyQt5 import QtWidgets,QtCore,QtTest 8 | from tools import change_comboBox_current_index,delete_pref 9 | 10 | 11 | PATH = os.path.dirname(os.path.realpath(__file__)) 12 | #get the parent directory 13 | PATH = Path(PATH).parent 14 | sys.path.append(PATH) 15 | 16 | import main 17 | 18 | HOME=os.getenv("HOME") 19 | 20 | def change_krnl_minor_vrsn(val)->str: 21 | #match the 14-1- like part of 5.16.14-1- 22 | pattern=r'\d+-\d+-' 23 | 24 | krnl_minor_vrsn=re.search(pattern,val).group(0) 25 | print(val) 26 | print(krnl_minor_vrsn) 27 | 28 | 29 | ind =krnl_minor_vrsn.find("-") 30 | krnl_minor_vrsn_strp=krnl_minor_vrsn[:ind] 31 | # new_ind = krnl_minor_vrsn[ind+1:].find("-") +ind 32 | # krnl_minor_vrsn = krnl_minor_vrsn[] 33 | 34 | print(krnl_minor_vrsn_strp) 35 | new_krnl_minor_vrsn_strp = str(int(krnl_minor_vrsn_strp)+1) 36 | new_krnl_minor_vrsn= krnl_minor_vrsn.replace(krnl_minor_vrsn_strp,new_krnl_minor_vrsn_strp) 37 | print(new_krnl_minor_vrsn) 38 | new_val = val.replace(krnl_minor_vrsn,new_krnl_minor_vrsn) 39 | print(new_val) 40 | return new_val 41 | 42 | def test_fix_kernel_version_conf(qtbot): 43 | mw=main.Ui() 44 | main.MainWindow=mw 45 | if len(mw.invalid_entries)==0: 46 | for val in mw.all_entries: 47 | if " >" not in val: 48 | continue 49 | new_val=change_krnl_minor_vrsn(val) 50 | 51 | 52 | break 53 | print("changing the current entry to invalid") 54 | mw.close() 55 | main.initialize_temp_file() 56 | main.set_value("GRUB_DEFAULT=",new_val) 57 | subprocess.run([f"pkexec cp {HOME}/.cache/grub-editor/temp.txt /etc/default/grub"] 58 | ,shell=True) 59 | mw = main.Ui() 60 | main.MainWindow=mw 61 | 62 | assert mw.dialog_invalid_default_entry.isVisible() 63 | 64 | #First press the cancel button check if window closes and 65 | # no changes were made to the snapshot/file 66 | print(main.file_loc) 67 | old_sum = subprocess.check_output([f"sha256sum {main.file_loc}"],shell=True).decode() 68 | 69 | qtbot.mouseClick(mw.dialog_invalid_default_entry.btn_cancel,QtCore.Qt.LeftButton) 70 | 71 | new_sum =subprocess.check_output([f"sha256sum {main.file_loc}"],shell=True).decode() 72 | assert old_sum ==new_sum 73 | 74 | assert not mw.dialog_invalid_default_entry.isVisible() 75 | 76 | 77 | #Now lets press the fix button and check if it actually fixes the entry 78 | 79 | #first we need to call setUiElements to reshow that dialog 80 | mw.setUiElements(show_issues=True) 81 | 82 | assert mw.dialog_invalid_default_entry.isVisible() 83 | 84 | assert mw.comboBox_grub_default.currentIndex() == len(mw.all_entries)-1 85 | 86 | qtbot.mouseClick(mw.dialog_invalid_default_entry.btn_ok, QtCore.Qt.LeftButton) 87 | 88 | assert not mw.dialog_invalid_default_entry.isVisible() 89 | issues=[] 90 | with qtbot.waitSignal(mw.saveConfs_worker.signals.finished,timeout=30*1000): 91 | pass 92 | new_default_val = main.get_value("GRUB_DEFAULT=",issues) 93 | 94 | assert not issues 95 | assert new_default_val in mw.all_entries 96 | assert new_default_val not in mw.invalid_entries 97 | 98 | 99 | 100 | 101 | 102 | 103 | def test_fix_kernel_version_snapshot(qtbot): 104 | mw=main.Ui() 105 | main.MainWindow=mw 106 | 107 | if len(mw.invalid_entries)!=0: 108 | mw.dialog_invalid_default_entry.close() 109 | 110 | ind=2 111 | mw.comboBox_configurations.setCurrentIndex(2) 112 | #a snapshot would be selected now 113 | 114 | 115 | 116 | for val in mw.all_entries: 117 | if " >" not in val: 118 | continue 119 | 120 | new_val=change_krnl_minor_vrsn(val) 121 | break 122 | print("changing the current entry to invalid") 123 | mw.close() 124 | main.initialize_temp_file() 125 | main.set_value("GRUB_DEFAULT=",new_val) 126 | print(main.file_loc) 127 | subprocess.run([f"cp {HOME}/.cache/grub-editor/temp.txt {main.file_loc}"],shell=True) 128 | mw = main.Ui() 129 | main.MainWindow=mw 130 | mw.comboBox_configurations.setCurrentIndex(ind) 131 | 132 | assert mw.dialog_invalid_default_entry.isVisible() 133 | 134 | #First press the cancel button check if window closes and no changes were made to the snapshot/file 135 | print(main.file_loc,ind) 136 | 137 | old_sum = subprocess.check_output([f"sha256sum {main.file_loc}"],shell=True).decode() 138 | 139 | qtbot.mouseClick(mw.dialog_invalid_default_entry.btn_cancel,QtCore.Qt.LeftButton) 140 | 141 | new_sum =subprocess.check_output([f"sha256sum {main.file_loc}"],shell=True).decode() 142 | assert old_sum ==new_sum 143 | 144 | assert not mw.dialog_invalid_default_entry.isVisible() 145 | 146 | 147 | #Now lets press the fix button and check if it actually fixes the entry 148 | 149 | #first we need to call setUiElements to reshow that dialog 150 | mw.setUiElements(show_issues=True) 151 | 152 | assert mw.dialog_invalid_default_entry.isVisible() 153 | 154 | assert mw.comboBox_grub_default.currentIndex() == len(mw.all_entries)-1 155 | 156 | qtbot.mouseClick(mw.dialog_invalid_default_entry.btn_ok, QtCore.Qt.LeftButton) 157 | 158 | assert not mw.dialog_invalid_default_entry.isVisible() 159 | issues=[] 160 | 161 | new_default_val = main.get_value("GRUB_DEFAULT=",issues) 162 | 163 | assert not issues 164 | assert new_default_val in mw.all_entries 165 | assert new_default_val not in mw.invalid_entries 166 | 167 | 168 | def test_do_this_everytime(qtbot): 169 | 170 | 171 | 172 | mw = main.Ui() 173 | main.MainWindow=mw 174 | 175 | ind=2 176 | # -1 because lines has the list of snapshots but the configurations has /etc/default/grub 177 | target_snapshot=mw.lines[ind-1] 178 | target_snap_path=f'{main.DATA_LOC}/snapshots/{target_snapshot}' 179 | for val in mw.all_entries: 180 | if " >" not in val: 181 | continue 182 | 183 | new_val=change_krnl_minor_vrsn(val) 184 | break 185 | print("changing the current entry to invalid",new_val,target_snap_path) 186 | mw.close() 187 | main.set_value("GRUB_DEFAULT=",new_val,target_snap_path) 188 | mw = main.Ui() 189 | main.MainWindow=mw 190 | delete_pref() 191 | if len(mw.invalid_entries)!=0: 192 | mw.dialog_invalid_default_entry.close() 193 | 194 | pref=main.get_preference("invalid_kernel_version") 195 | assert pref is None 196 | mw.comboBox_configurations.setCurrentIndex(ind) 197 | 198 | assert mw.dialog_invalid_default_entry.isVisible() 199 | 200 | dialog =mw.dialog_invalid_default_entry 201 | QtTest.QTest.qWait(1000) 202 | print('done 1') 203 | pref=main.get_preference("invalid_kernel_version") 204 | assert pref is None 205 | 206 | if not dialog.checkBox.isChecked(): 207 | mw.dialog_invalid_default_entry.checkBox.click() 208 | 209 | pref=main.get_preference("invalid_kernel_version") 210 | assert pref is None 211 | 212 | qtbot.mouseClick(dialog.btn_cancel, QtCore.Qt.LeftButton) 213 | 214 | # QtTest.QTest.qWait(10000) 215 | pref=main.get_preference("invalid_kernel_version") 216 | assert pref == "cancel" 217 | 218 | assert not dialog.isVisible() 219 | 220 | #to reshow the dialog 221 | mw.setUiElements() 222 | 223 | #close the dialog if it popped up for /etc/default/grub entry 224 | if len(mw.invalid_entries)>0: 225 | mw.dialog_invalid_default_entry.close() 226 | 227 | mw.comboBox_configurations.setCurrentIndex(ind) 228 | 229 | dialog=mw.dialog_invalid_default_entry 230 | assert not dialog.isVisible() 231 | 232 | 233 | -------------------------------------------------------------------------------- /tests/test_get_set_value.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import sys 4 | from PyQt5 import QtWidgets,QtCore 5 | from tools import create_tmp_file 6 | 7 | 8 | from grubEditor import main 9 | 10 | commented_config="""#GRUB_DEFAULT="Manjaro Linux" 11 | #GRUB_TIMEOUT=20 12 | #GRUB_TIMEOUT_STYLE=menu 13 | #GRUB_DISTRIBUTOR="Manjaro" 14 | #GRUB_CMDLINE_LINUX_DEFAULT="quiet apparmor=1 security=apparmor udev.log_priority=3" 15 | #GRUB_CMDLINE_LINUX="" 16 | 17 | # If you want to enable the save default function, uncomment the following 18 | # line, and set GRUB_DEFAULT to saved. 19 | GRUB_SAVEDEFAULT=true 20 | 21 | # Preload both GPT and MBR modules so that they are not missed 22 | GRUB_PRELOAD_MODULES="part_gpt part_msdos" 23 | 24 | 25 | 26 | # Uncomment to ensure that the root filesystem is mounted read-only so that 27 | # systemd-fsck can run the check automatically. We use 'fsck' by default, which 28 | # needs 'rw' as boot parameter, to avoid delay in boot-time. 'fsck' needs to be 29 | # removed from 'mkinitcpio.conf' to make 'systemd-fsck' work. 30 | # See also Arch-Wiki: https://wiki.archlinux.org/index.php/Fsck#Boot_time_checking 31 | #GRUB_ROOT_FS_RO=true 32 | 33 | """ 34 | 35 | 36 | 37 | PATH= os.path.dirname(os.path.realpath(__file__)) 38 | HOME=os.getenv('HOME') 39 | 40 | 41 | 42 | 43 | def test_commented_lines(qtbot): 44 | tmp_file=create_tmp_file(commented_config) 45 | issues=[] 46 | val =main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file) 47 | assert val==None 48 | assert issues==[f"GRUB_DEFAULT= is commented out in {tmp_file}"] 49 | 50 | main.conf_handler.set("GRUB_DEFAULT=","Garuda Linux",tmp_file) 51 | 52 | 53 | issues=[] 54 | print(tmp_file) 55 | assert main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file)=='Garuda Linux' 56 | 57 | 58 | config_fake_comment=""" 59 | GRUB_DEFAULT="Manjaro Linux" 60 | GRUB_TIMEOUT=20 61 | GRUB_TIMEOUT_STYLE=menu 62 | GRUB_DISTRIBUTOR="Manjaro" 63 | GRUB_CMDLINE_LINUX_DEFAULT="quiet apparmor=1 security=apparmor udev.log_priority=3" 64 | GRUB_CMDLINE_LINUX="" 65 | # setting 'GRUB_DEFAULT=saved' 66 | """ 67 | 68 | #after means that fake comment comes after the actual value 69 | def test_fake_comment_after_get(qtbot): 70 | mw=main.Ui() 71 | main.MainWindow=mw 72 | qtbot.addWidget(mw) 73 | 74 | tmp_file=create_tmp_file(config_fake_comment) 75 | 76 | issues = [] 77 | val = main.conf_handler.get("GRUB_DEFAULT=",issues,read_file=tmp_file) 78 | assert val == 'Manjaro Linux' 79 | 80 | assert not issues 81 | 82 | 83 | 84 | def test_quotation_marks_trailing_space(qtbot): 85 | test_config="GRUB_DEFAULT=\"0\" " 86 | 87 | tmp_file=create_tmp_file(test_config) 88 | 89 | issues=[] 90 | val = main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file) 91 | assert val=='0' 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | #before means that fake comment comes before the actual value 101 | 102 | config_fake_comment_before=""" 103 | #GRUB_DEFAULT=0 104 | GRUB_DEFAULT="Manjaro Linux" 105 | GRUB_TIMEOUT_STYLE=menu 106 | GRUB_TIMEOUT=20 107 | GRUB_DISTRIBUTOR="Manjaro" 108 | GRUB_CMDLINE_LINUX_DEFAULT="quiet apparmor=1 security=apparmor udev.log_priority=3" 109 | GRUB_CMDLINE_LINUX="" 110 | # setting 'GRUB_DEFAULT=saved' 111 | """ 112 | 113 | def test_fake_comment_before_get(qtbot): 114 | """ Test with a commented key value pair before the uncommented key value pair """ 115 | 116 | tmp_file=f'{main.CACHE_LOC}/temp3.txt' 117 | subprocess.run([f'touch {tmp_file}'],shell=True) 118 | 119 | with open(tmp_file,'w') as f: 120 | f.write(config_fake_comment_before) 121 | 122 | issues=[] 123 | val =main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file) 124 | assert issues ==[] 125 | assert val == 'Manjaro Linux' 126 | 127 | 128 | config_last=""" 129 | GRUB_DEFAULT="Manjaro Linux" 130 | GRUB_TIMEOUT=20 131 | GRUB_TIMEOUT_STYLE=menu""" 132 | 133 | def test_last_value(qtbot): 134 | 135 | tmp_file=f'{main.CACHE_LOC}/temp4.txt' 136 | subprocess.run([f'touch {tmp_file}'],shell=True) 137 | 138 | with open(tmp_file,'w') as f: 139 | f.write(config_last) 140 | 141 | issues=[] 142 | val =main.conf_handler.get("GRUB_TIMEOUT_STYLE=",issues,tmp_file) 143 | assert issues ==[] 144 | assert val=="menu" 145 | 146 | def test_not_in_conf_val(qtbot): 147 | ''' Looking for a value that is not mentioned in the configuration ''' 148 | 149 | tmp_file=f'{main.CACHE_LOC}/temp5.txt' 150 | subprocess.run([f'touch {tmp_file}'],shell=True) 151 | 152 | with open(tmp_file,'w') as f: 153 | f.write(config_last) 154 | issues=[] 155 | 156 | val =main.conf_handler.get("GRUB_CMDLINE_LINUX=",issues,tmp_file) 157 | assert issues ==[f"GRUB_CMDLINE_LINUX= was not found in {tmp_file}"] 158 | assert val == None 159 | 160 | main.conf_handler.set("GRUB_CMDLINE_LINUX=","something fake",tmp_file) 161 | issues=[] 162 | new_val = main.conf_handler.get("GRUB_CMDLINE_LINUX=",issues,tmp_file) 163 | assert new_val =="something fake" 164 | assert issues==[] 165 | 166 | def test_missing_double_quotes(qtbot): 167 | config="GRUB_DEFAULT=Manjaro Linux" 168 | tmp_file=create_tmp_file(config) 169 | 170 | issues=[] 171 | val =main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file) 172 | 173 | assert issues == [] 174 | assert val == "Manjaro Linux (Missing \")" 175 | 176 | 177 | def test_grub_default_saved(): 178 | config="GRUB_DEFAULT=saved" 179 | tmp_file=create_tmp_file(config) 180 | 181 | issues=[] 182 | val = main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file) 183 | assert val=="saved" 184 | assert issues==[] 185 | 186 | 187 | CONFIG_QUOTED =""" 188 | GRUB_DEFAULT="Manjaro Linux" """ 189 | 190 | def test_remove_quotes(qtbot): 191 | tmp_file = create_tmp_file(CONFIG_QUOTED) 192 | issues = [] 193 | val = main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file,remove_quotes_=True) 194 | assert val == "Manjaro Linux" 195 | val = main.conf_handler.get("GRUB_DEFAULT=",issues,tmp_file,remove_quotes_=False) 196 | assert val == "\"Manjaro Linux\"" 197 | 198 | -------------------------------------------------------------------------------- /tests/test_grubcfg_error.py: -------------------------------------------------------------------------------- 1 | 2 | import subprocess 3 | from time import sleep 4 | 5 | from PyQt5 import QtCore 6 | 7 | from grubEditor import main 8 | from grubEditor.libs import find_entries 9 | 10 | 11 | 12 | def test_grub_cfg_not_found(qtbot): 13 | 14 | #change the GRUB_CONF to a non-existing file so that not found error will be raised 15 | find_entries.GRUB_CONF_NONEDITABLE="/boot/grub/grub1.cfg" 16 | 17 | 18 | 19 | mw=main.Ui() 20 | main.MainWindow=mw 21 | qtbot.addWidget(mw) 22 | 23 | assert mw.dialog_grub_cfg_not_found.isVisible() 24 | 25 | find_entries.GRUB_CONF="/boot/grub/grub.cfg" 26 | 27 | qtbot.mouseClick(mw.dialog_grub_cfg_not_found.btn_ok, QtCore.Qt.LeftButton) 28 | 29 | assert not mw.dialog_grub_cfg_not_found.isVisible() 30 | 31 | 32 | 33 | def test_grub_cfg_permission(qtbot): 34 | 35 | subprocess.run(['pkexec chmod 600 /boot/grub/grub.cfg'],shell=True) 36 | 37 | mw=main.Ui() 38 | main.MainWindow=mw 39 | qtbot.addWidget(mw) 40 | 41 | sleep(1) 42 | assert mw.dialog_cfg_permission.isVisible() 43 | 44 | subprocess.run(['pkexec chmod 644 /boot/grub/grub.cfg'],shell=True) 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/test_invalid_default_entry.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | from PyQt5 import QtWidgets,QtCore 6 | 7 | from grubEditor import main 8 | 9 | def test_invalid_default_entry(qtbot): 10 | mw=main.Ui() 11 | main.MainWindow=mw 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/test_output_widget.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from time import sleep 4 | from PyQt5 import QtWidgets,QtCore,QtTest 5 | from tools import * 6 | from grubEditor import main 7 | 8 | def test_no_show_details(qtbot): 9 | """ Test the usage of show_details btn when update-grub is performed """ 10 | 11 | mw=main.Ui() 12 | main.MainWindow=mw 13 | main.DEBUG=True 14 | 15 | qtbot.mouseClick(mw.btn_add,QtCore.Qt.LeftButton) 16 | assert not scrollArea_visible(mw) 17 | 18 | #click btn set and check scroll area part is invisible 19 | qtbot.mouseClick(mw.btn_set,QtCore.Qt.LeftButton) 20 | assert not scrollArea_visible(mw) 21 | 22 | while password_not_entered(mw): 23 | sleep(1) 24 | print("count",mw.verticalLayout.count()) 25 | 26 | 27 | #click show details and check if scroll area is visible 28 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 29 | assert scrollArea_visible(mw) 30 | 31 | #Now click hide details button and check if scroll area is invisible 32 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 33 | assert not scrollArea_visible(mw) 34 | 35 | QtTest.QTest.qWait(1000) 36 | 37 | #Now repeat the same process again 38 | #click show details and check if scroll area is visible 39 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 40 | assert scrollArea_visible(mw) 41 | 42 | #Now click hide details button and check if scroll area is invisible 43 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 44 | assert not scrollArea_visible(mw) 45 | -------------------------------------------------------------------------------- /tests/test_progress.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import sys 4 | from time import sleep 5 | from PyQt5 import QtWidgets,QtCore 6 | from tools import * 7 | 8 | from grubEditor.widgets.progress import ProgressUi 9 | 10 | def test_btn_show_details(qtbot): 11 | #mw is mainWindow 12 | mw=ProgressUi() 13 | qtbot.addWidget(mw) 14 | 15 | #click btn show details and check if scroll area is shown 16 | qtbot.mouseClick(mw.btn_show_details, QtCore.Qt.LeftButton) 17 | assert scrollArea_visible(mw,mw.verticalLayout) 18 | 19 | #check if btn show details is now named to hide details 20 | assert mw.verticalLayout.itemAt(2).widget().text()=='Hide details' 21 | 22 | #click hide details and check if it works 23 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 24 | assert mw.verticalLayout.itemAt(2).widget().text()=='Show details' 25 | assert None == mw.verticalLayout.itemAt(3) 26 | 27 | 28 | 29 | # add another button to the window and then check if QScrollArea is created in right 30 | # place when btn_show_details is pressed 31 | btn_close =QtWidgets.QPushButton() 32 | btn_close.setText("Close") 33 | def btn_close_callback(): 34 | print("this button isnt supposed to work") 35 | 36 | btn_close.clicked.connect(btn_close_callback) 37 | mw.verticalLayout.addWidget(btn_close) 38 | 39 | 40 | vcount =mw.verticalLayout.count() 41 | #check if last widget is the close button 42 | assert mw.verticalLayout.itemAt(vcount-1).widget().text() == 'Close' 43 | 44 | 45 | #now lets press the button 46 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 47 | 48 | #check if QScrollArea has been created in 3rd index 49 | assert scrollArea_visible(mw,mw.verticalLayout) 50 | 51 | line1="just pretend this is the first line of something important" 52 | 53 | mw.update_lbl_details(line1) 54 | 55 | assert mw.lbl_details_text ==line1 56 | 57 | assert mw.lbl_details.text()==line1 58 | 59 | line2="\npretent that this is the second line of some big text" 60 | mw.update_lbl_details(line2) 61 | 62 | assert mw.lbl_details_text ==line1+line2 63 | assert mw.lbl_details.text()==line1+line2 64 | 65 | #click hide details button and check if lbl_details_text percists 66 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 67 | 68 | assert mw.lbl_details_text ==line1+line2 69 | 70 | #now click show details and check if lbl_details has the text that it is supposed to have 71 | qtbot.mouseClick(mw.btn_show_details,QtCore.Qt.LeftButton) 72 | assert mw.lbl_details.text()==line1+line2 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /tests/test_quotes_values.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from tools import create_tmp_file,create_snapshot 4 | 5 | from grubEditor.core import GRUB_CONF 6 | from grubEditor import main 7 | 8 | QUOTED_GRUB_TIMEOUT=""" 9 | GRUB_DEFAULT="Manjaro Linux" 10 | GRUB_TIMEOUT="-1" 11 | GRUB_TIMEOUT_STYLE=menu 12 | GRUB_DISTRIBUTOR="Manjaro" 13 | GRUB_CMDLINE_LINUX_DEFAULT="quiet apparmor=1 security=apparmor udev.log_priority=3" 14 | GRUB_CMDLINE_LINUX="" 15 | # """ 16 | 17 | 18 | def test_quoted_grub_timeout(qtbot): 19 | mw = main.Ui() 20 | main.MainWindow=mw 21 | qtbot.addWidget(mw) 22 | FILE_PATH = create_tmp_file(QUOTED_GRUB_TIMEOUT) 23 | main.conf_handler.current_file = FILE_PATH 24 | 25 | mw.setUiElements() 26 | val = main.conf_handler.get(GRUB_CONF.GRUB_TIMEOUT,[]) 27 | assert val == "\"-1\"" 28 | assert not mw.checkBox_boot_default_entry_after.isChecked() -------------------------------------------------------------------------------- /tests/test_reinstall_grub_package.py: -------------------------------------------------------------------------------- 1 | # import unittest 2 | 3 | # import os,sys,inspect 4 | 5 | 6 | # #stolen from stackoverflow or grepperchrome 7 | # currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 8 | # parentdir = os.path.dirname(currentdir) 9 | # sys.path.insert(0,parentdir) 10 | 11 | 12 | # import main 13 | # import threading 14 | # from PyQt5 import QtWidgets,QtCore 15 | # import sys 16 | # from time import sleep 17 | # import pytest 18 | 19 | 20 | 21 | # def test_btn_reinstall_grub(qtbot): 22 | # main.MainWindow=main.Ui() 23 | # mw=main.MainWindow 24 | # mw.tabWidget.setCurrentIndex(2) 25 | 26 | 27 | # lv=mw.chroot.listWidget 28 | # item =lv.item(0) 29 | # rect = lv.visualItemRect(item) 30 | # center = rect.center() 31 | # print(center) 32 | # print(item.text()) 33 | # assert lv.itemAt(center).text() == item.text() 34 | # # assert lv.currentRow() == 0 35 | 36 | 37 | # qtbot.mouseClick(mw.chroot.listWidget.viewport(),QtCore.Qt.LeftButton,pos=center) 38 | # # mw.chroot.listWidget.item(2).click() 39 | # assert type( mw.tabWidget.currentWidget()).__name__ =='ChrootAfterUi' 40 | # currentWidget=mw.tabWidget.currentWidget() 41 | # qtbot.mouseClick(currentWidget.btn_reinstall_grub_package,QtCore.Qt.LeftButton) 42 | # sleep(2) 43 | 44 | -------------------------------------------------------------------------------- /tests/test_remove_value.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import sys 4 | from PyQt5 import QtWidgets,QtCore 5 | from pathlib import Path 6 | 7 | # PATH=os.path.dirname(os.path.realpath(__file__)) 8 | # PARENT_PATH=str(Path(PATH).parent) 9 | 10 | 11 | # print(PARENT_PATH) 12 | # sys.path.append(PARENT_PATH) 13 | 14 | from grubEditor.core import CONF_HANDLER 15 | from tools import create_tmp_file,get_file_sum 16 | from grubEditor import main 17 | from grubEditor.locations import CACHE_LOC 18 | 19 | conf_handler = CONF_HANDLER() 20 | get_value = conf_handler.get 21 | set_value = conf_handler.set 22 | remove_value = conf_handler.remove 23 | 24 | commented_config="""#GRUB_DEFAULT="Manjaro Linux" 25 | #GRUB_TIMEOUT=20 26 | #GRUB_TIMEOUT_STYLE=menu 27 | #GRUB_DISTRIBUTOR="Manjaro" 28 | #GRUB_CMDLINE_LINUX_DEFAULT="quiet apparmor=1 security=apparmor udev.log_priority=3" 29 | #GRUB_CMDLINE_LINUX="" 30 | 31 | # If you want to enable the save default function, uncomment the following 32 | # line, and set GRUB_DEFAULT to saved. 33 | GRUB_SAVEDEFAULT=true 34 | 35 | # Preload both GPT and MBR modules so that they are not missed 36 | GRUB_PRELOAD_MODULES="part_gpt part_msdos" 37 | 38 | 39 | 40 | # Uncomment to ensure that the root filesystem is mounted read-only so that 41 | # systemd-fsck can run the check automatically. We use 'fsck' by default, which 42 | # needs 'rw' as boot parameter, to avoid delay in boot-time. 'fsck' needs to be 43 | # removed from 'mkinitcpio.conf' to make 'systemd-fsck' work. 44 | # See also Arch-Wiki: https://wiki.archlinux.org/index.php/Fsck#Boot_time_checking 45 | #GRUB_ROOT_FS_RO=true 46 | 47 | """ 48 | 49 | PATH= os.path.dirname(os.path.realpath(__file__)) 50 | HOME=os.getenv('HOME') 51 | 52 | 53 | 54 | 55 | 56 | def test_commented_lines(qtbot): 57 | tmp_file=create_tmp_file(commented_config) 58 | issues=[] 59 | val =get_value("GRUB_DEFAULT=",issues,tmp_file) 60 | assert val is None 61 | assert issues==[f"GRUB_DEFAULT= is commented out in {tmp_file}"] 62 | 63 | #check that the file doesn't change when trying to remove an already commented 64 | #config 65 | old_sum=get_file_sum(tmp_file) 66 | remove_value("GRUB_DEFAULT=",tmp_file) 67 | new_sum=get_file_sum(tmp_file) 68 | assert old_sum == new_sum 69 | 70 | 71 | set_value("GRUB_DEFAULT=","Garuda Linux",tmp_file) 72 | 73 | 74 | issues=[] 75 | print(tmp_file) 76 | assert get_value("GRUB_DEFAULT=",issues,tmp_file)=='Garuda Linux' 77 | 78 | remove_value("GRUB_DEFAULT=",tmp_file) 79 | assert get_value("GRUB_DEFAULT=",issues,tmp_file) is None 80 | 81 | 82 | 83 | 84 | 85 | 86 | config_last=""" 87 | GRUB_DEFAULT="Manjaro Linux" 88 | GRUB_TIMEOUT=20 89 | GRUB_TIMEOUT_STYLE=menu""" 90 | 91 | def test_last_value(qtbot): 92 | 93 | tmp_file=f'{CACHE_LOC}/temp4.txt' 94 | subprocess.run([f'touch {tmp_file}'],shell=True) 95 | 96 | with open(tmp_file,'w') as f: 97 | f.write(config_last) 98 | 99 | issues=[] 100 | val =get_value("GRUB_TIMEOUT_STYLE=",issues,tmp_file) 101 | assert issues ==[] 102 | assert val=="menu" 103 | 104 | remove_value("GRUB_TIMEOUT_STYLE=",tmp_file) 105 | assert get_value("GRUB_TIMEOUT_STYLE=",issues,tmp_file) is None 106 | 107 | 108 | -------------------------------------------------------------------------------- /tests/test_snapshots.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from PyQt5 import QtWidgets,QtCore 4 | import subprocess 5 | from time import sleep 6 | from datetime import datetime as dt 7 | import re 8 | from tools import change_comboBox_current_index , windows 9 | PATH = os.path.dirname(os.path.realpath(__file__)) 10 | #get the parent directory 11 | PATH = PATH[0:-5] 12 | # sys.path.append(PATH) 13 | 14 | from grubEditor import main 15 | from grubEditor.core import GRUB_CONF, conf_handler 16 | 17 | HOME=os.getenv("HOME") 18 | 19 | #checks if modified is shown but the main purpose of this function is to use it in other tests 20 | def test_change_config_modified(qtbot): 21 | #mw stand for mainWindow 22 | mw=main.Ui() 23 | main.MainWindow=mw; 24 | curr_ind = mw.comboBox_grub_default.currentIndex() 25 | for i in range(len(mw.all_entries)): 26 | if i!= curr_ind: 27 | grub_default_ind = i 28 | break 29 | # todo here 30 | mw.comboBox_grub_default.setCurrentIndex(grub_default_ind) 31 | 32 | 33 | 34 | 35 | def _test_ignore_changes(qtbot,mw,grub_default_ind): 36 | ''' Assumes that the grub default is in modified state ''' 37 | 38 | 39 | #read data from /etc/default/grub 40 | with open('/etc/default/grub') as f: 41 | conf_data = f.read() 42 | 43 | assert mw.comboBox_grub_default in mw.original_modifiers 44 | 45 | 46 | qtbot.mouseClick(mw.create_snapshot_dialog.btn_ignore_changes, QtCore.Qt.LeftButton) 47 | 48 | assert not mw.create_snapshot_dialog.isVisible() 49 | date_time =str(dt.now()).replace(' ','_')[:-7] 50 | 51 | snapshots =[] 52 | for i in range(len(mw.configurations)): 53 | snapshots.append(mw.comboBox_configurations.itemText(i)) 54 | 55 | 56 | #check if a snapshot was created 57 | for i in range(len(mw.configurations)): 58 | item_text=mw.comboBox_configurations.itemText(i) 59 | try: 60 | snapshots.remove(item_text) 61 | except: 62 | if '(modified)' not in item_text: 63 | assert item_text==date_time 64 | break 65 | 66 | #check if new snapshot actually ignored the changes 67 | with open(f'{main.DATA_LOC}/snapshots/{date_time}') as f: 68 | new_data =f.read() 69 | 70 | assert new_data ==conf_data 71 | 72 | assert grub_default_ind == mw.comboBox_grub_default.currentIndex() 73 | 74 | #check if it is showing a modified version of /etc/default/grub 75 | 76 | assert '(modified)' in mw.configurations[mw.comboBox_configurations.currentIndex()] 77 | 78 | 79 | def _test_add_changes_snapshot(qtbot,mw): 80 | #now lets do the same test but with btn_add_changes_to_snapshot 81 | 82 | 83 | 84 | date_time =str(dt.now()).replace(' ','_')[:-7] 85 | 86 | snapshots =[] 87 | for i in range(len(mw.configurations)): 88 | snapshots.append(mw.comboBox_configurations.itemText(i)) 89 | 90 | 91 | #check if a snapshot was created 92 | for i in range(len(mw.configurations)): 93 | try: 94 | snapshots.remove(mw.comboBox_configurations.itemText(i)) 95 | except Exception: 96 | if '(modified)' not in mw.comboBox_configurations.itemText(i): 97 | assert mw.comboBox_configurations.itemText(i)==date_time 98 | break 99 | 100 | #check if it is showing a modified version of /etc/default/grub 101 | assert '(modified)' in mw.configurations[mw.comboBox_configurations.currentIndex()] 102 | 103 | new_snapshot=f'{main.DATA_LOC}/snapshots/{date_time}' 104 | 105 | cmd = f"diff {new_snapshot} /etc/default/grub" 106 | diff_out = subprocess.run([cmd],shell=True,capture_output=True).stdout.decode() 107 | lines = diff_out.splitlines() 108 | print(lines,"lines") 109 | #first line would be something like 1c1 or 2c2 110 | assert lines[0][0]==lines[0][2] 111 | assert lines[0][1]=='c' 112 | 113 | assert 'GRUB_DEFAULT=' in lines[1] 114 | assert 'GRUB_DEFAULT=' in lines[3] 115 | 116 | def test_btn_create_snapshot(qtbot): 117 | #mw stand for mainWindow 118 | mw=main.Ui() 119 | main.MainWindow=mw; 120 | mw.tabWidget.setCurrentIndex(1) 121 | qtbot.addWidget(mw) 122 | 123 | #this test only works if grub default is predefined 124 | assert conf_handler.get(GRUB_CONF.GRUB_DEFAULT,[]) != "saved" 125 | 126 | #check if another configuration gets added when btn_create_snapshot is pressed 127 | snapshots_count =len(mw.configurations)-1 128 | qtbot.mouseClick(mw.btn_create_snapshot,QtCore.Qt.LeftButton) 129 | new_snapshots_count=len(mw.configurations)-1 130 | assert new_snapshots_count -1== snapshots_count 131 | 132 | 133 | 134 | 135 | #get to the edit_configurations tab and then change something to check if a new windows open ups to ask which 136 | # configuration i want to save to snapshot (from the file or edited one) 137 | mw.tabWidget.setCurrentIndex(0) 138 | 139 | grub_default_ind=change_comboBox_current_index(mw) 140 | assert '(modified)' in mw.configurations[mw.comboBox_configurations.currentIndex()] 141 | 142 | mw.tabWidget.setCurrentIndex(1) 143 | 144 | #remove default preference 145 | main.set_preference("create_snapshot","None") 146 | 147 | 148 | qtbot.mouseClick(mw.btn_create_snapshot,QtCore.Qt.LeftButton) 149 | 150 | assert mw.create_snapshot_dialog.isVisible() 151 | 152 | qtbot.mouseClick(mw.btn_create_snapshot,QtCore.Qt.LeftButton) 153 | 154 | assert mw.create_snapshot_dialog.isVisible() 155 | 156 | _test_ignore_changes(qtbot,mw,grub_default_ind) 157 | 158 | sleep(1) 159 | 160 | 161 | qtbot.mouseClick(mw.create_snapshot_dialog.btn_add_changes_to_snapshot, QtCore.Qt.LeftButton) 162 | assert not mw.create_snapshot_dialog.isVisible() 163 | 164 | _test_add_changes_snapshot(qtbot,mw) 165 | 166 | def test_btn_delete_snapshot(qtbot): 167 | #mw stand for mainWindow 168 | mw=main.Ui() 169 | main.MainWindow=mw; 170 | mw.tabWidget.setCurrentIndex(1) 171 | 172 | snapshots = [] 173 | for i in range(len(mw.configurations)): 174 | snapshots.append(mw.comboBox_configurations.itemText(i)) 175 | 176 | date_time =str(dt.now()).replace(' ','_')[:-7] 177 | 178 | qtbot.mouseClick(mw.btn_create_snapshot, QtCore.Qt.LeftButton) 179 | 180 | snapshot_is_in_list_var= False 181 | #check if a snapshot was created 182 | for i in range(len(mw.configurations)): 183 | try: 184 | snapshots.remove(mw.comboBox_configurations.itemText(i)) 185 | except ValueError: 186 | if '(modified)' not in mw.comboBox_configurations.itemText(i): 187 | if mw.comboBox_configurations.itemText(i)==date_time: 188 | snapshot_is_in_list_var =True 189 | break 190 | assert snapshot_is_in_list_var==True 191 | 192 | for i in range(mw.VLayout_snapshot.count()): 193 | text =mw.VLayout_snapshot.itemAt(i).itemAt(0).widget().text() 194 | if text == date_time: 195 | new_snapshot_ind = i 196 | break 197 | assert main.GRUB_CONF_LOC== mw.configurations[mw.comboBox_configurations.currentIndex()] 198 | 199 | #now that we have found the index of the snapshot we have just created 200 | #Lets find the delete btn of the snapshot 201 | print(i) 202 | target_snapshot_row=mw.VLayout_snapshot.itemAt(new_snapshot_ind) 203 | btn_delete = target_snapshot_row.itemAt(3).widget() 204 | 205 | assert target_snapshot_row.itemAt(0).widget().text()==date_time 206 | 207 | mw.tabWidget.setCurrentIndex(0) 208 | 209 | #change something the edit_configurations UI and check if the value persists after deleti 210 | mw.btn_substract.click() 211 | assert '(modified)' in mw.configurations[mw.comboBox_configurations.currentIndex()] 212 | 213 | #get current value of grub timeout 214 | old_val = mw.ledit_grub_timeout.text() 215 | 216 | mw.tabWidget.setCurrentIndex(1) 217 | 218 | qtbot.mouseClick(btn_delete,QtCore.Qt.LeftButton) 219 | sleep(1) 220 | assert main.GRUB_CONF_LOC+"(modified)"== mw.configurations[mw.comboBox_configurations.currentIndex()] 221 | 222 | assert mw.VLayout_snapshot.itemAt(new_snapshot_ind) == None 223 | 224 | assert '(modified)' in mw.configurations[mw.comboBox_configurations.currentIndex()] 225 | 226 | mw.tabWidget.setCurrentIndex(0) 227 | 228 | #get the new value of grub timeout 229 | assert mw.ledit_grub_timeout.text() == old_val 230 | 231 | assert '(modified)' in mw.configurations[mw.comboBox_configurations.currentIndex()] 232 | 233 | 234 | 235 | 236 | #test the view snapshot button 237 | def test_btn_view(qtbot): 238 | 239 | #mw stand for mainWindow 240 | mw=main.Ui() 241 | main.MainWindow=mw 242 | 243 | #first find the view btn of the first snapshots 244 | #before that we need to create a snapshot if no snapshots exist 245 | if mw.VLayout_snapshot.itemAt(0) is None: 246 | qtbot.mouseClick(mw.btn_create_snapshot, QtCore.Qt.LeftButton) 247 | 248 | snapshot_name =mw.VLayout_snapshot.itemAt(0).layout().itemAt(0).widget().text() 249 | btn_view = mw.VLayout_snapshot.itemAt(0).layout().itemAt(2).widget() 250 | 251 | #check if it is view button 252 | assert btn_view.text()=='view' 253 | 254 | #delete the preferences file 255 | subprocess.run([f"rm {main.CONFIG_LOC}/main.json"],shell=True) 256 | 257 | qtbot.mouseClick(btn_view, QtCore.Qt.LeftButton) 258 | 259 | 260 | #check if btn_view_window is visible 261 | assert mw.view_btn_win.isVisible() 262 | 263 | mw.view_btn_win.close() 264 | 265 | 266 | #now check if that view_btn_win opens when preference has a value 267 | main.set_preference("view_default","default_text_editor") 268 | 269 | qtbot.mouseClick(btn_view,QtCore.Qt.LeftButton) 270 | assert not mw.view_btn_win.isVisible() 271 | assert mw.comboBox_configurations.currentText() =='/etc/default/grub' 272 | 273 | #! only works when kate is default text editor if not test has to be changed 274 | #might fail when kate takes too long to load 275 | sleep(5) 276 | windows= subprocess.check_output(["wmctrl -l "],shell=True).decode() 277 | assert f"{snapshot_name} — Kate" in windows 278 | 279 | main.set_preference("view_default","on_the_application_itself") 280 | 281 | qtbot.mouseClick(btn_view,QtCore.Qt.LeftButton) 282 | assert not mw.view_btn_win.isVisible() 283 | assert mw.tabWidget.currentIndex() ==0 284 | 285 | assert mw.comboBox_configurations.currentText() ==f"{snapshot_name}" 286 | 287 | 288 | 289 | 290 | def test_btn_set(qtbot): 291 | mw=main.Ui() 292 | main.MainWindow=mw 293 | mw.tabWidget.setCurrentIndex(1) 294 | 295 | 296 | if mw.VLayout_snapshot.count()==0: 297 | qtbot.mouseClick(mw.btn_create_snapshot,QtCore.Qt.LeftButton) 298 | 299 | row=mw.VLayout_snapshot.itemAt(0).layout() 300 | btn_set = row.itemAt(4).widget() 301 | snapshot_name=row.itemAt(0).widget().text() 302 | assert main.conf_handler.current_file==main.GRUB_CONF_LOC 303 | qtbot.mouseClick(btn_set,QtCore.Qt.LeftButton) 304 | 305 | assert mw.lbl_status.text() =="Waiting for authentication" 306 | sleep(1) 307 | win_list=windows()[0] 308 | auth_win_vis=False 309 | for i in range(len(win_list)): 310 | print(win_list[i]) 311 | if "Authentication Required — PolicyKit1" in win_list[i]: 312 | auth_win_vis=True 313 | break 314 | 315 | assert auth_win_vis==True 316 | 317 | with qtbot.waitSignal(mw.set_snapshot_worker.signals.finished,timeout=30*1000): 318 | pass 319 | 320 | conf_sum = subprocess.check_output([f"sha256sum {main.GRUB_CONF_LOC}"],shell=True).decode() 321 | snapshot_sum = subprocess.check_output([f"sha256sum {main.DATA_LOC}/snapshots/{snapshot_name}"],shell=True).decode() 322 | assert conf_sum[:65]==snapshot_sum[:65] -------------------------------------------------------------------------------- /tests/test_widget_dialog.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | 5 | 6 | PATH = os.path.dirname(os.path.realpath(__file__)) 7 | #get the parent directory 8 | PATH = Path(PATH).parent 9 | sys.path.append(PATH) 10 | 11 | 12 | import main 13 | 14 | def test_setTextBtn(qtbot): 15 | mw=main.DialogUi() 16 | qtbot.addWidget(mw) 17 | 18 | mw.setText("hey there") 19 | assert mw.label.text()=="hey there" 20 | 21 | mw.setBtnOkText("Not ok") 22 | assert mw.btn_ok.text()=="Not ok" 23 | 24 | -------------------------------------------------------------------------------- /tests/tools.py: -------------------------------------------------------------------------------- 1 | """ Tools for testing """ 2 | 3 | import subprocess 4 | import traceback 5 | from random import randint 6 | import os 7 | import sys 8 | 9 | from pathlib import Path 10 | 11 | 12 | 13 | PATH=os.path.dirname(os.path.realpath(__file__)) 14 | PARENT_PATH=str(Path(PATH).parent) 15 | 16 | 17 | print(PARENT_PATH) 18 | sys.path.append(PARENT_PATH) 19 | from grubEditor.main import main 20 | 21 | HOME=os.getenv("HOME") 22 | 23 | def change_comboBox_current_index(mw): 24 | """ Changes the current index of the combobox default entries 25 | for numbers from 0 - max it will put the minimum that is not current value 26 | """ 27 | curr_ind = mw.comboBox_grub_default.currentIndex() 28 | 29 | for i in range(len(mw.all_entries)): 30 | if i!= curr_ind: 31 | grub_default_ind = i 32 | break 33 | 34 | mw.comboBox_grub_default.setCurrentIndex(grub_default_ind) 35 | 36 | return grub_default_ind 37 | 38 | def windows(): 39 | """Returns windows names list and their id list in a tuple""" 40 | #shows all the visible windows 41 | exception_found=False 42 | while True: 43 | try: 44 | window_list= subprocess.check_output(['wmctrl -l'],shell=True) 45 | window_list= window_list.decode() 46 | # printer(type(window_list)) 47 | window_list=window_list.split('\n') 48 | # printer(window_list,'window_list') 49 | exception_found=False 50 | except Exception as e: 51 | exception_found=True 52 | print(str(e)) 53 | print(traceback.format_exc()) 54 | print('error in windows function call but it was handled like a boss😎') 55 | print('hope i aint struck in this loop 😅') 56 | finally: 57 | if not exception_found: 58 | break 59 | 60 | final_id_list =[] 61 | final_window_list =[] 62 | for window in window_list: 63 | window = window.split() 64 | # print(window) 65 | while True: 66 | if '' in window: 67 | window.remove('') 68 | else: 69 | break 70 | 71 | # print(window,'window') 72 | if len(window)>3: 73 | # printer(window) 74 | final_id_list.append(window[0]) 75 | window.pop(0) 76 | window.pop(0) 77 | window.pop(0) 78 | 79 | # printer(window) 80 | tmp='' 81 | # print(window) 82 | for word in window: 83 | tmp = tmp+' '+word 84 | # printer(tmp) 85 | final_window_list.append(tmp[1:]) 86 | # print(final_window_list) 87 | return final_window_list,final_id_list 88 | 89 | def create_tmp_file(data): 90 | """ Creates a file with the data provided as argument and returns the path of file """ 91 | value =randint(0,20) 92 | tmp_file=f'{HOME}/.cache/grub-editor/temp{value}.txt' 93 | subprocess.run([f'touch {tmp_file}'],shell=True) 94 | 95 | with open(tmp_file,'w') as f: 96 | f.write(data) 97 | 98 | return tmp_file 99 | 100 | def create_test_file(data): 101 | """ Creates a file with the data provided as argument and returns the name of file """ 102 | value =randint(0,20) 103 | test_file=f'{HOME}/.cache/grub-editor/test{value}.txt' 104 | subprocess.run([f'touch {test_file}'],shell=True) 105 | 106 | with open(test_file,'w') as f: 107 | f.write(data) 108 | 109 | return test_file 110 | 111 | def create_snapshot(data): 112 | """ Create a snapshot with the data provided as argument 113 | and returns the name of the snapshot 114 | Eg name: test_snapshot0 115 | Snapshot path is f"{main.DATA_LOC}/snapshots/" 116 | 117 | """ 118 | num=randint(0,20) 119 | 120 | snapshot_name=f"{main.DATA_LOC}/snapshots/test_snapshot{num}" 121 | subprocess.run([f"touch {snapshot_name}"],shell=True) 122 | 123 | with open(snapshot_name,'w') as f: 124 | f.write(data) 125 | 126 | return f"test_snapshot{num}" 127 | 128 | def scrollArea_visible(mw,targetLayout=None)->bool: 129 | """ Checks if the scroll area which shows more details is visible 130 | Warning:dependant on the current layout of window. May fail if a new widget was inserted 131 | """ 132 | if targetLayout is None: 133 | targetLayout=mw.verticalLayout_2 134 | 135 | print(targetLayout.count()) 136 | for i in range(targetLayout.count()): 137 | print(i,targetLayout.itemAt(i).widget()) 138 | if 'QScrollArea' in str(targetLayout.itemAt(i).widget()): 139 | return True 140 | return False 141 | 142 | 143 | def password_not_entered(mw): 144 | return mw.lbl_status.text() =="Waiting for authentication" 145 | 146 | def delete_pref(): 147 | """ Deletes the user preference file """ 148 | subprocess.run([f"rm {main.CONFIG_LOC}/main.json"],shell=True) 149 | 150 | def get_file_sum(file_path): 151 | """ Returns the sum of the file provided as argument """ 152 | return subprocess.check_output([f"sha256sum {file_path}"],shell=True).decode() 153 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | 2 | check if the current grub.cfg is up to date with /etc/default/grub 3 | check if other operating systems are there by executing os prober and if it takes too long then use grub.cfg to find out 4 | show no snapshots found above the list widget if no snapshots were found 5 | add UEFI support 6 | 7 | if grub default has an invalid value then a Dialog is show but add a checkbox to allow the user to disable that dialog 8 | 9 | write tests for checks if error_dialog text of the label part in qt designer is in the value used in error_dialog.py 10 | 11 | dynamicaly set combobox item text with resize event so that no part if item text will be hidden 12 | 13 | 14 | fix bug that causes the loading configuration from to change when default text editor is selected on view button 's new window 15 | 16 | 17 | pop the last invalid default entry when fix has finished 18 | 19 | checkbox do this everytime in invalid grub default fixer 20 | 21 | if user preferes fixing the invalid_kernel_version then inform the user that permissions are needed fix the invalid kernerl 22 | if /etc/default/grub has invallid entry 23 | 24 | do not allow brackets in the value of snapshot names 25 | 26 | 27 | Invalid entry grub default error when look for other os is turned off 28 | 29 | Fix inconsistency of removing quotes in GRUB_DEFAULT with get_value but for other keys accepting argument called remove_quotes_ 30 | 31 | Add tests to handle GRUB_DEFAULT covered in single quotes --------------------------------------------------------------------------------