Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Test Application

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip

- name: Run tests
run: |
python -m unittest discover -s tests -v

- name: Check shell script syntax (Linux)
if: runner.os == 'Linux'
run: bash -n package_linux.sh

- name: Check PowerShell script syntax (Windows)
if: runner.os == 'Windows'
run: |
powershell -NoProfile -Command "& { Set-StrictMode -Version Latest; .\package_win.ps1 -WhatIf }"
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.agents
__pycache__
*.exe
*.out
*.bin
.claude
dist
build
31 changes: 31 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Testing the Terminal Notes Application

## Running Tests Locally

To run the test suite, execute:

```bash
python -m unittest discover -s tests -v
```

Or to run a specific test file:

```bash
python -m unittest tests.test_terminalnotesapp -v
```

## Test Coverage

The test suite covers:

- Note loading and saving functionality
- Note creation, modification, and deletion
- Import and export to JSON files
- Edge cases and error handling
- User input validation
- File handling scenarios

## GitHub Actions

This repository includes GitHub Actions workflow for continuous integration.
See `.github/workflows/test.yml` for details.
223 changes: 223 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import json
import os
import sys
import time

NOTES_FILE = "notes.json"

def load_notes():
"""Load notes from JSON file."""
if not os.path.exists(NOTES_FILE):
return []
with open(NOTES_FILE, 'r') as f:
try:
return json.load(f)
except json.JSONDecodeError:
return []

def save_notes(notes):
"""Save notes to JSON file."""
with open(NOTES_FILE, 'w') as f:
json.dump(notes, f, indent=2)


def print_notes():
"""Print all notes."""
notes = load_notes()
if not notes:
print("No notes found.")
return
print("\n===== YOUR NOTES =====")
for note in notes:
print(f"ID: {note['id']}")
print(f"Content: {note['content']}")
print("-" * 40)
print(f"Total notes: {len(notes)}\n")


def export_notes_json():
"""Export notes to a JSON file."""
notes = load_notes()
if not notes:
print("No notes to export.")
return
filename = input("Enter filename to export to (default: notes_export.json): ").strip()
if not filename:
filename = "notes_export.json"
if not filename.endswith('.json'):
filename += '.json'
try:
with open(filename, 'w') as f:
json.dump(notes, f, indent=2)
print(f"Notes exported to {filename}")
except Exception as e:
print(f"Error exporting notes: {e}")


def import_notes_json():
"""Import notes from a JSON file."""
filename = input("Enter filename to import from: ").strip()
if not filename:
print("No filename provided.")
return
if not os.path.exists(filename):
print(f"File '{filename}' not found.")
return
try:
with open(filename, 'r') as f:
imported_notes = json.load(f)
if not isinstance(imported_notes, list):
print("Invalid file format: expected a JSON array.")
return
# Validate each entry has id and content
valid_notes = []
for i, item in enumerate(imported_notes):
if not isinstance(item, dict) or 'id' not in item or 'content' not in item:
print(f"Invalid entry at index {i}: skipping")
continue
valid_notes.append(item)
if not valid_notes:
print("No valid notes found in file.")
return
# Ask user whether to merge or replace
current_notes = load_notes()
print(f"Found {len(valid_notes)} valid notes in {filename}")
print(f"Currently have {len(current_notes)} notes.")
choice = input("Choose action: (m)erge with existing, (r)eplace existing: ").strip().lower()
if choice == 'r' or choice == 'replace':
notes_to_save = valid_notes
# Reassign IDs to avoid conflicts
timestamp_base = int(time.time() * 1000)
for i, note in enumerate(notes_to_save):
note['id'] = timestamp_base + i
action = "replaced"
else: # merge or any other input
notes_to_save = current_notes + valid_notes
# Adjust IDs for imported notes to avoid conflicts
if current_notes:
max_id = max(note['id'] for note in current_notes)
for note in valid_notes:
note['id'] = max_id + 1 + valid_notes.index(note)
action = "merged"
save_notes(notes_to_save)
print(f"Notes {action} successfully. Total notes now: {len(notes_to_save)}")
except json.JSONDecodeError as e:
print(f"Invalid JSON in file: {e}")
except Exception as e:
print(f"Error importing notes: {e}")

def create_note():
"""Create a new note."""
notes = load_notes()
content = input("Enter note content: ").strip()
if not content:
print("Note cannot be empty.")
return
# Generate a unique ID using current timestamp in milliseconds
note_id = int(time.time() * 1000)
notes.append({"id": note_id, "content": content})
save_notes(notes)
print(f"Note created with ID {note_id}")

