added init from other hoster

This commit is contained in:
Zac Poorman 2026-01-09 12:15:52 -06:00
parent a7a76ae599
commit 2858b81f29
9 changed files with 866 additions and 6 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

51
.gitignore vendored Normal file
View File

@ -0,0 +1,51 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.venv/
venv/
ENV/
env/
# Testing
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.tox/
.nox/
*.cover
*.log
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Project specific
*.spec

20
LICENSE
View File

@ -1,9 +1,21 @@
MIT License
Copyright (c) 2026 LOCTight
Copyright (c) 2025 Zac Poorman
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

112
README.md
View File

@ -1,3 +1,111 @@
# loctight-public
# LOCTight
Simple FOSS solution for keeping a pc active and unlocked for a period of time and then locking.
![Tests](https://github.com/OOCAZ/loctight-public/actions/workflows/tests.yml/badge.svg)
![Lint](https://github.com/OOCAZ/loctight-public/actions/workflows/lint.yml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
A simple, open-source program to keep your PC active and open for a specified amount of time, then automatically lock your computer.
---
## Why LOCTight?
LOCTight was inspired by a real-world need: a professor who struggled to keep their computer awake during lectures, but also wanted the machine to lock automatically when stepping away. This tool is designed to solve exactly that problem—keeping your computer awake when you need it, and locking it when you don't.
---
## Features
- Keeps your computer active for a user-defined period
- Automatically locks your workstation after the timer expires
- Extensively tested on **Windows**
- **macOS** and **Linux** support coming soon!
- Comprehensive test suite with automated CI/CD
---
## Testing & Quality
LOCTight includes a comprehensive test suite to ensure reliability:
- **13+ unit tests** covering core functionality
- **Automated CI/CD** via GitHub Actions
- Tested on multiple platforms (Windows, Linux, macOS)
- Tested on Python 3.9, 3.10, 3.11, and 3.12
### Running Tests
```bash
# Install test dependencies
pip install -e ".[test]"
# Run tests
pytest tests/ -v
# Run tests with coverage
pytest tests/ -v --cov=src --cov-report=term --cov-report=html
```
See [tests/README.md](tests/README.md) for more details.
---
## Customization & Commercial Use
LOCTight is released under the [MIT License](LICENSE), so you are free to use, modify, and distribute it.
**Need custom features or branding for your company or school?**
Visit [loctight.dev](https://loctight.dev) for a quote on customizing the softwares aesthetics or functionality to fit your organizations needs.
Customizations can include:
- Custom timer durations and scheduling options
- Additional capabilities such as notifications, reporting, or integrations
- Enhanced security features
- Unique branding and user interface adjustments
- Any other functionality your organization requires
---
## Get Support & Track Issues
If you encounter bugs, have feature requests, or need support, please use the [GitHub Issues](../../issues) page for this repository.
This helps us track and resolve problems efficiently, and allows the community to contribute solutions and suggestions.
---
## Contributing
We welcome contributions from the community!
If you have ideas for new features, improvements, or bug fixes, feel free to fork the repository and submit a pull request.
Some ideas for contributions include:
- Adding new timer options or scheduling features
- Implementing additional platform support (macOS, Linux)
- Expanding functionality with new capabilities
- Improving the user interface or accessibility
Check the [issues](../../issues) page for open requests or to suggest your own.
---
## Get Started
1. Clone or download this repository.
2. Ensure you have Python 3 installed on your machine.
- You can download Python from [python.org](https://www.python.org/downloads/).
3. Install the required dependencies by running:
```bash
pip install pyautogui ttkbootstrap darkdetect
```
4. Run `loctight.py` with Python 3.
5. Follow the on-screen instructions.
---
## License
MIT License. See [LICENSE](LICENSE) for details.
---
_For custom builds, or business inquiries, visit [loctight.dev](https://loctight.dev)._

22
pyproject.toml Normal file
View File

@ -0,0 +1,22 @@
[project]
name = "LOCTight"
version = "1.1.0"
authors = [{ name="OOCAZ (Zac Poorman)", email="poortexanlegos@gmail.com" }]
dependencies = [
"pyautogui",
"ttkbootstrap",
"darkdetect"
]
[project.optional-dependencies]
test = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0"
]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"flake8>=6.0.0",
"black>=23.0.0",
"isort>=5.12.0"
]

249
src/loctight.py Normal file
View File

@ -0,0 +1,249 @@
#! python3
# Written by OOCAZ (Zac Poorman)
# LOCTight - A simple timer to keep your computer active and open.
import os
import subprocess
import sys
import threading
import time
from sys import platform
from tkinter import messagebox
# Windows-specific import
if sys.platform == "win32":
import ctypes
import darkdetect
import pyautogui
import ttkbootstrap as tb
from ttkbootstrap.constants import *
if sys.platform.startswith("linux"):
if "DISPLAY" not in os.environ:
print("Error: No DISPLAY environment variable set. GUI features will not work.")
sys.exit(1)
def lock_workstation():
"""Lock the workstation based on the current platform."""
if platform == "linux" or platform == "linux2":
# Try multiple Linux screen lockers in order of preference
lockers = [
["loginctl", "lock-session"],
["xdg-screensaver", "lock"],
["gnome-screensaver-command", "--lock"],
["dm-tool", "lock"],
["xscreensaver-command", "-lock"],
]
for locker in lockers:
try:
subprocess.run(locker, check=True, capture_output=True)
return # Success, exit function
except (subprocess.CalledProcessError, FileNotFoundError):
continue # Try next locker
# If all fail, show warning but don't crash
messagebox.showwarning(
"Screen Lock Failed",
"Could not lock screen. No supported screen locker found.",
)
elif platform == "darwin":
subprocess.run(
[
"/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession",
"-suspend",
],
check=True,
)
else: # Windows
ctypes.windll.user32.LockWorkStation()
def jiggle(x, checks):
a = 0
while a < x and timer_running[0]:
b = 0
while b < 6 and timer_running[0]:
pyautogui.moveRel(0, 1, duration=0.001)
time.sleep(10)
b += 1
a += 1
if checks == 0 and timer_running[0]:
lock_workstation()
paused = [False]
def pause_timer():
if timer_running[0]:
paused[0] = not paused[0]
if paused[0]:
pause_button.config(text="Resume Timer")
else:
pause_button.config(text="Pause Timer")
def countdown(variable, checks):
minutes = variable
seconds = 0
while minutes > 0 and timer_running[0]:
seconds = 59
update_time_label(minutes - 1, seconds)
pyautogui.moveRel(0, 1, duration=0.001)
while seconds >= 0 and timer_running[0]:
while paused[0] and timer_running[0]:
time.sleep(0.1)
update_time_label(minutes - 1, seconds)
time.sleep(1)
seconds -= 1
minutes -= 1
if timer_running[0]:
update_time_label(0, 0)
# Check the IntVar at the end, not at the start! That way user can change mind
if checks.get() == 0:
lock_workstation()
timer_running[0] = False
paused[0] = False
pause_button.config(text="Pause Timer", state=tb.DISABLED)
enable_buttons()
def update_time_label(m, s):
# Only update if timer is running or resetting to 00:00
if timer_running[0] or (m == 0 and s == 0):
time_str = f"Time Left: {m:02}:{s:02}"
label3.config(text=time_str)
def start_timer(duration):
if not timer_running[0]:
timer_running[0] = True
paused[0] = False
pause_button.config(text="Pause Timer", state="normal")
disable_buttons()
t = threading.Thread(
target=countdown,
args=(duration, checks),
daemon=True,
)
t.start()
def cancel_timer():
timer_running[0] = False
paused[0] = False
pause_button.config(text="Pause Timer", state="disabled")
enable_buttons()
update_time_label(0, 0)
entry.delete(0, tb.END) # Clear the entry field when cancelling
def short():
start_timer(30)
def longs():
start_timer(60)
def custom():
try:
Ctime = int(entry.get())
if Ctime <= 0:
raise ValueError
start_timer(Ctime)
except ValueError:
messagebox.showerror(
"Invalid Input", "Please enter a positive integer for minutes."
)
def disable_buttons():
button1.config(state="disabled")
button2.config(state="disabled")
button3.config(state="disabled")
# Remove start_button if not present
cancel_button.config(state="normal")
pause_button.config(state="normal")
def enable_buttons():
button1.config(state="normal")
button2.config(state="normal")
button3.config(state="normal")
# Remove start_button if not present
cancel_button.config(state="disabled")
pause_button.config(state="disabled")
theme = "darkly" if darkdetect.isDark() else "flatly"
window = tb.Window(themename=theme) # "auto" matches system light/dark mode
window.title("LOCTight")
window.geometry("500x400")
window.resizable(True, True)
main_frame = tb.Frame(window, padding=30)
main_frame.pack(expand=True, fill="both")
label1 = tb.Label(
main_frame,
text="Select a time to keep your computer active and open.\nCheck the box to leave unlocked after time ends.",
anchor="center",
justify="center",
wraplength=400,
)
label1.pack(pady=(0, 20))
button_frame = tb.Frame(main_frame)
button_frame.pack(pady=5)
button1 = tb.Button(button_frame, text="30 Minute Timer", command=short)
button1.grid(row=0, column=0, padx=10)
button2 = tb.Button(button_frame, text="1 Hour Timer", command=longs)
button2.grid(row=0, column=1, padx=10)
custom_frame = tb.Frame(main_frame)
custom_frame.pack(pady=10)
entry = tb.Entry(custom_frame, width=10)
entry.grid(row=0, column=0, padx=(0, 10))
button3 = tb.Button(custom_frame, text="Custom (min)", command=custom)
button3.grid(row=0, column=1)
checks = tb.IntVar(value=0)
chkbtn = tb.Checkbutton(
main_frame,
text="Leave Computer Unlocked",
variable=checks,
onvalue=1,
offvalue=0,
)
chkbtn.pack(pady=10)
label3 = tb.Label(
main_frame, text="Time Left: 00:00", anchor="center", relief="groove", width=25
)
label3.pack(pady=10)
action_frame = tb.Frame(main_frame)
action_frame.pack(pady=10)
pause_button = tb.Button(
action_frame, text="Pause Timer", command=pause_timer, state="disabled"
)
pause_button.grid(row=0, column=0, padx=10)
cancel_button = tb.Button(
action_frame, text="Cancel Timer", command=cancel_timer, state="disabled"
)
cancel_button.grid(row=0, column=1, padx=10)
timer_running = [False]
paused = [False]
if __name__ == "__main__":
window.mainloop()

69
tests/README.md Normal file
View File

@ -0,0 +1,69 @@
# LOCTight Tests
This directory contains the unit tests for the LOCTight application.
## Running Tests
### Local Testing
To run the tests locally:
```bash
# Install test dependencies
pip install pytest pytest-cov
# Run all tests
pytest tests/ -v
# Run with coverage report
pytest tests/ -v --cov=src --cov-report=term --cov-report=html
# Run specific test file
pytest tests/test_core_logic.py -v
```
### CI/CD Testing
Tests are automatically run on every push and pull request via GitHub Actions across multiple platforms:
- Ubuntu (Linux)
- Windows
- macOS
And multiple Python versions:
- Python 3.9
- Python 3.10
- Python 3.11
- Python 3.12
## Test Structure
### test_core_logic.py
Tests the core timer and control logic without requiring GUI components:
- **TestTimerFunctions**: Tests for jiggle, countdown, pause/resume, and start/cancel logic
- **TestInputValidation**: Tests for custom timer input validation
- **TestPlatformLocking**: Tests for platform-specific workstation locking
- **TestButtonStateManagement**: Tests for UI state management logic
## Test Coverage
The tests focus on:
- Timer countdown logic
- Mouse jiggle functionality
- Pause and resume behavior
- Input validation
- Platform-specific locking mechanisms
- Button state management
## Continuous Integration
The project uses GitHub Actions for automated testing:
- **tests.yml**: Runs the full test suite on multiple platforms and Python versions
- **lint.yml**: Checks code quality with flake8, black, and isort
See `.github/workflows/` for the full CI configuration.

3
tests/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""
LOCTight test suite
"""

344
tests/test_core_logic.py Normal file
View File

@ -0,0 +1,344 @@
"""
Unit tests for LOCTight core functionality
Tests the timer and mouse jiggle functions in isolation
"""
import subprocess
import sys
import time
import unittest
from unittest.mock import MagicMock, Mock, patch
# Mock pyautogui to avoid X11/display requirements in CI
# Create a proper mock module with the attributes we need
mock_pyautogui = MagicMock()
mock_pyautogui.moveRel = MagicMock()
sys.modules["pyautogui"] = mock_pyautogui
class TestTimerFunctions(unittest.TestCase):
"""Test the core timer logic"""
@patch("time.sleep")
@patch("pyautogui.moveRel")
def test_jiggle_performs_mouse_movements(self, mock_move, mock_sleep):
"""Test that jiggle moves mouse correct number of times"""
# Create a simple version of jiggle for testing
timer_running = [True]
def jiggle(x, checks):
a = 0
while a < x and timer_running[0]:
b = 0
while b < 6 and timer_running[0]:
mock_move(0, 1, duration=0.001)
mock_sleep(10)
b += 1
a += 1
jiggle(1, 1)
self.assertEqual(mock_move.call_count, 6)
self.assertEqual(mock_sleep.call_count, 6)
@patch("time.sleep")
def test_countdown_logic(self, mock_sleep):
"""Test countdown decrements properly"""
timer_running = [True]
update_count = [0]
def update_time_label(m, s):
update_count[0] += 1
def countdown(minutes):
m = minutes
s = 0
while m > 0 and timer_running[0]:
s = 59
update_time_label(m - 1, s)
while s >= 0 and timer_running[0]:
update_time_label(m - 1, s)
mock_sleep(1)
s -= 1
m -= 1
update_time_label(0, 0)
# Test 1 minute countdown
countdown(1)
# Should update label 61 times (60 seconds + initial + final)
self.assertEqual(update_count[0], 62)
def test_time_formatting(self):
"""Test time label format"""
def format_time(m, s):
return f"Time Left: {m:02}:{s:02}"
self.assertEqual(format_time(5, 30), "Time Left: 05:30")
self.assertEqual(format_time(0, 9), "Time Left: 00:09")
self.assertEqual(format_time(59, 59), "Time Left: 59:59")
self.assertEqual(format_time(0, 0), "Time Left: 00:00")
def test_pause_toggle_logic(self):
"""Test pause toggle behavior"""
timer_running = [True]
paused = [False]
def pause_timer():
if timer_running[0]:
paused[0] = not paused[0]
return paused[0]
return None
# First pause
result = pause_timer()
self.assertTrue(result)
self.assertTrue(paused[0])
# Resume
result = pause_timer()
self.assertFalse(result)
self.assertFalse(paused[0])
# When timer not running, should not toggle
timer_running[0] = False
result = pause_timer()
self.assertIsNone(result)
self.assertFalse(paused[0])
def test_cancel_timer_logic(self):
"""Test cancel timer behavior"""
timer_running = [True]
paused = [True]
def cancel_timer():
timer_running[0] = False
paused[0] = False
return timer_running[0], paused[0]
t, p = cancel_timer()
self.assertFalse(t)
self.assertFalse(p)
def test_start_timer_logic(self):
"""Test start timer can only start when not already running"""
timer_running = [False]
def start_timer():
if not timer_running[0]:
timer_running[0] = True
return True
return False
# First start should succeed
result = start_timer()
self.assertTrue(result)
self.assertTrue(timer_running[0])
# Second start should fail (already running)
result = start_timer()
self.assertFalse(result)
self.assertTrue(timer_running[0])
class TestInputValidation(unittest.TestCase):
"""Test input validation logic"""
def test_custom_timer_validation(self):
"""Test custom timer input validation"""
def validate_custom_time(value):
try:
time_val = int(value)
if time_val <= 0:
return False, "Must be positive"
return True, time_val
except ValueError:
return False, "Must be integer"
# Valid inputs
valid, val = validate_custom_time("45")
self.assertTrue(valid)
self.assertEqual(val, 45)
valid, val = validate_custom_time("1")
self.assertTrue(valid)
self.assertEqual(val, 1)
# Invalid inputs
valid, msg = validate_custom_time("abc")
self.assertFalse(valid)
self.assertEqual(msg, "Must be integer")
valid, msg = validate_custom_time("-10")
self.assertFalse(valid)
self.assertEqual(msg, "Must be positive")
valid, msg = validate_custom_time("0")
self.assertFalse(valid)
self.assertEqual(msg, "Must be positive")
def test_preset_timers(self):
"""Test preset timer values"""
SHORT_TIMER = 30
LONG_TIMER = 60
self.assertEqual(SHORT_TIMER, 30)
self.assertEqual(LONG_TIMER, 60)
class TestPlatformLocking(unittest.TestCase):
"""Test platform-specific lock commands"""
@patch("sys.platform", "win32")
@patch("src.loctight.platform", "win32")
def test_windows_lock(self):
"""Test Windows lock workstation call"""
# Mock ctypes module
mock_ctypes = MagicMock()
with patch.dict("sys.modules", {"ctypes": mock_ctypes}):
# Import after mocking to ensure ctypes is available
import importlib
import src.loctight
# Inject the mock into the module
src.loctight.ctypes = mock_ctypes
from src.loctight import lock_workstation
lock_workstation()
mock_ctypes.windll.user32.LockWorkStation.assert_called_once()
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "darwin")
def test_macos_lock(self, mock_subprocess):
"""Test macOS lock command"""
from src.loctight import lock_workstation
lock_workstation()
mock_subprocess.assert_called_once_with(
[
"/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession",
"-suspend",
],
check=True,
)
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "linux")
def test_linux_lock_first_locker_succeeds(self, mock_subprocess):
"""Test Linux lock succeeds on first locker"""
from src.loctight import lock_workstation
# Mock successful lock on first try
mock_subprocess.return_value = MagicMock()
lock_workstation()
# Should only call the first locker
mock_subprocess.assert_called_once_with(
["loginctl", "lock-session"], check=True, capture_output=True
)
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "linux")
def test_linux_lock_fallback_mechanism(self, mock_subprocess):
"""Test Linux lock tries multiple lockers on failure"""
from src.loctight import lock_workstation
# First two lockers fail, third succeeds
mock_subprocess.side_effect = [
FileNotFoundError(), # loginctl not found
subprocess.CalledProcessError(1, "xdg-screensaver"), # xdg fails
MagicMock(), # gnome-screensaver succeeds
]
lock_workstation()
# Should have tried three lockers
self.assertEqual(mock_subprocess.call_count, 3)
calls = mock_subprocess.call_args_list
self.assertEqual(calls[0][0][0], ["loginctl", "lock-session"]) # First attempt
self.assertEqual(calls[1][0][0], ["xdg-screensaver", "lock"]) # Second attempt
self.assertEqual(
calls[2][0][0], ["gnome-screensaver-command", "--lock"]
) # Third attempt
@patch("src.loctight.subprocess.run")
@patch("src.loctight.platform", "linux")
@patch("src.loctight.messagebox.showwarning")
def test_linux_lock_all_fail(self, mock_messagebox, mock_subprocess):
"""Test Linux lock handles all lockers failing gracefully"""
from src.loctight import lock_workstation
# All lockers fail
mock_subprocess.side_effect = FileNotFoundError()
lock_workstation()
# Should have tried all 5 lockers
self.assertEqual(mock_subprocess.call_count, 5)
# Should show warning messagebox
mock_messagebox.assert_called_once_with(
"Screen Lock Failed",
"Could not lock screen. No supported screen locker found.",
)
class TestButtonStateManagement(unittest.TestCase):
"""Test button enable/disable logic"""
def test_disable_buttons_on_timer_start(self):
"""Test that starting timer disables appropriate buttons"""
buttons_state = {
"button1": "NORMAL",
"button2": "NORMAL",
"button3": "NORMAL",
"pause": "DISABLED",
"cancel": "DISABLED",
}
def disable_buttons():
buttons_state["button1"] = "DISABLED"
buttons_state["button2"] = "DISABLED"
buttons_state["button3"] = "DISABLED"
buttons_state["pause"] = "NORMAL"
buttons_state["cancel"] = "NORMAL"
disable_buttons()
self.assertEqual(buttons_state["button1"], "DISABLED")
self.assertEqual(buttons_state["button2"], "DISABLED")
self.assertEqual(buttons_state["button3"], "DISABLED")
self.assertEqual(buttons_state["pause"], "NORMAL")
self.assertEqual(buttons_state["cancel"], "NORMAL")
def test_enable_buttons_on_timer_end(self):
"""Test that ending timer enables appropriate buttons"""
buttons_state = {
"button1": "DISABLED",
"button2": "DISABLED",
"button3": "DISABLED",
"pause": "NORMAL",
"cancel": "NORMAL",
}
def enable_buttons():
buttons_state["button1"] = "NORMAL"
buttons_state["button2"] = "NORMAL"
buttons_state["button3"] = "NORMAL"
buttons_state["pause"] = "DISABLED"
buttons_state["cancel"] = "DISABLED"
enable_buttons()
self.assertEqual(buttons_state["button1"], "NORMAL")
self.assertEqual(buttons_state["button2"], "NORMAL")
self.assertEqual(buttons_state["button3"], "NORMAL")
self.assertEqual(buttons_state["pause"], "DISABLED")
self.assertEqual(buttons_state["cancel"], "DISABLED")
if __name__ == "__main__":
unittest.main()