vappman 0.6__py3-none-any.whl → 0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vappman/PowerWindow.py CHANGED
@@ -588,7 +588,7 @@ class Window:
588
588
  def alert(self, title='ALERT', message='', height=1, width=80):
589
589
  """Alert box"""
590
590
  def mod_key(key):
591
- return 7 if key == 10 else key
591
+ return 7 if key in (10, curses.KEY_ENTER) else key
592
592
 
593
593
  # need 3 extra cols for rectangle (so we don't draw in southeast corner)
594
594
  # and 3 rows (top/prompt/bottom)
vappman/main.py CHANGED
@@ -6,7 +6,7 @@ Interactive, visual thin layer atop appman
6
6
  # pylint: disable=broad-exception-caught,consider-using-with
7
7
  # pylint: disable=too-many-instance-attributes,too-many-branches
8
8
  # pylint: disable=too-many-return-statements,too-many-statements
9
-
9
+ # pylint: disable=consider-using-in,too-many-nested-blocks
10
10
  # pylint: disable=wrong-import-position,disable=wrong-import-order
11
11
  # import VirtEnv
12
12
  # VirtEnv.ensure_venv(__name__)
@@ -14,13 +14,11 @@ Interactive, visual thin layer atop appman
14
14
  import os
15
15
  import sys
16
16
  import re
17
+ import glob
17
18
  import shutil
18
19
  import subprocess
19
20
  import traceback
20
- import copy
21
- import shutil
22
21
  import curses as cs
23
- from types import SimpleNamespace
24
22
  from vappman.PowerWindow import Window, OptionSpinner
25
23
 
26
24
 
@@ -37,9 +35,10 @@ class Vappman:
37
35
  spin.add_key('help_mode', '? - toggle help screen', vals=[False, True])
38
36
 
39
37
  # EXPAND
40
- other = 'airbou/qscU'
38
+ other = 'airtbou/qxscU'
41
39
  other_keys = set(ord(x) for x in other)
42
40
  other_keys.add(cs.KEY_ENTER)
41
+ other_keys.add(27) # ESCAPE
43
42
  other_keys.add(10) # another form of ENTER
44
43
  self.opts = spin.default_obj
45
44
 
@@ -51,6 +50,9 @@ class Vappman:
51
50
  self.check_preqreqs()
52
51
  self.apps = self.cmd_dict('appman list')
53
52
  self.installs = self.get_installed() # dict keyed by app
53
+ self.appman_dir = self.get_appman_dir()
54
+ self.dot_desktop_dir = self.get_dot_desktop_dir()
55
+ self.terminal_emulator = None
54
56
  self.win = Window(head_line=True, body_rows=len(self.apps)+20, head_rows=10,
55
57
  keys=spin.keys ^ other_keys, mod_pick=self.mod_pick)
56
58
 
@@ -96,6 +98,43 @@ class Vappman:
96
98
  rv = self.cmd_dict('appman files --byname')
97
99
  return rv
98
100
 
101
+ @staticmethod
102
+ def get_appman_dir():
103
+ """ Try to figure out where the apps are stored. """
104
+
105
+ appman_dir = None, None
106
+ try:
107
+ config_dir = os.getenv('XDG_CONFIG_HOME')
108
+ if not config_dir:
109
+ config_dir = os.path.join(os.getenv('HOME'), '.config')
110
+ config_file = os.path.join(config_dir, 'appman', 'appman-config')
111
+ with open(config_file, 'r', encoding='utf-8') as fh:
112
+ appman_dir = fh.read().strip()
113
+ appman_dir = os.path.join(os.getenv('HOME'), appman_dir)
114
+ os.listdir(appman_dir)
115
+ return appman_dir
116
+ except Exception as exc:
117
+ print(f'NOTE: cannot get appman dir; tried below {appman_dir!r}; {exc}')
118
+ print(' Check if contents of ~/config/appman/appman-config'
119
+ + ' is the subdir of $HOME w your appman apps')
120
+ return None
121
+
122
+ @staticmethod
123
+ def get_dot_desktop_dir():
124
+ """ Try to figure out where the .desktop files are stored. """
125
+
126
+ try:
127
+ data_dir = os.getenv('XDG_DATA_HOME')
128
+ if not data_dir:
129
+ data_dir = os.path.join(os.getenv('HOME'), '.local', 'share')
130
+ dot_dir = os.path.join(data_dir, 'applications')
131
+ os.listdir(dot_dir)
132
+ return dot_dir
133
+ except Exception as exc:
134
+ print(f'NOTE: cannot get .desktop dir; tried below {data_dir!r}; {exc}')
135
+ print(' Check if contents of ~/.local/share/applications for .desktop files')
136
+ return None
137
+
99
138
  def main_loop(self):
