-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtui-shop.py
executable file
·400 lines (328 loc) · 13.8 KB
/
tui-shop.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
#!/usr/bin/python3
# IMPORTS
import urwid # TUI
from pyfiglet import Figlet # Logo
from shutil import get_terminal_size, rmtree # File stuff + terminal size
from os import path, system, geteuid, mkdir # os stuff
from json import load, dump, loads # Json
from random import choice # Used for random app and splashes
from github import Github, GithubException # Github support
from thefuzz import process # Fuzzy search
from requests.exceptions import ConnectionError, ReadTimeout # For low internet error
# CONSTANT PATHS
FILEPATH = '/opt/tui-shop'
REPONAME = 'rainbow-sh/tui-shop-repo'
# Create opt path if it doesn't exist
if not path.exists(FILEPATH): mkdir(FILEPATH)
# FUNCTIONS
# Change displayed widget
def change_screen(name:str): loop.widget = urwid.Filler(globals()[name])
# Update install dict
def update_installed(name:str):
global install, installscreen # Globals
install = load(open(FILEPATH + '/installed.json')) # Get install dict from file
# Update installed screen
installscreen[2].widget_list.pop()
installscreen[2].widget_list.append(urwid.Pile([urwid.Button(('inv', f'{i[0] + 1}. {i[1]}'), on_press=page_gen(i[1])) for i in enumerate(sorted(install))]))
# Set widget to download page
page_gen(name)('')
# Clear screen
def clear_screen():
global loop # Globals
loop.screen.clear() # Stop loop drawing
system('clear') # Clear screen
# Add characters to input box on search screens
def search(k:str):
global listscreen, loop # Globals
try:
if loop.widget.base_widget.widget_list == listscreen.widget_list: # If scrren is set to search
if k=='backspace': listscreen[2][2][0].set_text(listscreen[2][2][0].get_text()[0][:-1]) # Backspace handling
elif len(listscreen[2][2][0].text) <= 15 and len(k)==1: # Other keys
listscreen[2][2][0].set_text(listscreen[2][2][0].get_text()[0] + k)
except TypeError: pass # Skip mouse input
# Download app
def download(name:str):
global apps, install, loop # Globals
clear_screen() # Clear
# Dependencies
for dep in apps[name]["dependencies"]: # For each dependency
system(f'apt -y install {dep} || pacman -Syu --noconfirm {dep} || dnf -y install {dep} || zypper -n {dep} || nix {dep}') # Download it
print() # Linebreak
# Clone repo
if apps[name]['repo']:
system(f'git clone https://github.com/{apps[name]["repo"]}.git {FILEPATH}/tmp')
print() # Linebreak
# Check if installed successfully
if system(' && '.join([f'cd {FILEPATH}/tmp'] + apps[name]['actions']['install'])) == 0:
# Add app to installed
with open(FILEPATH + '/installed.json', 'w') as f:
if name not in install: install.append(name) # If not updating
dump(install, f) # Dump to file
rmtree(FILEPATH + '/tmp') # Remove tmp directory
input('\nP\u001b[1mPress enter to continue...\u001b[0m') # Click to continue
update_installed(name) # Update install dict
# If not
else:
rmtree(FILEPATH + '/tmp') # Remove tmp directory
input('\n\u001b[1m\u001b[31mProcess failed (press enter)\u001b[0m') # Warning
# Remove app
def remove(name:str):
global apps, install # Globals
clear_screen() # Clear
# Clone repo
system(f'git clone https://github.com/{apps[name]["repo"]}.git {FILEPATH}/tmp')
print() # Linebreak
# Check if removed successfully
if system(' && '.join([f'cd {FILEPATH}/tmp'] + apps[name]['actions']['remove'])) == 0:
# Remove app from installed
with open(FILEPATH + '/installed.json', 'w') as f:
install.remove(name)
dump(install, f)
rmtree(FILEPATH + '/tmp') # Remove tmp directory
input('\n\u001b[1mPress enter to continue...\u001b[0m') # Click to continue
update_installed(name) # Update install dict
# If not
else:
rmtree(FILEPATH + '/tmp') # Remove tmp directory
input('\n\u001b[1m\u001b[31mProcess failed (press enter)\u001b[0m') # Warning
# Generate download page
def page_gen(name:str):
global loop # Globals
def page(_):
global apps, loop, install, config, github # Globals
# Render logo
logo = []
for line in apps[name]['logo']:
for i in line:
if i=='n': logo.append(' ') # If n then no color
else: logo.append((i, '█')) # Else add color
logo.append('\n') # Linebreak
# Get data from github
ghdata = github.get_repo(apps[name]['repo'])
# Generate page
downpage = urwid.Pile((
BAR,
LINEBREAK,
urwid.Pile((
urwid.Text(('bold', name), align=urwid.CENTER), # Name
urwid.Text(apps[name]['description'], align=urwid.CENTER), # description
LINEBREAK,
urwid.Text(logo, align=urwid.CENTER), # Logo
LINEBREAK
))
))
if name in install: # If installed
downpage.widget_list.append(urwid.Columns((
urwid.Button('REMOVE', on_press=lambda _: remove(name)), # Remove button
urwid.Button('UPDATE', on_press=lambda _: download(name)) # Update button
), 3))
# If not installed
else: downpage.widget_list.append(urwid.Button('INSTALL', on_press=lambda _: download(name))) # Install button
downpage.widget_list.append(LINEBREAK) # Linebreak
# Github data
downpage.widget_list.append(urwid.Columns((
urwid.Pile((
urwid.Text(('bold', 'Author'), align=urwid.CENTER), # Author
urwid.Text(apps[name]['repo'].split('/')[0], align=urwid.CENTER)
)),
urwid.Pile((
urwid.Text(('bold', 'Stars'), align=urwid.CENTER), # Stars
urwid.Text(str(ghdata.stargazers_count), align=urwid.CENTER)
)),
urwid.Pile((
urwid.Text(('bold', 'Written in'), align=urwid.CENTER), # Language
urwid.Text(ghdata.language if ghdata.language else '???', align=urwid.CENTER)
)),
urwid.Pile((
urwid.Text(('bold', 'License'), align=urwid.CENTER), # License
urwid.Text(ghdata.raw_data['license']['spdx_id'] if ghdata.raw_data['license'] else 'None', align=urwid.CENTER)
)),
)))
loop.widget = urwid.Filler(downpage) # Update page
# Return function pointer
try: return page
except Exception: # If there's an error with the app
loop.stop()
print("\u001b[1m\u001b[31mTHERE'S SOMETHING WRONG WITH THAT APP, TRY TO DOWNLOAD ANOTHER ONE\u001b[0m")
quit(1) # Quit with error
# Generate search page
def gen_search(_):
global listscreen # Globals
# Get query
q = listscreen[2][2][0].text
# Make a new screen
searchscreen = urwid.Pile((
BAR,
LINEBREAK,
urwid.Pile((
urwid.Text(('bold', 'SEARCH'), align=urwid.CENTER), # Title
# Search box
LINEBREAK,
urwid.Columns((
urwid.Text(''),
urwid.Button('OK', on_press=gen_search) # OK button
)),
LINEBREAK,
# App list
urwid.Pile([urwid.Button(('inv', f'{i[0] + 1}. {i[1][0]}'), on_press=page_gen(i[1][0])) for i in enumerate(process.extract(q, apps.keys(), limit=5) if q else [(i, None) for i in apps.keys()])]) # Results
))
))
listscreen = searchscreen # Set search screen to new search screen
change_screen('listscreen') # Update screen
# VARIABLES
# Config
if not path.exists(FILEPATH + '/config.json'): # If file doesn't exist
with open(FILEPATH + '/config.json', 'w+') as f: f.write('''{
"github": ""
}''') # Create
config = load(open(FILEPATH + '/config.json')) # Load
# Github client
github = Github(config['github'])
# Apps dict
try: apps = {i.name.replace('.json', '').replace('_', ' '):loads(i.decoded_content.decode('utf-8')) for i in github.get_repo(REPONAME).get_contents('apps')}
except GithubException: # If github token invalid
print(f'\u001b[1m\u001b[31mPUT A VALID GITHUB TOKEN IN THE CONFIG ({FILEPATH}/config.json)\u001b[0m')
exit(1) # Quit with error
except (ConnectionError, ReadTimeout): # If no internet connection
print(f'\u001b[1m\u001b[31mLOW INTERNET CONNECTION\u001b[0m')
exit(1) # Quit with error
# Install dict
if not path.exists(FILEPATH + '/installed.json'): # If file doesn't exist
with open(FILEPATH + '/installed.json', 'w+') as f: f.write('["tui-shop"]') # Create
install = load(open(FILEPATH + '/installed.json')) # Load
# CONSTANTS
LINEBREAK = urwid.Text('') # Urwid text linebreak
BAR = urwid.Columns((
urwid.Button(('bold', 'HOME'), on_press=lambda _: change_screen('mainscreen')),
urwid.Button(('bold', 'SEARCH'), on_press=lambda _: change_screen('listscreen')),
urwid.Button(('bold', 'INSTALLED'), on_press=lambda _: change_screen('installscreen')),
urwid.Button(('bold', 'ABOUT'), on_press=lambda _: change_screen('aboutscreen')),
urwid.Button(('bold', 'QUIT'), on_press=lambda _: change_screen('quitscreen'))
), 5) # Bar (self-explanatory button names)
# SCREENS
# Load screen
loadscreen = urwid.Pile((
urwid.Text(Figlet().renderText('TUI-SHOP'), align=urwid.CENTER), # Logo
urwid.Text(choice(('Welcome!', 'Your favorite package manager!', 'Also try apt!', 'Made with python!')), align=urwid.CENTER), # Splash
urwid.Button('OK', on_press=lambda _: change_screen('mainscreen')) # OK button
))
rand = choice(list(apps.keys())) # Random app
# Render logo
logo = []
for line in apps[rand]['logo']:
for i in line:
if i=='n': logo.append(' ') # If n then no color
else: logo.append((i, '█')) # Else add color
logo.append('\n') # Linebreak
# Home screen
mainscreen = urwid.Pile((
BAR,
LINEBREAK,
urwid.Text(('bold', 'HOME'), align=urwid.CENTER), # Title
LINEBREAK,
urwid.Columns((
urwid.Pile((
urwid.Text(('bold', 'NEWS'), align=urwid.CENTER), # Title
LINEBREAK,
urwid.Text(github.get_repo(REPONAME).get_contents('news.txt').decoded_content, align=urwid.CENTER), # Get news from github
)),
urwid.Text('|\n' * get_terminal_size().lines, align=urwid.CENTER),
urwid.Pile((
urwid.Text(('bold', 'RANDOM APP'), align=urwid.CENTER), # Title
LINEBREAK,
urwid.Pile((
urwid.Text(('bold', rand), align=urwid.CENTER), # Name
urwid.Text(logo, align=urwid.CENTER), # Logo
urwid.Text(apps[rand]["description"], align=urwid.CENTER), # Description
urwid.Button(('bold', 'GO'), on_press=page_gen(rand)) # GO button
))
))
))
))
# Standard search screen
listscreen = urwid.Pile((
BAR,
LINEBREAK,
urwid.Pile((
urwid.Text(('bold', 'SEARCH'), align=urwid.CENTER), # Title
LINEBREAK,
# Search box
urwid.Columns((
urwid.Text(''),
urwid.Button('OK', on_press=gen_search) # OK button
)),
LINEBREAK,
# App list
urwid.Pile([urwid.Button(('inv', f'{i[0] + 1}. {i[1]}'), on_press=page_gen(i[1])) for i in enumerate(apps.keys())])
))
))
# Installed screen
installscreen = urwid.Pile((
BAR,
LINEBREAK,
urwid.Pile((
urwid.Text(('bold', 'INSTALLED'), align=urwid.CENTER), # Title
LINEBREAK,
urwid.Pile([urwid.Button(('inv', f'{i[0] + 1}. {i[1]}'), on_press=page_gen(i[1])) for i in enumerate(sorted(install))]) # App list
))
))
# About screen
aboutscreen = urwid.Pile((
BAR,
LINEBREAK,
urwid.Text(('bold', 'ABOUT'), align=urwid.CENTER), # Title
LINEBREAK,
urwid.Pile((
urwid.Text(('bold', 'Creator'), align=urwid.CENTER),
urwid.Text('https://github.com/G_cat101', align=urwid.CENTER),
LINEBREAK,
urwid.Text(('bold', 'Repo'), align=urwid.CENTER),
urwid.Text('https://github.com/rainbow-sh/tui-shop', align=urwid.CENTER),
LINEBREAK,
urwid.Text(('bold', 'App repo'), align=urwid.CENTER),
urwid.Text('https://github.com/rainbow-sh/tui-shop-repo', align=urwid.CENTER),
LINEBREAK,
urwid.Text(('bold', 'Made by rainbow.sh (https://github.com/rainbow-sh) and licensed under GPL-3.0'), align=urwid.CENTER),
LINEBREAK,
)) # About (self-explanatory fields)
))
# Quit screen
quitscreen = urwid.Pile((
BAR,
LINEBREAK,
urwid.Text(('bold', 'QUIT'), align=urwid.CENTER), # Title
LINEBREAK,
urwid.Text('Are you sure?', align=urwid.CENTER), # ?
LINEBREAK,
urwid.Columns((
urwid.Button('Yes', on_press=lambda _: (loop.stop(), quit(0))), # Stops the loop and quits
urwid.Button('No', on_press=lambda _: change_screen('mainscreen')) # Return to home screen
), 5)
))
# If not imported
if __name__=='__main__':
# Create loop
loop = urwid.MainLoop(urwid.Filler(loadscreen), (
('bold', 'default,bold', 'default', 'bold'),
('inv', 'black', 'light gray'),
('r', 'dark red', ''),
('l', 'light green', ''),
('b', 'dark blue', ''),
('y', 'yellow', ''),
('p', 'dark magenta', ''),
('w', 'light gray', ''),
('g', 'dark gray', ''),
('d', 'black', '')
), unhandled_input=search)
# If root
if geteuid()==0:
try: loop.run() # Run loop
except KeyboardInterrupt: quit(0) # Quit on ctrl+c
except AttributeError: quit(1) # Idk why this bug happends but it just quits with error
except (ConnectionError, ReadTimeout): # If no internet connection
print(f'\u001b[1m\u001b[31mLOW INTERNET CONNECTION\u001b[0m')
exit(1) # Quit with error
else: # If not root
print('\u001b[1m\u001b[31mRUN AS ROOT\u001b[0m')
quit(1) # Quit with error
# THE END