def remove_note():
"""Remove a note by ID."""
notes = load_notes()
if not notes:
print("No notes to remove.")
return
print("Select a note to remove:")
for note in notes:
content = note['content']
if len(content) > 50:
display_content = content[:50] + "..."
else:
display_content = content
print(f"ID: {note['id']} - Content: {display_content}")
try:
note_id = int(input("Enter note ID to remove: "))
except ValueError:
print("Invalid ID.")
return
# Find note with matching ID
for i, note in enumerate(notes):
if note["id"] == note_id:
del notes[i]
save_notes(notes)
print(f"Note ID {note_id} removed.")
return
print(f"No note found with ID {note_id}")

def change_note():
"""Change the content of an existing note."""
notes = load_notes()
if not notes:
print("No notes to change.")
return
print("Select a note to change:")
for note in notes:
content = note['content']
if len(content) > 50:
display_content = content[:50] + "..."
else:
display_content = content
print(f"ID: {note['id']} - Content: {display_content}")
try:
note_id = int(input("Enter note ID to change: "))
except ValueError:
print("Invalid ID.")
return
# Find note with matching ID
for note in notes:
if note["id"] == note_id:
new_content = input("Enter new content: ").strip()
if not new_content:
print("Note cannot be empty.")
return
note["content"] = new_content
save_notes(notes)
print(f"Note ID {note_id} updated.")
return
print(f"No note found with ID {note_id}")

def menu():
print("============")
print(" MENU ")
print("============")
print("1) Create New Note")
print("2) Remove Note")
print("3) Change Note")
print("4) Print All Notes")
print("5) Export Notes to JSON")
print("6) Import Notes from JSON")
print("7) Exit")

def main():
while True:
try:
menu()
choice = int(input("Enter one of the choices: "))
if choice == 1:
create_note()
elif choice == 2:
remove_note()
elif choice == 3:
change_note()
elif choice == 4:
print_notes()
elif choice == 5:
export_notes_json()
elif choice == 6:
import_notes_json()
elif choice == 7:
print("Goodbye!")
sys.exit(0)
else:
print("Invalid choice. Please enter a number between 1 and 7.")
except ValueError:
print("Enter a valid integer!")
except KeyboardInterrupt:
print("\nGoodbye!")
sys.exit(0)

if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions notes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"id": 1001,
"content": "Test note 1"
},
{
"id": 1002,
"content": "Test note 2 with longer content to test"
}
]
34 changes: 34 additions & 0 deletions package_linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# package_linux.sh
# Script to package the terminal notes app into a standalone executable using PyInstaller for Linux/macOS

# Ensure we are in the script's directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"

# Check if Python3 is available
if ! command -v python3 &> /dev/null; then
echo "Error: python3 is not installed or not in PATH. Please install Python 3.6+."
exit 1
fi

# Check if PyInstaller is installed; if not, install it
if ! command -v pyinstaller &> /dev/null; then
echo "PyInstaller not found. Installing via pip..."
python3 -m pip install --upgrade pyinstaller
if [[ $? -ne 0 ]]; then
echo "Failed to install PyInstaller."
exit 1
fi
fi

# Build the executable
echo "Building executable with PyInstaller..."
pyinstaller --onefile --name terminalnotesapp main.py
if [[ $? -ne 0 ]]; then
echo "PyInstaller failed."
exit 1
fi

echo "Build completed. Executable is in ./dist/terminalnotesapp"
echo "You can distribute the executable from the dist folder."
34 changes: 34 additions & 0 deletions package_win.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env pwsh
# package_win.ps1
# Script to package the terminal notes app into a standalone executable using PyInstaller for Windows PowerShell 5.1+

# Ensure we are in the script's directory
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
Set-Location $scriptDir

# Check if Python is available
if (-not (Get-Command python -ErrorAction SilentlyContinue)) {
Write-Error "Python is not installed or not in PATH. Please install Python 3.6+."
exit 1
}

# Check if PyInstaller is installed; if not, install it
if (-not (Get-Command pyinstaller -ErrorAction SilentlyContinue)) {
Write-Host "PyInstaller not found. Installing via pip..."
python -m pip install --upgrade pyinstaller
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to install PyInstaller."
exit 1
}
}

# Build the executable
Write-Host "Building executable with PyInstaller..."
pyinstaller --onefile --name terminalnotesapp main.py
if ($LASTEXITCODE -ne 0) {
Write-Error "PyInstaller failed."
exit 1
}

Write-Host "Build completed. Executable is in ./dist/terminalnotesapp.exe"
Write-Host "You can distribute the .exe file from the dist folder."
Loading
Loading