100
139
  """ TBD """
101
140
 
@@ -108,18 +147,20 @@ class Vappman:
108
147
  # EXPAND
109
148
  lines = [
110
149
  'ALWAYS AVAILABLE:',
111
- ' q - quit program (CTL-C disabled)',
150
+ ' q or x - quit program (CTL-C disabled)',
112
151
  ' a - about (more info about app)',
113
152
  ' s - sync (update appman itself)',
114
153
  ' c - clean (remove unneeded files/folters)',
115
154
  ' U - update ALL installed apps',
116
155
  ' / - filter apps',
117
156
  ' ENTER = install, remove, or return from help',
157
+ ' ESC = clear filter and jump to top',
118
158
  'CONTEXT SENSITIVE:',
119
159
  ' i - install uninstalled app',
120
160
  ' r - remove installed app',
121
161
  ' b - backup installed app',
122
162
  ' u - update installed app',
163
+ ' t - test by opening a terminal emulator and launching the app'
123
164
  ' o - overwrite app from its backup',
124
165
 
125
166
  ]
@@ -128,6 +169,12 @@ class Vappman:
128
169
  else:
129
170
  def wanted(line):
130
171
  return not self.filter or self.filter.search(line)
172
+ def version_of(app):
173
+ # ◆ krita | 5.2.2 | appimage-type2 | 355 MiB
174
+ fields = self.installs[app].split('|')
175
+ if len(fields) >= 2:
176
+ return fields[1].strip()
177
+ return '?version?'
131
178
 
132
179
  # self.win.set_pick_mode(self.opts.pick_mode, self.opts.pick_size)
133
180
  self.win.set_pick_mode(True)
@@ -135,11 +182,11 @@ class Vappman:
135
182
  for app, line in self.installs.items():
136
183
  if app in self.apps:
137
184
  line = self.apps[app]
138
- if wanted(line):
139
- line = '✔✔✔' + line[1:]
185
+ if wanted(line[2:]):
186
+ line = f'✔✔✔ {app} [{version_of(app)}] :{line.split(':', maxsplit=1)[1]}'
140
187
  self.win.add_body(line)
141
188
  for app, line in self.apps.items():
142
- if app not in self.installs and wanted(line):
189
+ if app not in self.installs and wanted(line[2:]):
143
190
  self.win.add_body(line)
144
191
  self.win.render()
145
192
 
@@ -149,15 +196,17 @@ class Vappman:
149
196
  def get_keys_line(self):
150
197
  """ TBD """
151
198
  # EXPAND
152
- filt = self.prev_filter if self.prev_filter else '{No-Filt}'
153
- line = 'KEYS:'
199
+ line = ''
154
200
  for key, verb in self.actions.items():
155
- line += f' {key}:{verb}'
201
+ if key[0] == verb[0]:
202
+ line += f' {verb}'
203
+ else:
204
+ line += f' {key}:{verb}'
156
205
  # or EXPAND
157
- line += f' ?:help q:quit a:about s:sync c:clean U:upd /{filt} '
206
+ line += f' ?:help quit about sync clean Upd /{self.prev_filter} '
158
207
  # for action in self.actions:
159
208
  # line += f' {action[0]}:{action}'
160
- return line
209
+ return line[1:]
161
210
 
162
211
  def get_actions(self, line):
163
212
  """ Determine the type of the current line and available commands."""
@@ -173,6 +222,7 @@ class Vappman:
173
222
  actions['b'] = 'bkup'
174
223
  actions['o'] = 'overwr'
175
224
  actions['u'] = 'upd'
225
+ actions['t'] = 'test'
176
226
  else:
177
227
  actions['i'] = 'install'
178
228
 
@@ -185,30 +235,122 @@ class Vappman:
185
235
  """
186
236
  this = Vappman.singleton
187
237
  this.pick_app, this.actions = this.get_actions(line)
188
- keys_line = this.get_keys_line().ljust(this.win.get_pad_width())
189
- this.win.head.pad.addstr(0, 0, keys_line, cs.A_BOLD)
190
-
238
+ header = this.get_keys_line()
239
+ # ASSUME line ends in /....
240
+ parts = header.split('/', maxsplit=1)
241
+ wds = parts[0].split()
242
+ this.win.head.pad.move(0, 0)
243
+ for wd in wds:
244
+ if wd:
245
+ this.win.add_header(wd[0], attr=cs.A_BOLD|cs.A_UNDERLINE, resume=True)
246
+ if wd[1:]:
247
+ this.win.add_header(wd[1:] + ' ', resume=True)
248
+
249
+ this.win.add_header('/', attr=cs.A_BOLD+cs.A_UNDERLINE, resume=True)
250
+ if len(parts) > 1 and parts[1]:
251
+ this.win.add_header(f'{parts[1]}', resume=True)
252
+ _, col = this.win.head.pad.getyx()
253
+ pad = ' ' * (this.win.get_pad_width()-col)
254
+ this.win.add_header(pad, resume=True)
191
255
  return line
192
- # #IF WE WANT TO DO SOMETHING ON THE LINE...
193
- # suffix = ''
194
- # for action in actions:
195
- # suffix += f' {action[0]}:{action}'
196
- # block_char = "\u2588"
197
- # suffix = f'{block_char*5} {suffix}'
198
- # over = len(line) + len(suffix) - this.win.get_pad_width()
199
- # if over < 0:
200
- # line += ' '*(-over)
201
- # elif over > 0:
202
- # line = line[0:-over]
203
-
204
- # return line + suffix
256
+
205
257
  def run_appman(self, cmd):
258
+ """ Run a 'appman' command """
206
259
  Window.stop_curses()
207
260
  os.system(f'clear; stty sane; {cmd};'
208
- + ' /bin/echo -e "\n\n===== Press ENTER for menu ====> \c"; read FOO')
261
+ + r' /bin/echo -e "\n\n===== Press ENTER for menu ====> \c"; read FOO')
209
262
  self.installs = self.get_installed()
210
263
  Window._start_curses()
211
264
 
265
+ @staticmethod
266
+ def launch_desktop_file(desktop_file_path):
267
+ """ Launch the .desktop file using xdg-open in a detached process """
268
+ try:
269
+ trial = ['xdg-open', desktop_file_path]
270
+ subprocess.Popen(trial,
271
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, close_fds=True)
272
+ return None
273
+ except Exception:
274
+ return trial
275
+
276
+ def launch_in_terminal(self, executable):
277
+ """ Find a terminal emulator"""
278
+ if not self.terminal_emulator:
279
+ maybes = [
280
+ [ 'konsole', '--noclose', '-e', '"{command}"'],
281
+ [ 'gnome-terminal', '--', 'bash', '-c', '"{command}"; exec bash' ],
282
+ [ 'xfce4-terminal', '--hold', '--command="{command}"' ],
283
+ [ 'lxterminal', '-e', """bash -c '"{command}"; echo; read -p "Press Enter to close..."'"""],
284
+ # [ terminator', ],
285
+ # [ alacritty', ],
286
+ # [ termite', ],
287
+ # [ urxvt', ],
288
+ # [ sakura', ],
289
+ # [ tilix', ],
290
+ # [ kitty', ],
291
+ # [ hyper', ],
292
+ # [ guake', ],
293
+ # [ yakuake', ],
294
+ ]
295
+ for maybe in maybes:
296
+ if shutil.which(maybe[0]):
297
+ self.terminal_emulator = maybe
298
+ break
299
+ if self.terminal_emulator:
300
+ try:
301
+ trial = []
302
+ for wd in self.terminal_emulator:
303
+ trial.append(wd.replace('{command}', executable))
304
+ subprocess.Popen(trial)
305
+ return None
306
+ except Exception:
307
+ return trial
308
+ return trial
309
+
310
+ def launch_app(self, app):
311
+ """ Try to run an app."""
312
+ # First dig out where it might be installed as a .desktop file
313
+ # by searching the 'remove' script
314
+ def get_unique_words_from_file(file_path):
315
+ seen_words = set()
316
+ with open(file_path, 'r', encoding='utf-8') as file:
317
+ for line in file:
318
+ line_words = line.split()
319
+ for word in line_words:
320
+ if word not in seen_words:
321
+ seen_words.add(word)
322
+ return seen_words
323
+
324
+ failures = []
325
+ executables = []
326
+ try:
327
+ for globname in get_unique_words_from_file(
328
+ os.path.join(self.appman_dir, app, 'remove')):
329
+ results = glob.glob(globname)
330
+ if '/share/applications/AM-ZZZ' in globname:
331
+ for result in results:
332
+ if result.endswith('.desktop'):
333
+ failure = self.launch_desktop_file(result)
334
+ if failure:
335
+ failures.append(' '.join(failure))
336
+ return
337
+ elif '/.local/bin/' in globname:
338
+ for result in results:
339
+ if os.access(result, os.X_OK):
340
+ executables.append(result)
341
+ for executable in executables:
342
+ failure = self.launch_in_terminal(executable)
343
+ if failure:
344
+ failures.append(' '.join(failure))
345
+ return
346
+ except Exception as exc:
347
+ failures += f'cannot find .desktop/executable to run [{exc}]'
348
+ if failures:
349
+ message = ' '.join([f'Cannot launch {app}'] + failures)
350
+ self.win.alert(message=message)
351
+
352
+ self.launch_app(self.pick_app)
353
+
212
354
  def do_key(self, key):
213
355
  """ TBD """
214
356
  if not key:
@@ -217,16 +359,22 @@ class Vappman:
217
359
  if self.opts.help_mode:
218
360
  self.opts.help_mode = False
219
361
  return True
220
- elif self.pick_is_installed:
221
- key = ord('r') # removed installed app
362
+ if self.pick_is_installed:
363
+ key = ord('r') # remove installed app
222
364
  else:
223
365
  key = ord('i') # install uninstalled app
224
366
 
225
367
  if key in self.spin.keys:
226
368
  value = self.spin.do_key(key, self.win)
227
369
  return value
228
-
229
- if key == ord('q'):
370
+
371
+ if key == 27: # ESCAPE
372
+ self.prev_filter = ''
373
+ self.filter = None
374
+ self.win.pick_pos = 0
375
+ return None
376
+
377
+ if key in (ord('q'), ord('x')):
230
378
  self.win.stop_curses()
231
379
  os.system('clear; stty sane')
232
380
  sys.exit(0)
@@ -239,6 +387,10 @@ class Vappman:
239
387
  self.run_appman(f'appman remove {self.pick_app}')
240
388
  return None
241
389
 
390
+ if key == ord('t') and self.pick_is_installed:
391
+ self.launch_app(self.pick_app)
392
+ return None
393
+
242
394
  if key == ord('s'):
243
395
  self.run_appman('appman sync')
244
396
  if key == ord('c'):
@@ -252,11 +404,12 @@ class Vappman:
252
404
  if key == ord('u'):
253
405
  self.run_appman(f'appman update {self.pick_app}')
254
406
  if key == ord('U'):
255
- self.run_appman(f'appman update')
407
+ self.run_appman('appman update')
256
408
  # EXPAND
257
409
 
258
410
  if key == ord('/'):
259
411
  # pylint: disable=protected-access
412
+ start_filter = self.prev_filter
260
413
 
261
414
  prefix = ''
262
415
  while True:
@@ -266,18 +419,22 @@ class Vappman:
266
419
  pattern.strip()
267
420
  if not pattern:
268
421
  self.filter = None
269
- return None
422
+ break
270
423
 
271
424
  try:
272
425
  if re.match(r'^[\-\w\s]*$', pattern):
273
426
  words = pattern.split()
274
- self.filter = re.compile(r'\b' + r'\b.*'.join(words), re.IGNORECASE)
275
- return None
427
+ self.filter = re.compile(r'\b' + r'(|.*\b)'.join(words), re.IGNORECASE)
428
+ break
276
429
  self.filter = re.compile(pattern, re.IGNORECASE)
277
- return None
430
+ break
278
431
  except Exception:
279
432
  prefix = 'Bad regex: '
280
433
 
434
+ if start_filter != self.prev_filter:
435
+ # when filter changes, move to top
436
+ self.win.pick_pos = 0
437
+
281
438
  return None
282
439
  return None
283
440
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vappman
3
- Version: 0.6
3
+ Version: 0.8
4
4
  Summary: A visual wrapper for appman
5
5
  Author-email: Joe Defen <joedef@google.com>
6
6
  License: MIT
@@ -41,7 +41,7 @@ But it does NOT cover:
41
41
  * (-v) version of appman
42
42
  * --force-latest to get the most recent stable release AND
43
43
  all other options and unmentioned commands.
44
-
44
+
45
45
  ## Usage
46
46
  * Run `vappman` from the command line.
47
47
  * It presents some keys available on the top line.
@@ -56,13 +56,20 @@ But it does NOT cover:
56
56
  * Installed apps have prefix '✔✔✔' (i.e., three checks).
57
57
  * Uninstalled apps have prefix '◆' (i.e., a solid diamond).
58
58
  * Enter `/` to enter a "filter" for installed/uninstalled apps, if you wish.
59
- * If you enter plain ole "words", then those words must match the beginning of words
60
- of the apps or descriptions (in order, but not contiguously).
61
- * Or you can enter an regular expression acceptable to python (e.g., `\b` means word
62
- boundary, etc.)
59
+ * If you enter plain ole "words", then those words must match:
60
+ * the start of words on the apps line (in order, but not contiguously) and/or
61
+ * the start of the remainder of the previous word match
62
+ (i.e., `/bit fight` matches `bitfighter`).
63
+ * Or you can enter an regular expression acceptable to python; e.g.,
64
+ * `^` matches the line starting with the app name
65
+ * `\b` matches a word boundary; and so forth.
66
+ * NOTES:
67
+ * `ESC` clears the filter and jumps to the top of the listing.
68
+ * Each time the filter is changed, the position jumps to the top of the listing.
63
69
  * Use `i` to install apps, and `r` to remove apps. When you install or remove an app, `appman` drops out of `curses` mode, runs the `appman` command so you can see the result, and then prompts your to hit ENTER to return to `vappman.
70
+ * Use `t` to "test" an installed app. This launches a terminal emulator and then the app so you can see issues. This is not for daily use obviously, but for after install or when having unknown issues and you wish to start the investigation.
64
71
 
65
- ## Example Screenshot
72
+ ## Example Screenshot (of v0.7 ... current release will vary slightly)
66
73
  ![vappman-with-filter](https://github.com/joedefen/vappman/blob/main/images/vappman-with-filter.png?raw=true).
67
74
 
68
75
  ---
@@ -75,4 +82,8 @@ NOTES: in this example:
75
82
  all the filtered apps; otherwise, the decoration suggests where you are in the
76
83
  partial view of the filtered apps.
77
84
  * the matching installed app has the '✔✔✔' prefix.
85
+ * the fixed top line shows some of the available action keys (e.g., `q` quits the app)
86
+ * use `?` to open the help screen describing all keys (including navigation)
78
87
 
88
+ ## Screen Recording (Intro to vappman based on v0.7)
89
+ [![Screen Recording](https://i9.ytimg.com/vi_webp/NUHYN9_DZtA/mq3.webp?sqp=CMTu4LMG-oaymwEmCMACELQB8quKqQMa8AEB-AHqBYAC4AOKAgwIABABGEogZShRMA8=&rs=AOn4CLBaBrOpAhJkRIQQNNdCzYaqpOYl-Q)](https://youtu.be/NUHYN9_DZtA)
@@ -0,0 +1,9 @@
1
+ vappman/PowerWindow.py,sha256=pQGXsAMeuiHn-vtEiVG0rgW1eCslh3ukC-VrPhH_j3k,28587
2
+ vappman/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ vappman/main.py,sha256=FBTCzbOa9dCyzTHX3ietF9_avrMRareYFxmnd65wc2M,17066
4
+ vappman-0.8.dist-info/LICENSE,sha256=qB9OdnyyF6WYHiEIXVm0rOSdcf8e2ctorrtWs6CC5lU,1062
5
+ vappman-0.8.dist-info/METADATA,sha256=BJMibKDn0HtJFZcvP55MtNLnqK1P77NeTFOHkaDTm7o,4921
6
+ vappman-0.8.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
7
+ vappman-0.8.dist-info/entry_points.txt,sha256=7_1MiUvkCJoElLePOCJYqhkQN4xmadBRQCKupOwzt90,46
8
+ vappman-0.8.dist-info/top_level.txt,sha256=5_Gb5oZh7s2-i62gLXZ6INVALAV9D0-yqh0TvNqpPC4,8
9
+ vappman-0.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (70.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,9 +0,0 @@
1
- vappman/PowerWindow.py,sha256=OLCX-RkbJZ2wwaY7M-4Eo9PQuR95TWrGNIY8DdVRpsE,28567
2
- vappman/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- vappman/main.py,sha256=rSWpH_l_IgH3jR4HSG-H8lvmuuAZWStPo-AMtSIsZYY,10483
4
- vappman-0.6.dist-info/LICENSE,sha256=qB9OdnyyF6WYHiEIXVm0rOSdcf8e2ctorrtWs6CC5lU,1062
5
- vappman-0.6.dist-info/METADATA,sha256=pWiQZo-R_ePgEX3Yz9oAcNty2XBmYCArV--3ErQd_HI,3850
6
- vappman-0.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
- vappman-0.6.dist-info/entry_points.txt,sha256=7_1MiUvkCJoElLePOCJYqhkQN4xmadBRQCKupOwzt90,46
8
- vappman-0.6.dist-info/top_level.txt,sha256=5_Gb5oZh7s2-i62gLXZ6INVALAV9D0-yqh0TvNqpPC4,8
9
- vappman-0.6.dist-info/RECORD,,
File without changes